@honeybbq/teamspeak 1.0.0-beta.17 → 1.0.0-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1131 +1,106 @@
1
- <div align="center">
2
-
3
- # @honeybbq/teamspeak
4
-
5
- **An independent TeamSpeak client protocol library written in pure TypeScript.**
6
-
7
- Works with TeamSpeak 3 and TeamSpeak 6 servers. No proprietary SDK, no copied code.
8
-
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
- [![npm](https://img.shields.io/npm/v/@honeybbq/teamspeak)](https://www.npmjs.com/package/@honeybbq/teamspeak)
11
- [![npm beta](https://img.shields.io/npm/v/@honeybbq/teamspeak/beta?label=beta)](https://www.npmjs.com/package/@honeybbq/teamspeak?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)
13
- [![Node Version](https://img.shields.io/node/v/@honeybbq/teamspeak)](https://github.com/HoneyBBQ/teamspeak-js/blob/main/package.json)
14
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/HoneyBBQ/teamspeak-js/blob/main/LICENSE)
15
-
16
- </div>
17
-
18
- **[API reference](https://honeybbq.github.io/teamspeak-js/)**
19
-
20
- Upgrading from 0.x? The package was renamed; see the [migration guide](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/migrating-to-1.0.md).
21
-
22
- ## Features
23
-
24
- - **Full protocol handshake** — ECDH key exchange, RSA puzzle, EAX-encrypted transport
25
- - **Command & notification system** — Send commands, receive server events
26
- - **Event-driven API** — Register handlers for text messages, client enter/leave, channel moves, kicks, etc.
27
- - **Voice data** — Send Opus voice packets (codec 4 & 5)
28
- - **File transfers** — Upload, download, and delete files on the server
29
- - **Address resolution** — SRV records, TSDNS, and direct addresses; server nicknames on request (`nicknameLookup: true`), since that lookup goes to named.myteamspeak.com
30
- - **Middleware** — Pluggable command and event middleware chains
31
- - **Built-in rate limiter** — Token-bucket throttling to prevent server-side flood kicks
32
- - **Identity management** — Generate, import/export, and upgrade security level of identities
33
- - **Full command set** — 111 typed functions covering every command the voice protocol answers
34
- - **Reconnection** — Opt-in retry with backoff, tuned for TeamSpeak's antiflood ban
35
- - **Streams** — TeamSpeak 6 screen share signalling: find, publish and watch streams, with SDP and ICE as strings for any WebRTC stack
36
- - **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
37
- - **Custom transports** — Supply your own socket, for proxies, tunnels or tests
38
- - **ESM with types** — Ships ES modules with full TypeScript declarations
39
- - **Zero native deps** — Pure TypeScript, no native addons required
40
-
41
- ## Installation
42
-
43
- 1.0 is in beta, published under the `beta` tag. A plain `npm install` still
44
- gets 0.3, so ask for the beta:
1
+ <p align="center">
2
+ <a href="https://honeybbq.github.io/teamspeak-js/">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/HoneyBBQ/teamspeak-js/main/docs/assets/banner-dark.svg">
5
+ <img alt="teamspeak-js" src="https://raw.githubusercontent.com/HoneyBBQ/teamspeak-js/main/docs/assets/banner-light.svg" width="460">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <strong>An independent TeamSpeak client protocol library written in pure TypeScript.</strong><br>
12
+ Works with TeamSpeak 3 and TeamSpeak 6 servers. No proprietary SDK, no copied code.
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://www.npmjs.com/package/@honeybbq/teamspeak"><img alt="npm version" src="https://img.shields.io/npm/v/@honeybbq/teamspeak"></a>
17
+ <a href="https://github.com/HoneyBBQ/teamspeak-js/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/HoneyBBQ/teamspeak-js/actions/workflows/ci.yml/badge.svg"></a>
18
+ <a href="https://scorecard.dev/viewer/?uri=github.com/HoneyBBQ/teamspeak-js"><img alt="OpenSSF Scorecard" src="https://api.scorecard.dev/projects/github.com/HoneyBBQ/teamspeak-js/badge"></a>
19
+ <a href="https://github.com/HoneyBBQ/teamspeak-js/blob/main/package.json"><img alt="Node.js version" src="https://img.shields.io/node/v/@honeybbq/teamspeak"></a>
20
+ <a href="https://github.com/HoneyBBQ/teamspeak-js/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
21
+ </p>
22
+
23
+ <p align="center">
24
+ <a href="https://honeybbq.github.io/teamspeak-js/">Docs</a> ·
25
+ <a href="https://honeybbq.github.io/teamspeak-js/documents/guide.html">Guide</a> ·
26
+ <a href="https://github.com/HoneyBBQ/teamspeak-js/blob/main/CHANGELOG.md">Changelog</a> ·
27
+ <a href="https://honeybbq.github.io/teamspeak-js/documents/migrating-to-1.0.html">Migrating to 1.0</a>
28
+ </p>
29
+
30
+ ## Why this library
31
+
32
+ - **The voice protocol, end to end.** ECDH key exchange, the RSA puzzle, EAX-encrypted transport, and Opus voice (codecs 4 and 5), in TypeScript with no native addons.
33
+ - **ServerQuery too.** The same typed commands run over raw TCP, SSH (with host-key verification) or WebQuery, so an administration script and a voice bot share one vocabulary.
34
+ - **Typed commands and events.** Every command the voice protocol answers is a typed function, and the server's notifications arrive as typed events with pluggable middleware.
35
+ - **File transfers.** Upload, download and delete files on the server, with avatars and icons covered.
36
+ - **TeamSpeak 6 screen-share signalling.** Find, publish and watch streams, with SDP and ICE as plain strings for any WebRTC stack.
37
+ - **Built for long-running bots.** Opt-in reconnection tuned for TeamSpeak's antiflood ban, and a token-bucket rate limiter that keeps you clear of flood kicks.
38
+ - **Identities and server facts.** Generate, import, export and upgrade identities; read the server version, its uid hash (SHA-1 on TS3, SHA-256 on TS6) and its name, icon and banner.
39
+ - **TeamSpeak 3 and 6, ESM with types.** Pure TypeScript, zero native dependencies, full declarations, and a custom-transport hook for proxies and tests.
40
+
41
+ ## Install
45
42
 
46
43
  ```bash
47
- npm install @honeybbq/teamspeak@beta
48
- # or
49
- pnpm add @honeybbq/teamspeak@beta
44
+ npm install @honeybbq/teamspeak
50
45
  ```
51
46
 
52
- Everything in this README is the 1.0 API. You need Node.js 22.12 or newer.
47
+ 1.0 is in beta and published under the `latest` and `beta` tags; Node.js 22.12 or newer is required. Upgrading from 0.x? See [Migrating to 1.0](https://honeybbq.github.io/teamspeak-js/documents/migrating-to-1.0.html).
53
48
 
54
- ## Quick Start
49
+ ## Quick start
55
50
 
56
51
  ```typescript
57
52
  import { Client, generateIdentity } from "@honeybbq/teamspeak/client";
58
53
 
59
- // Generate a new identity (or load an existing one)
60
54
  const identity = await generateIdentity(8);
61
-
62
- // Create the client
63
55
  const client = new Client(identity, "localhost", "TSBot", {
64
56
  serverPassword: process.env.TEAMSPEAK_SERVER_PASSWORD,
65
57
  defaultChannel: "Lobby",
66
- defaultChannelPassword: process.env.TEAMSPEAK_DEFAULT_CHANNEL_PASSWORD,
67
58
  });
68
59
 
69
- // Register event handlers
70
60
  client.on("connected", ({ reconnected }) => {
71
- console.log(reconnected ? "Reconnected to server!" : "Connected to server!");
72
- });
73
-
74
- client.on("textMessage", (msg) => {
75
- console.log(`[${msg.invokerName}]: ${msg.message}`);
61
+ console.log(reconnected ? "Reconnected" : "Connected");
76
62
  });
77
-
63
+ client.on("textMessage", (msg) => console.log(`[${msg.invokerName}]: ${msg.message}`));
78
64
  client.on("disconnected", ({ error, willReconnect }) => {
79
65
  console.log("Disconnected:", error?.message ?? "clean", willReconnect ? "(reconnecting)" : "");
80
66
  });
81
67
 
82
- // Connect; resolves once the handshake completes.
83
68
  await client.connect({ signal: AbortSignal.timeout(15_000) });
84
-
85
- // Stay connected until interrupted
86
- process.on("SIGINT", async () => {
87
- await client.disconnect();
88
- });
89
- ```
90
-
91
- ## API Overview
92
-
93
- ### Client Lifecycle
94
-
95
- | Method | Description |
96
- | --------------------------------------------- | ------------------------------------------------- |
97
- | `new Client(identity, addr, nickname, opts?)` | Create a new client |
98
- | `connect({ signal? })` | Connect; resolves once the handshake completes |
99
- | `disconnect({ signal? })` | Leave the server and close the connection |
100
- | `status` | `"disconnected"`, `"connecting"` or `"connected"` |
101
- | `clientID` | Our clid, assigned by the server |
102
- | `channelID` | The channel we are in |
103
- | `serverCapabilities` | Server version, family and uid hash |
104
- | `uid` | Our own uid, hashed as the server does |
105
-
106
- `connect()` rejects with an `InvalidAddressError` for an address that cannot be
107
- one — no host, or a port outside 1–65535 — before anything starts; a host name
108
- that does not resolve fails with a `TeamspeakError` whose `cause` is the lookup's
109
- error. A `connect()` called while `disconnect()` is still saying goodbye waits
110
- for it to finish, then connects.
111
-
112
- Commands, raw `exec`/`execNoWait`, voice and whispers need a connected client:
113
- until `connect()` resolves, and from the moment `disconnect()` begins, they fail
114
- with a `NotConnectedError` and send nothing.
115
-
116
- ### Events
117
-
118
- Name an event with an `EventName` constant, or with its string: the two are the
119
- same, and the listener's payload type follows either.
120
-
121
- ```typescript
122
- import { EventName } from "@honeybbq/teamspeak/client";
123
-
124
- client.on(EventName.TextMessage, (message) => console.log(message.message));
125
- client.on("textMessage", (message) => console.log(message.message)); // the same
126
- ```
127
-
128
- | Method | Description |
129
- | ------------------------------------------ | -------------------------------------------------------------- |
130
- | `on("connected", handler)` | Fires when fully connected (`ConnectedEvent`) |
131
- | `on("disconnected", handler)` | Fires on disconnect (`DisconnectedEvent`) |
132
- | `on("textMessage", handler)` | Fires on text messages |
133
- | `on("clientEnter", handler)` | Fires when a client joins |
134
- | `on("clientLeave", handler)` | Fires when a client leaves |
135
- | `on("clientMoved", handler)` | Fires when a client moves channels |
136
- | `on("clientUpdated", handler)` | Fires when a client's properties change (`ClientUpdatedEvent`) |
137
- | `on("clientServerGroupAdded", handler)` | Fires when a client joins a server group |
138
- | `on("clientServerGroupRemoved", handler)` | Fires when a client leaves a server group |
139
- | `on("clientChannelGroupChanged", handler)` | Fires when a client's channel group changes |
140
- | `on("channelCreated", handler)` | Fires when a channel is created |
141
- | `on("channelEdited", handler)` | Fires when a channel's properties are edited |
142
- | `on("channelMoved", handler)` | Fires when a channel is moved or reordered |
143
- | `on("channelDeleted", handler)` | Fires when a channel is deleted |
144
- | `on("channelDescriptionChanged", handler)` | Fires when a channel's description changes |
145
- | `on("channelPasswordChanged", handler)` | Fires when a channel's password changes |
146
- | `on("serverEdited", handler)` | Fires when the server's settings are edited |
147
- | `on("channelSubscribed", handler)` | Fires when the bot is subscribed to a channel |
148
- | `on("channelUnsubscribed", handler)` | Fires when the bot is unsubscribed from a channel |
149
- | `on("pluginCommand", handler)` | Fires on a plugin command (`sendPluginCommand`) |
150
- | `on("clientChatComposing", handler)` | Fires when a client is typing a private message |
151
- | `on("kicked", handler)` | Fires when the bot is kicked |
152
- | `on("poked", handler)` | Fires when poked by a client |
153
- | `on("voiceData", handler)` | Fires on incoming voice data |
154
-
155
- `on()` and `once()` return a function that removes the listener, which is also
156
- a `Disposable` (`using listener = client.on(...)`). Listeners run one at a
157
- time, in the order their events arrived, so a listener may call the client and
158
- await an answer. `disconnected` delivers `undefined` after `disconnect()`, and
159
- the cause otherwise — a `ConnectionTimeoutError`, or a `RemovedError` for a kick, a ban
160
- or a server stopping. `await using client = new Client(...)` disconnects when
161
- the scope ends.
162
-
163
- `connected` is delivered after `connect()` resolves, so a listener added right
164
- after `await client.connect()` still hears it. `disconnect()` waits a second at
165
- most for the server to acknowledge its goodbye, less if its `signal` aborts
166
- sooner, and resolves once `disconnected` has been delivered. A `connect()` waits
167
- for an earlier session's `disconnected` to be delivered before it starts, so no
168
- listener or `events()` iterator of the new session hears the old one end.
169
-
170
- A listener that throws does not stop the others: the event still reaches the
171
- listeners after it, and the exception surfaces as an `uncaughtException`, as an
172
- EventEmitter listener's would.
173
-
174
- Voice is the exception to "every event in order": while more than 256
175
- deliveries are waiting, voice frames are dropped (and counted in a warning at
176
- most every ten seconds) rather than queued without bound.
177
-
178
- `events(name, { signal })` is the same as an async iterator, buffering events
179
- that arrive while the loop body runs. The buffer is unbounded (voice excepted,
180
- as above), so stop iterating when you stop reading. Aborting `signal` stops
181
- listening and makes the pending and later `next()` calls reject with the
182
- signal's reason, as Node's `events.on` does; `break`ing out of the loop, or a
183
- disconnect, ends it normally:
184
-
185
- ```typescript
186
- for await (const msg of client.events("textMessage", { signal })) {
187
- await reply(msg);
188
- }
189
- ```
190
-
191
- `notification` is the catch-all: it delivers every notification the server
192
- sends, as it arrived, the ones that also have a typed event among them, after
193
- that event. It is how to reach one the SDK does not model; `NotificationName`
194
- names the known ones:
195
-
196
- ```typescript
197
- import { NotificationName } from "@honeybbq/teamspeak";
198
-
199
- client.on("notification", (n) => {
200
- if (n.name === NotificationName.ChannelEdited) console.log(n.params);
201
- });
202
- ```
203
-
204
- ### Commands
205
-
206
- Every command the voice protocol answers is a typed function, grouped by area.
207
- Each is addressed by branded ids, so a `ChannelID` cannot be passed where a
208
- `ClientID` is meant. The constructors — `asChannelID(5)`, `asClientID(3)` and
209
- the rest — throw a `RangeError` for anything but an integer in the id's wire
210
- range: 0–65535 for a `ClientID` and the file transfer ids, 0–2^32−1 for a
211
- `PermissionID` (an `IconID` also takes the signed form), 0–2^64−1 for the
212
- `bigint` ids. Required arguments are positional; everything optional
213
- goes in one options object, always the last argument, which also carries
214
- `signal` and `timeoutMs` to abandon the command or bound how long it waits.
215
-
216
- ```typescript
217
- import {
218
- listChannels,
219
- createChannel,
220
- moveClient,
221
- kickClient,
222
- sendChannelMessage,
223
- addServerGroupPermission,
224
- iterateClientDatabase,
225
- permissionByName,
226
- PermissionName,
227
- asClientID,
228
- Codec,
229
- Reason,
230
- } from "@honeybbq/teamspeak";
231
-
232
- const channels = await listChannels(client, { signal: AbortSignal.timeout(5_000) });
233
-
234
- const cid = await createChannel(client, {
235
- name: "Lounge",
236
- topic: "the quiet one",
237
- codec: Codec.OpusVoice,
238
- isPermanent: true,
239
- });
240
-
241
- await moveClient(client, client.clientID, cid, { password: "channel-password" });
242
- await sendChannelMessage(client, "hello");
243
- await kickClient(client, someone, Reason.KickChannel, { reasonMessage: "take a break" });
244
-
245
- await addServerGroupPermission(client, sgid, permissionByName(PermissionName.ClientTalkPower), 50, {
246
- skip: true,
247
- });
248
-
249
- for await (const entry of iterateClientDatabase(client)) {
250
- console.log(entry.nickname, entry.lastConnectedAt);
251
- }
252
- ```
253
-
254
- A command that sends content — `createChannel`'s channel, `editChannel`'s
255
- settings, `updateClient`, `editClient`, `editClientDatabase`,
256
- `addBan`'s `BanFields` — takes it as its own
257
- argument, and `{ signal, timeoutMs }` as a separate one after it: set among the
258
- content, they are not read.
259
-
260
- Options objects use camelCase names (`maxClients` for `channel_maxclients`),
261
- and only the fields you set are sent: an unset field leaves the setting alone,
262
- while `""` sends it empty — `editChannel(client, cid, { password: "" })` clears
263
- a password. A key an options type does not name is never sent. The exceptions
264
- are `editServer` and `editInstance`, which take the raw `virtualserver_*` and
265
- `serverinstance_*` properties as `getServerInfo` reports them; `PropertyName` names
266
- the keys.
267
-
268
- Results are camelCase too. Timestamps are `Date`s (`undefined` where the server
269
- sends 0 for "never"), durations are milliseconds with an `Ms` suffix
270
- (`idleTimeMs`, `durationMs`), and a permission is a `Permission`, built with
271
- `permissionByID` or `permissionByName`. Small enumerations — `GroupType`,
272
- `Codec`, `PrivilegeKeyType`, `LogLevel` and the rest — are objects of named values with
273
- a type of the same name.
274
-
275
- | Module | Covers |
276
- | ------------ | -------------------------------------------------------------- |
277
- | `channels` | Create, edit, move, delete, subscribe, channel permissions |
278
- | `clients` | List, move, kick, poke, edit, the client database, permissions |
279
- | `server` | Server and instance settings, groups, temp passwords, bindings |
280
- | `groups` | Channel groups, the permission catalogue, privilege keys |
281
- | `moderation` | Bans, complaints, offline messages, text messages, the log |
282
- | `files` | List, delete, rename, create directories, transfer negotiation |
283
-
284
- Many of these commands are answered over the voice protocol with a
285
- notification rather than data rows — `servergrouplist` with
286
- `notifyservergrouplist`, `ftinitupload` with `notifystartupload` after the
287
- command has already succeeded. The client pairs each answer with its command,
288
- so this is invisible to callers; [docs/voice-protocol-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/voice-protocol-responses.md)
289
- has the measured table. A refusal is always thrown, never returned as an empty
290
- result. A list or find with nothing to report answers `[]`. A command that
291
- exists to return something — `createChannel`'s id, `initFileUpload`'s key,
292
- `getClientProperties`'s fields — throws a `NoDataError` when the server answers
293
- without it, rather than returning an id of 0 or an empty object.
294
-
295
- For anything outside the typed set, `client.exec(line, { signal?, timeoutMs? })`
296
- sends a raw command line and resolves to the rows that answer it.
297
-
298
- ### File Transfers
299
-
300
- `uploadFile` and `downloadFile` (from `@honeybbq/teamspeak/client`) negotiate the
301
- transfer and move the bytes in one call, and `listFilesRecursive` (from the
302
- package) lists a directory and everything under it, each entry with its full path:
303
-
304
- ```typescript
305
- import { downloadFile, uploadFile } from "@honeybbq/teamspeak/client";
306
- import { listFilesRecursive } from "@honeybbq/teamspeak";
307
-
308
- await uploadFile(client, cid, "/docs/notes.txt", data);
309
- const bytes = await downloadFile(client, cid, "/docs/notes.txt");
310
- const tree = await listFilesRecursive(client, cid, "/", { maxDepth: 4 });
311
- ```
312
-
313
- They work over a voice `Client`, and over a `QueryClient` on raw TCP or SSH when
314
- passed the file transfer host as `{ host }`; WebQuery refuses every file command.
315
- `downloadFile` takes `{ dest }` to stream into a `Writable` instead of holding
316
- the file, and refuses a file over `maxSize` (256 MiB) in memory.
317
-
318
- To control the two halves, negotiate with the `files` module, then move the bytes over TCP:
319
-
320
- ```typescript
321
- import { Readable } from "node:stream";
322
- import { initFileUpload } from "@honeybbq/teamspeak";
323
-
324
- const info = await initFileUpload(client, cid, "/notes.txt", BigInt(data.length));
325
- await client.uploadFileData(host, info, Readable.from([data]));
69
+ process.on("SIGINT", () => void client.disconnect());
326
70
  ```
327
71
 
328
- An upload does not replace a file already at the path unless passed
329
- `{ overwrite: true }`.
330
-
331
- | Function | Description |
332
- | ---------------------------------------------------- | --------------------------------- |
333
- | `initFileUpload(client, ...)` | Negotiate an upload |
334
- | `initFileDownload(client, ...)` | Negotiate a download |
335
- | `client.uploadFileData(host, info, data, options?)` | Transfer file data to the server |
336
- | `client.downloadFileData(host, info, out, options?)` | Receive file data from the server |
337
-
338
- The transfer functions take `FileTransferOptions`:
339
-
340
- | Option | Description |
341
- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
342
- | `signal` | An `AbortSignal` that abandons the transfer; it rejects with the signal's reason |
343
- | `socketFactory` | Opens the connection instead of a direct TCP dial, to route it through a proxy or tunnel |
344
- | `trace` | Hooks called at each milestone of the transfer (`dialStart`, `dialDone`, `progress`, `done`, `failed`); none sees the transfer key |
345
- | `idleTimeoutMs` | The longest, in milliseconds, a transfer may go without moving a byte, the key write included. Default 30 000; `0` disables it |
346
-
347
- A transfer that goes idle for `idleTimeoutMs` fails with a `FileTransferError`
348
- whose `cause` is a `TimeoutError`, and its connection is destroyed. A download
349
- that sends more bytes than the negotiated size, or ends short of it, fails with
350
- a `FileTransferError` too; bytes past the announced size are never written.
351
-
352
- ### Identity
353
-
354
- ```typescript
355
- import { generateIdentity, parseIdentity } from "@honeybbq/teamspeak/client";
356
-
357
- // Generate a new identity with security level 8
358
- const identity = await generateIdentity(8);
359
-
360
- // Export to string for persistent storage
361
- const exported = identity.export();
362
-
363
- // Import from a previously exported string
364
- const restored = parseIdentity(exported);
365
-
366
- // Raise the security level (CPU-intensive). Identities are immutable: this
367
- // returns a new one with the same key.
368
- const stronger = await identity.upgradeToLevel(10, { signal });
369
-
370
- // The unique identifier, hashed as a TS3 (sha1) or TS6 (sha256) server does
371
- const uid = identity.uid("sha1");
372
- ```
373
-
374
- The private key never leaves the identity except through `export()`:
375
- `toString()`, `toJSON()` and `util.inspect()` describe it without the key.
376
- `parseIdentity` takes exactly what `export()` writes — the key as canonical
377
- base64 of at most 32 bytes, a colon, and a decimal offset that fits in 64 bits
378
- — and throws an `InvalidIdentityError` for anything else.
379
-
380
- ### Options
381
-
382
- ```typescript
383
- import { Client } from "@honeybbq/teamspeak/client";
384
- import { consoleLogger } from "@honeybbq/teamspeak";
385
-
386
- const client = new Client(identity, "ts.example.com", "Bot", {
387
- logger: consoleLogger, // silent unless set
388
- resolver: customResolver,
389
- commandMiddleware: [timing],
390
- eventMiddleware: [filterMiddleware],
391
- serverPassword: process.env.TEAMSPEAK_SERVER_PASSWORD,
392
- defaultChannel: "Lobby",
393
- defaultChannelPassword: process.env.TEAMSPEAK_DEFAULT_CHANNEL_PASSWORD,
394
- });
395
- ```
396
-
397
- Address resolution — the built-in one or a custom `resolver` — has ten seconds
398
- in all: the `signal` a resolver is given aborts then, and the client connects
399
- to the address as given.
400
-
401
- `commandMiddleware` wraps the typed commands: each middleware receives the
402
- request (its command name and parameters) with its options and returns the
403
- answering rows, so it can time, retry or answer a command itself. Raw `exec`
404
- and `execNoWait` lines do not pass through it.
405
-
406
- ```typescript
407
- import type { CommandMiddleware } from "@honeybbq/teamspeak";
408
-
409
- const timing: CommandMiddleware = (next) => async (request, options) => {
410
- const started = Date.now();
411
- try {
412
- return await next(request, options);
413
- } finally {
414
- console.log(request.command, Date.now() - started, "ms");
415
- }
416
- };
417
- ```
418
-
419
- Connection auth options:
420
-
421
- - `serverPassword`: plain-text server password; sent as the TeamSpeak protocol hash during `clientinit`
422
- - `defaultChannel`: default channel name requested during `clientinit`
423
- - `defaultChannelPassword`: plain-text password for `defaultChannel`; sent as the TeamSpeak protocol hash
424
-
425
- ### Client version
426
-
427
- By default `clientinit` sends a recent Windows version. The `version` option
428
- sends another, and `@honeybbq/teamspeak/versions` has one export for each
429
- version that is known to work, named `<Platform>_<version>_<build>`:
430
-
431
- ```typescript
432
- import { Client } from "@honeybbq/teamspeak/client";
433
- import { Windows_3_6_2_1695203293 } from "@honeybbq/teamspeak/versions";
434
-
435
- const client = new Client(identity, "ts.example.com", "Bot", {
436
- version: Windows_3_6_2_1695203293,
437
- });
438
- ```
439
-
440
- A `ClientVersion` is a platform, a version string and a signature, and only
441
- the ones in that entry carry a signature a server accepts. A server refuses a
442
- build below its configured minimum, so an old version fails to connect on a
443
- server that has raised it. `new Client` throws when a field is empty or the
444
- signature is not the base64 of 64 bytes. Importing `@honeybbq/teamspeak/client`
445
- does not load the table; only the entry you import it from does.
446
-
447
- ### Tracing
448
-
449
- `trace` takes hooks that are called at each milestone of the client's life, for timing, metrics, logs or a progress display. For each connection attempt, the one `connect()` starts and each one the reconnect loop makes, the milestones come in this order: `resolveStart`, `resolveDone`, `connectStart`, `connectDone`, `handshakeDone`, `sessionStart`, `welcomeDone`. An attempt that never gets a session ends with `attemptFailed` instead, which says in which phase (`"open"`, `"handshake"` or `"join"`) and why. When a session ends, `sessionEnd` is called with the cause (`undefined` after `disconnect()`), preceded on a `disconnect()` by `disconnectStart` and `disconnectDone`, which says whether the server acknowledged the goodbye. `reconnectScheduled` fires when the reconnect loop is about to wait for its next attempt, and `reconnectStopped` when it ends without a session. Every attempt ends with exactly one of `sessionStart` and `attemptFailed`, and every session with exactly one `sessionEnd`.
450
-
451
- The infos carry what each milestone knows: the resolver's candidates, the local address, the RSA puzzle's level and how long it took, how many packets were sent again, the server's public key (to pin a server's identity), the nickname the server accepted, the server's `initserver` parameters without any secret, and where the client landed with how many channels and clients the welcome delivered. Durations are in milliseconds.
452
-
453
- ```ts
454
- const client = new Client(identity, "ts.example.com", "Bot", {
455
- trace: {
456
- resolveDone: (i) => console.log(`resolved to ${i.target} in ${i.durationMs.toFixed(0)} ms`),
457
- handshakeDone: (i) =>
458
- console.log(`encrypted after ${i.durationMs.toFixed(0)} ms, ${i.resends} resends`),
459
- sessionStart: (i) => console.log(`joined as ${i.nickname} after ${i.durationMs.toFixed(0)} ms`),
460
- welcomeDone: (i) => console.log(`${i.channels} channels, ${i.clients} clients in view`),
461
- attemptFailed: (i) => console.log(`attempt ${i.attempt} failed while ${i.phase}:`, i.error),
462
- sessionEnd: (i) => console.log(`session over after ${(i.durationMs / 1000).toFixed(0)} s`),
463
- reconnectScheduled: (i) => console.log(`retry ${i.attempt} in ${i.delayMs} ms`),
464
- },
465
- });
466
- ```
467
-
468
- A file transfer has a trace of its own, given in the options of `uploadFileData`, `downloadFileData` and `dialFileTransfer` (and of the client's methods that forward to them): `dialStart`, `dialDone`, `progress` (at most once per 64 KiB, and once more at the end with the final count), then exactly one of `done` and `failed`, which names the stage (`"dial"` or `"transfer"`). The transfer key never appears in any of it.
469
-
470
- The rules for a hook, in both traces:
471
-
472
- - It runs synchronously, at the moment of the milestone, and the client waits for it. Keep it quick and hand slow work to your own task.
473
- - What it returns is ignored. A promise it returns is not awaited.
474
- - It must not call `connect()` or `disconnect()`. To give up on an attempt, abort the signal you passed to `connect()`; to leave once connected, call `disconnect()` from a `connected` listener.
475
- - It may read `client.status`, `client.clientID` and the like. `sessionStart` runs before `connect()` resolves and before `connected` is delivered, and `sessionEnd` before `disconnected`.
476
- - If it throws, or the promise it returns rejects, the error is reported the way a throwing event listener's is, as an uncaught exception, and the client carries on as if nothing happened.
477
-
478
- The object is copied when the client is made (or the transfer call starts), so changing it afterwards has no effect.
479
-
480
- ### Diagnostics
481
-
482
- The client logs nothing unless given a `logger`. For tracing, it publishes on
483
- two [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
484
- channels, which cost nothing without a subscriber:
485
-
486
- - `honeybbq:teamspeak:command` (`commandChannel`), a tracing channel: `start`
487
- when a command is sent, `asyncEnd` when its answer arrives, `error` when it
488
- fails.
489
- - `honeybbq:teamspeak:incoming` (`incomingChannel`): every command the server sends.
490
-
491
- Both carry the command's name, never its parameters, which hold password hashes
492
- and privilege keys.
493
-
494
- ### Reconnection
495
-
496
- Off by default: a silent reconnect makes an outage look like a healthy session.
497
- An explicit `disconnect()` never reconnects, whatever the policy says.
498
-
499
- ```typescript
500
- const client = new Client(identity, "ts.example.com", "Bot", {
501
- reconnect: {
502
- backoff: { initialDelayMs: 1_000, maxDelayMs: 60_000, factor: 2, jitter: 0.2 },
503
- // Optional: decide for yourself which failures are worth retrying.
504
- shouldRetry: (err, attempt) => attempt < 10,
505
- },
506
- });
507
- ```
508
-
509
- The defaults are conservative because TeamSpeak **bans an IP after roughly
510
- three rapid connects** — a retry loop without a delay bans itself, and the ban
511
- outlasts the outage it was recovering from. Recovery is also not instant even
512
- when it succeeds: the server holds the dropped session for a while and refuses a
513
- reconnect with `521 too many clones already connected` until it reaps it, so the
514
- loop keeps trying rather than giving up on the first refusal. It stops once a
515
- `connect()` of your own has connected the client, and it does not retry an
516
- address that cannot be one (`InvalidAddressError`).
517
-
518
- ### Custom transports
519
-
520
- `fileTransferSocketFactory` and `socketFactory` are the only seams into the
521
- network layer. Without them the SDK always creates its own sockets, so an
522
- application running behind a proxy has no way through. `socketFactory` returns
523
- a `DatagramSocket` — a `node:dgram` socket, or anything with the few methods
524
- it names — and `fileTransferSocketFactory` any `Duplex` stream, such as the
525
- socket a SOCKS library returns.
526
-
527
- ```typescript
528
- const client = new Client(identity, "ts.example.com", "Bot", {
529
- // UDP, for the voice and command channel.
530
- socketFactory: (family) => createSocket(family === 6 ? "udp6" : "udp4"),
531
- // TCP, dialled separately by each file transfer.
532
- fileTransferSocketFactory: (host, port) => createConnection({ host, port }),
533
- });
534
- ```
535
-
536
- ### Server capabilities
537
-
538
- ```typescript
539
- const caps = client.serverCapabilities;
540
- // {
541
- // version: "6.0.0-beta13.1 [Build: 1790080330]", major: 6, family: "ts6",
542
- // uidAlgorithm: "sha256", name: "TeamSpeak 6 Server", iconID: undefined,
543
- // hostBannerImageURL: undefined, welcomeMessage: "…"
544
- // }
545
- ```
546
-
547
- Read from `initserver` during the handshake, so it costs no extra round trip.
548
- The uid hash matters because TS3 hashes a client's public key with SHA-1 and TS6
549
- with SHA-256 — a client that assumes either one computes an identifier the other
550
- server never sends.
551
-
552
- ### Streams
553
-
554
- TeamSpeak 6 clients can share a screen, a window or a camera. `@honeybbq/teamspeak/stream`
555
- does the signalling of that: it finds the streams of the clients in view, publishes one,
556
- and watches one. The video itself travels between the publisher and each viewer over
557
- WebRTC, which is yours: `Streams` hands you the SDP and the ICE candidates as strings and
558
- takes them back, so any WebRTC stack plugs in, in a browser, in Electron or in Node. The
559
- package has no WebRTC dependency.
560
-
561
- TeamSpeak does not document this protocol. It was verified against the TeamSpeak 6 server
562
- 6.0.0-beta13.1 and the TeamSpeak 6 client 6.0.0-beta5 (macOS), and a TeamSpeak update may
563
- break it. A TeamSpeak 3 server does not answer the stream
564
- commands at all, so `publish` and `watch` throw a `StreamsUnsupportedError` before sending
565
- anything.
566
-
567
- Watching: find a stream in `streams.list()` or wait for `started`, ask to watch it, and
568
- give the publisher's offer to a peer connection. The viewer's connection is receive-only.
569
-
570
- ```ts
571
- import { Streams } from "@honeybbq/teamspeak/stream";
572
-
573
- const streams = new Streams(client);
574
- streams.on("started", (info) => console.log(`${info.name} is streaming`));
575
-
576
- const view = await streams.watch(info, { message: "may I watch?" });
577
-
578
- const pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
579
- pc.ontrack = (event) => (video.srcObject = event.streams[0]);
580
- pc.onicecandidate = (event) => {
581
- if (event.candidate) void view.sendCandidate(event.candidate.toJSON());
582
- };
583
- // Candidates that arrived with the offer are delivered to the first listener.
584
- view.on("candidate", (candidate) => void pc.addIceCandidate(candidate));
585
- view.on("closed", () => pc.close());
586
-
587
- await pc.setRemoteDescription({ type: "offer", sdp: view.offer });
588
- const answer = await pc.createAnswer();
589
- await pc.setLocalDescription(answer);
590
- await view.answer(answer.sdp!);
591
- ```
592
-
593
- Publishing: the publisher makes the offer, one peer connection for each viewer. The server
594
- relays every request to watch, from any channel, and enforces neither `viewerLimit` nor
595
- `accessibility`: deciding who is let in is the publisher's job, and `request` is emitted
596
- for each one, with nothing accepted for you.
597
-
598
- ```ts
599
- const stream = await streams.publish({ name: "My screen", source: "screen" });
600
-
601
- stream.on("request", async (request) => {
602
- if (!allowed(request.viewer)) return request.deny("Not today");
603
-
604
- const pc = new RTCPeerConnection({ iceServers });
605
- for (const track of capture.getTracks()) pc.addTrack(track, capture);
606
- // Always offer audio, even with no track to send (see the interop notes below).
607
- if (!pc.getTransceivers().some((t) => t.receiver.track.kind === "audio")) {
608
- pc.addTransceiver("audio", { direction: "sendonly" });
609
- }
610
- await pc.setLocalDescription(await pc.createOffer());
611
-
612
- const viewer = await request.accept(pc.localDescription!.sdp);
613
- pc.onicecandidate = (event) => {
614
- if (event.candidate) void viewer.sendCandidate(event.candidate.toJSON());
615
- };
616
- viewer.on("answer", (sdp) => pc.setRemoteDescription({ type: "answer", sdp }));
617
- viewer.on("candidate", (candidate) => void pc.addIceCandidate(candidate));
618
- viewer.on("closed", () => pc.close());
619
- });
620
-
621
- await stream.stop();
622
- ```
623
-
624
- A request that nobody has answered can end: the viewer withdraws it or goes out of view, or
625
- the stream ends. `request.on("closed", ({ cause }) => …)` fires then, with `"withdrawn"` or
626
- `"ended"`, so a UI can drop the request without polling `request.state`; `accept` and `deny`
627
- then throw. It is not emitted for a request that was accepted or denied.
628
-
629
- #### Interop with the TeamSpeak 6 client
630
-
631
- What follows was observed with the TeamSpeak 6 client 6.0.0-beta5 (macOS) against a
632
- TeamSpeak 6 server 6.0.0-beta13.1.
633
-
634
- - **Offer audio, always.** When you publish, the client answers an offer only if it has an
635
- `m=audio` section. For a video-only offer it never answers: it shows a fallback picture and
636
- keeps the "Join" button, which then does nothing, because the server already counts the
637
- viewer as joined. Add a send-only audio transceiver even when there is no audio track; an
638
- audio section before the video section was accepted. `accept` and `ViewerPeer.sendOffer`
639
- send an offer without audio unchanged, and emit a `"warning"` event on `Streams` with the
640
- code `"offer-without-audio"`.
641
- - **Trim the codecs.** The client's own offers carry one video and one audio section, about
642
- 3.9 KB. A browser's offer with every codec is about 6 KB with audio and video, and about
643
- 4.8 KB for video alone, against a limit of about 9.2 KB for a command line. Narrowing the
644
- codecs keeps room for the candidates and the message:
645
-
646
- ```ts
647
- const video = pc.addTransceiver(track, { direction: "sendonly" });
648
- const codecs = RTCRtpSender.getCapabilities("video")!.codecs;
649
- video.setCodecPreferences(codecs.filter((c) => /^video\/(VP8|H264)$/.test(c.mimeType)));
650
- ```
651
-
652
- - **Accept every request.** A viewer in the client shows "waiting" until the publisher
653
- answers. When it withdraws and asks again, that is a new request, with a new `JoinRequest`.
654
- - **A channel switch ends the viewing, not the stream.** The client leaves a stream it
655
- watches when it changes channel, so the publisher sees that viewer leave. The server does not end a stream when its publisher or a viewer changes channel.
656
- - Denying, stopping, leaving, disconnecting and several viewers at once, some of them the
657
- client and some the SDK, behaved as they do between SDK clients.
658
-
659
- ```ts
660
- streams.on("warning", (w) => console.warn(`${w.code}: ${w.message}`));
661
- ```
662
-
663
- Things to know:
664
-
665
- - A client may run several streams, so every stream is known by its id. `list()` is what
666
- this client has heard of. A stream that ends while its publisher is out of the channel is
667
- reported to nobody there, and stays listed until the connection ends.
668
- - A stream survives its publisher changing channel, with no event. It ends when the
669
- publisher stops it or disconnects.
670
- - The server drops a command line longer than about 9.2 KB without an answer. Streams
671
- commands are checked first and throw a `StreamPayloadTooLargeError`; an SDP offer or an
672
- ICE candidate is a few kilobytes at most.
673
- - The server lets anyone stop any stream. `Streams` sends `stopstream` only for streams it
674
- published, and treats a `notifystreamstopped` for any stream as final.
675
- - The server remembers no decision, so a client can ask again, and every ask is relayed.
676
- To refuse for good, pass `rememberDenied: true` (the 1024 most recent uids, in memory
677
- only; a requester out of view is looked up with `clientgetuidfromclid`), or leave the
678
- request unanswered. The server has no way to remove a viewer: closing a viewer's peer
679
- ends your side of it, and stopping the stream ends it for everyone.
680
- - `dispose()` stops the streams published through that `Streams` and leaves the ones it
681
- watched, while the client is connected. A stream whose `publish` gave up after the command
682
- went out is stopped when its announcement arrives, so none is left running unseen.
683
- - A client whose `client_is_streaming` clears, or that comes into view not streaming, has
684
- its streams dropped, after a grace of two seconds: `stopped` with the cause `"gone"`. The
685
- server clears the flag right before it reports the stop, so a stop that arrives in that time
686
- ends the stream with its own cause and reason instead, and a flag set again cancels the drop.
687
- - The bitrate the server reports for a stream is not the one requested.
688
- - A client announces that it streams only with `client_is_streaming`, and a stream that was
689
- running before you came is not announced at all. `Streams` asks for the streams of each
690
- client that sets the flag, once, on a TeamSpeak 6 server.
691
-
692
- ## Design
693
-
694
- The conventions below hold across the whole package. Each is stated once here.
695
-
696
- ### Errors
697
-
698
- An error is one of two kinds:
699
-
700
- - **A refusal the server reports.** It is a `ServerError` carrying the server's
701
- numeric `code`, its text as `serverMessage`, and `extraMessage` and
702
- `failedPermissionID` when the server names them.
703
- - **A condition the SDK detects itself**: not connected, no data, a timeout, a
704
- failed handshake, a failed file transfer. Each has its own class.
705
-
706
- Every error the SDK throws on purpose extends `TeamspeakError`, so
707
- `instanceof TeamspeakError` tells its failures from anything else. A failure
708
- that wraps another keeps it in `cause`. Match a refusal by `error.code` against
709
- `ErrorCode`, and everything else by class:
710
-
711
- ```typescript
712
- import {
713
- ErrorCode,
714
- NotConnectedError,
715
- ServerError,
716
- TeamspeakError,
717
- asChannelID,
718
- moveClient,
719
- } from "@honeybbq/teamspeak";
720
-
721
- try {
722
- await moveClient(client, client.clientID, asChannelID(5), { password: "wrong" });
723
- } catch (err) {
724
- if (err instanceof ServerError && err.code === ErrorCode.ChannelInvalidPassword) {
725
- // ask for the password again
726
- } else if (err instanceof NotConnectedError) {
727
- // the connection is gone
728
- } else if (err instanceof ServerError) {
729
- console.log(err.code, err.serverMessage, err.extraMessage, err.failedPermissionID);
730
- } else if (err instanceof TeamspeakError) {
731
- console.log(err.name, err.message, err.cause);
732
- } else {
733
- throw err;
734
- }
735
- }
736
- ```
737
-
738
- `ErrorCode` names the server's codes as constants. It is the table `errcode`
739
- is in the Go SDK, with the same numbers. A server may send a code it does not
740
- name, so `code` is a plain `number`. `instanceof` also works between two
741
- installed copies of the package.
742
-
743
- Errors live with the code that produces them, so each entry point exports only
744
- the errors its own code throws:
745
-
746
- | Entry point | Errors |
747
- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
748
- | `@honeybbq/teamspeak` (shared by every connection) | `TeamspeakError`, `ServerError`, `CommandTimeoutError`, `NotConnectedError`, `AlreadyConnectedError`, `NoDataError` |
749
- | `@honeybbq/teamspeak/client` (voice only) | `ConnectionTimeoutError`, `HandshakeError` and its `PuzzleTooHardError`, `InvalidAddressError`, `InvalidIdentityError`, `FileTransferError`, `RemovedError` (kick, ban or server stop) |
750
- | `@honeybbq/teamspeak/stream` (streams only) | `StreamsUnsupportedError`, `StreamJoinDeniedError`, `StreamEndedError`, `StreamPayloadTooLargeError` |
751
- | `@honeybbq/teamspeak/query` (query only) | `QueryAuthError`, `GuestAccessError`, `SshHostKeyError`, `MissingDependencyError`, `WebQueryHttpError` |
752
-
753
- A call that is given a value it cannot use, such as an out-of-range id or an
754
- option that contradicts another, throws the language's own `TypeError` or
755
- `RangeError` before anything is sent. Those are not `TeamspeakError`s: they are
756
- a bug in the caller, not a condition to handle. `WebQueryHttpError` carries the
757
- HTTP `status`; a refusal the server itself reports is a `ServerError` over every
758
- transport.
759
-
760
- **Cancellation.** An aborted `signal` is the caller's own cancellation, not an
761
- SDK error. `connect()`, the typed commands, `exec`, file transfers and `events()`
762
- reject with the signal's `reason` itself, unwrapped: a `DOMException` named
763
- `AbortError` after a bare `abort()`, a `TimeoutError` after
764
- `AbortSignal.timeout()`, or whatever value was passed to `abort(reason)`. A
765
- signal that is already aborted rejects before anything is sent. Aborting a
766
- `connect()` abandons the attempt and leaves the client disconnected; a command
767
- that was given up on may still have taken effect on the server. The signal and
768
- the command's own `timeoutMs` are separate: when `timeoutMs` (ten seconds by
769
- default) passes first, the command rejects with a `CommandTimeoutError`.
770
-
771
- **No data.** `NoDataError` means the command succeeded but its answer lacked the
772
- data it exists to return, such as the id of a channel just created. Treat the
773
- command as having taken effect and do not retry it blindly, or it creates a
774
- second object or is refused as a duplicate. `createChannel` and the group
775
- commands look the new object up themselves and throw it only when nothing
776
- matches. A list that comes back empty is `[]`, not this error.
777
-
778
- ### Ids
779
-
780
- Every kind of id is a branded type: `ChannelID`, `ClientID`, `ClientDBID`,
781
- `ServerGroupID`, `ChannelGroupID`, `PermissionID`, `IconID`, `ServerID`,
782
- `BanID` and the rest, with `UID` and `ChannelUID` as branded strings. The
783
- brand costs nothing at runtime, and passing the wrong kind is a compile error.
784
- Make one with its constructor, which throws a `RangeError` for a value outside
785
- the id's wire range:
786
-
787
- ```typescript
788
- import { asChannelID, asClientID, moveClient } from "@honeybbq/teamspeak";
789
-
790
- const cid = asChannelID(5); // or asChannelID(5n)
791
- await moveClient(client, client.clientID, cid);
792
- // moveClient(client, cid, client.clientID) does not compile.
793
- const clid = asClientID(3);
794
- ```
795
-
796
- A `ClientID`, `PermissionID`, `IconID` and the file transfer ids are `number`s.
797
- The ones the protocol makes 64 bits wide are `bigint`s: `ChannelID`,
798
- `ClientDBID`, `ServerGroupID`, `ChannelGroupID`, `ServerID`, `BanID`,
799
- `MessageID` and `APIKeyID`. Their constructors take a `number` or a `bigint`.
800
-
801
- ### Options and absent values
802
-
803
- **Options.** Required arguments are positional. Everything optional is one
804
- options object, the last argument. A field that is not set is not sent:
805
- `undefined` leaves the setting alone and `""` sends it empty.
806
-
807
- ```typescript
808
- import { editChannel } from "@honeybbq/teamspeak";
809
-
810
- await editChannel(client, cid, {
811
- name: "Lounge",
812
- topic: "", // sent empty: clears the topic
813
- // password is not set: not sent, so the password stays as it is.
814
- });
815
- ```
816
-
817
- **Records.** In a record the server's answer fills in, an optional field is
818
- `undefined` when the server did not send it. That is not the same as `0` or
819
- `false`: `totalClients` is `undefined` in a `getChannelInfo` result, which does
820
- not carry it, and `0` in a `listChannels` result of an empty channel.
821
-
822
- ```typescript
823
- import { getChannelInfo } from "@honeybbq/teamspeak";
824
-
825
- const info = await getChannelInfo(client, cid);
826
- if (info.totalClients !== undefined) console.log(info.totalClients, "clients");
827
- ```
828
-
829
- **Durations and times** are numbers of milliseconds with an `Ms` suffix
830
- (`idleTimeMs`, `timeoutMs`) and `Date`. A `Date` field is `undefined` where the
831
- server sends 0 for "never". The Go SDK uses `time.Duration` and `time.Time`.
832
-
833
- ### Events
834
-
835
- Each server event has a typed name (`"textMessage"`, `"clientEnter"`, ...;
836
- `EventName` has a constant for each), and `"notification"` delivers every
837
- notification as the server sent it, including those no typed event models. The
838
- notification arrives after the typed event it produced. `on()` returns a
839
- function that removes the listener; `events(name, { signal })` is the same as an
840
- async iterator.
841
-
842
- ```typescript
843
- const off = client.on("textMessage", (message) => {
844
- console.log(message.invokerName, message.message);
845
- });
846
- off();
847
-
848
- client.on("notification", (n) => console.log(n.name, n.params));
849
- ```
850
-
851
- - **Order.** Listeners run one at a time, in the order the events arrived, so a
852
- listener may call the client and await an answer.
853
- - **State.** The client updates its state, such as `client.view`, before it
854
- queues the event that reports the change, so a listener sees state at least as
855
- new as its event.
856
- - **A throw** in a listener does not stop the others. The exception is rethrown
857
- from a microtask, as an uncaught exception, as an EventEmitter listener's
858
- would be. The Go SDK recovers a panicking handler and logs it instead.
859
- - **Middleware.** `eventMiddleware` sees each server event before the listeners
860
- and may pass it on, replace it or drop it; `connected`, `disconnected`,
861
- `kicked` and the view events bypass it. `commandMiddleware` wraps the typed commands. Both are
862
- described above.
863
-
864
- ### Stability
865
-
866
- From 1.0 the package follows semver, as described under
867
- [Compatibility](#compatibility): a breaking change to `@honeybbq/teamspeak`,
868
- `/client` and `/versions` waits for 2.0. The enumerations, `ErrorCode` and the
869
- event map may gain members in a minor release; give a `switch` over them a
870
- default case.
871
-
872
- An API that may still change in a minor release is marked `@experimental` in its TSDoc,
873
- which editors and the API reference show. Today that is the whole
874
- `@honeybbq/teamspeak/query` entry point, the server view (`client.view` and the
875
- view events), the `previous` field of the client-left and channel-deleted
876
- events, and `getServerVariables`.
877
-
878
- ### Server behaviour the SDK works around
879
-
880
- The servers answer some commands in ways that differ between TeamSpeak 3 and 6
881
- and between transports, and the SDK evens those out. The measured tables are in
882
- [voice-protocol-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/voice-protocol-responses.md)
883
- and [serverquery-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/serverquery-responses.md).
884
-
885
- ## ServerQuery
886
-
887
- The `@experimental` subpath `@honeybbq/teamspeak/query` administers a server
888
- the way the voice protocol cannot: the same typed commands the core exports,
889
- plus the ones only an administrator may run, over the three transports a
890
- ServerQuery speaks.
891
-
892
- | Transport | Where | Server | Login | Notifications |
893
- | --------- | --------------------------- | ---------------- | ----------------- | ------------- |
894
- | `tcp` | 10011 | TeamSpeak 3 only | `transport.login` | yes |
895
- | `ssh` | 10022 | both | the SSH login | yes |
896
- | `web` | 10080 (HTTP), 10443 (HTTPS) | both | the api key | **no** |
897
-
898
- TeamSpeak 3 (3.12 and later for WebQuery, 3.3 and later for SSH) serves SSH and
899
- WebQuery as well as raw TCP, but only the raw port is on until
900
- `query_protocols` lists the others; the official Docker image takes it as
901
- `TS3SERVER_QUERY_PROTOCOLS=raw,ssh,http`. TeamSpeak 6 has no raw port. Its
902
- WebQuery also answers a request with no api key as a guest; TeamSpeak 3 refuses
903
- one.
904
-
905
- WebQuery is reached over `http:` or `https:`, by the scheme of `url`. TeamSpeak 3
906
- serves HTTPS on 10443 once `query_protocols` lists `https` and
907
- `query_https_certificate_file` and `query_https_private_key_file` are set. The
908
- built-in `fetch` verifies the certificate against the trust store Node uses and
909
- never skips that. A certificate from a private CA, or a self-signed one, is
910
- trusted by starting Node with `NODE_EXTRA_CA_CERTS=ca.pem`, or by a `fetch`
911
- that trusts the CA:
912
-
913
- ```typescript
914
- import { readFileSync } from "node:fs";
915
- import { Agent, request } from "node:https";
916
-
917
- const agent = new Agent({ ca: readFileSync("ca.pem") });
918
- const trusting = ((url: string, init: RequestInit) =>
919
- new Promise<Response>((resolve, reject) => {
920
- const req = request(
921
- url,
922
- { method: init.method, headers: init.headers as never, agent },
923
- (res) => {
924
- const chunks: Buffer[] = [];
925
- res.on("data", (chunk: Buffer) => chunks.push(chunk));
926
- res.on("end", () =>
927
- resolve(new Response(Buffer.concat(chunks), { status: res.statusCode })),
928
- );
929
- },
930
- );
931
- req.on("error", reject);
932
- req.end(init.body as string);
933
- })) as typeof fetch;
934
-
935
- new QueryClient({
936
- transport: { kind: "web", url: "https://ts.example.com:10443", apiKey, fetch: trusting },
937
- });
938
- ```
939
-
940
- Only raw TCP and SSH carry notifications: the server pushes them down the same
941
- connection. WebQuery has no session to push to and refuses to register, so the
942
- client never tries, and a listener for a server event there throws rather than
943
- sitting silent. `connected` and `disconnected` work on every transport, since
944
- they come from the client itself.
945
-
946
- ```typescript
947
- import { QueryClient } from "@honeybbq/teamspeak/query";
948
- import { listChannels } from "@honeybbq/teamspeak";
949
-
950
- const client = new QueryClient({
951
- transport: {
952
- kind: "tcp", // 10011
953
- host: "ts.example.com",
954
- login: { name: "serveradmin", password: process.env.QUERY_PASSWORD! },
955
- },
956
- server: { id: 1 },
957
- nickname: "bot",
958
- events: { categories: ["textserver", "textprivate"] },
959
- reconnect: { backoff: { initialDelayMs: 1_000 } },
960
- });
961
-
962
- client.on("textMessage", (message) => console.log(message.invokerName, message.message));
963
-
964
- await client.connect();
965
-
966
- for (const channel of await listChannels(client)) console.log(channel.name);
967
-
968
- await client.disconnect();
969
- ```
970
-
971
- The three transports are figures of speech the client rebuilds on every
972
- connection, which is what makes reconnecting possible; `custom` opens one
973
- itself, for a proxy, a tunnel or a test. Each built-in transport also offers
974
- the seam into its own connection — `connect` for `tcp`, returning a `Duplex`,
975
- and `connection` for `ssh`, taking a client or a factory for one — so an
976
- application behind a TLS terminator or a SOCKS proxy, or one that already
977
- holds an SSH client, can supply it. `ssh2` is an optional peer dependency:
978
- without it the other two transports still work, and an `ssh` transport throws
979
- a `MissingDependencyError` naming the package.
980
-
981
- A query client delivers the events its categories carry: `serverEdited` comes
982
- under `server`; the channel events, `clientEnter`, `clientLeave` and
983
- `clientMoved` under `channel`; and `textMessage` under the three text
984
- categories. `QueryEventsOption` has the table. The voice client's events about
985
- clients' updates, group changes, subscriptions, plugin commands and typing are
986
- not sent to a ServerQuery connection by TeamSpeak 3 or by TeamSpeak 6 over SSH,
987
- so the query client does not offer them.
988
-
989
- `serverFamily` reports which generation the server is, read from its own
990
- `version` answer, and `transport` reads back the live connection for metrics.
991
-
992
- ### SSH host keys
72
+ ## Entry points
993
73
 
994
- The `ssh` transport checks the server's host key before it sends the login, and
995
- there is no default for how: the login is a credential, normally
996
- `serveradmin`'s, and a client that accepts any key hands it to whatever answers
997
- the port. Give exactly one of these, or the client refuses to be built, naming
998
- all three:
74
+ | Import | What it is | Docs |
75
+ | ------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
76
+ | `@honeybbq/teamspeak` | What every connection shares: the command language, ids, errors, typed commands, event types | [Design](https://honeybbq.github.io/teamspeak-js/documents/design.html) |
77
+ | `@honeybbq/teamspeak/client` | The voice client, identities, file transfers, diagnostics and the retry policy | [Guide](https://honeybbq.github.io/teamspeak-js/documents/guide.html) |
78
+ | `@honeybbq/teamspeak/stream` | TeamSpeak 6 screen-share signalling; no WebRTC code | [Streams](https://honeybbq.github.io/teamspeak-js/documents/streams.html) |
79
+ | `@honeybbq/teamspeak/query` | The ServerQuery client over raw TCP, SSH or WebQuery; `@experimental` | [ServerQuery](https://honeybbq.github.io/teamspeak-js/documents/serverquery.html) |
80
+ | `@honeybbq/teamspeak/versions` | Client versions a server accepts, for the `version` option | [Configuration](https://honeybbq.github.io/teamspeak-js/documents/configuration.html) |
999
81
 
1000
- ```typescript
1001
- const client = new QueryClient({
1002
- transport: {
1003
- kind: "ssh",
1004
- host: "ts.example.com",
1005
- username: "serveradmin",
1006
- password: process.env.QUERY_PASSWORD!,
1007
- knownHosts: "/etc/ssh/ssh_known_hosts",
1008
- },
1009
- });
1010
- ```
82
+ ## Documentation
1011
83
 
1012
- - `knownHosts`: the path of an OpenSSH `known_hosts` file, read each time a
1013
- connection is attempted and never found on its own: `~/.ssh/known_hosts` is
1014
- used only if you name it. The check is `StrictHostKeyChecking=yes`: a host the
1015
- file does not mention is refused, so is one recorded with another key, and so
1016
- is a key a `@revoked` line names. Plain, `[host]:port` and hashed
1017
- (`ssh-keygen -H`) entries are read. The query port is not 22, so record it as
1018
- `ssh-keyscan -p 10022 ts.example.com`, which writes `[ts.example.com]:10022`;
1019
- an entry for port 22 does not vouch for 10022.
1020
- - `hostKey`: your own check, `(key) => void`, that throws to refuse. `key` has
1021
- the host, port, `keyType`, the key's bytes and its `fingerprint`, the
1022
- `SHA256:…` that `ssh-keygen -lf` prints. It has to be synchronous and return
1023
- nothing: a returned value, a `false` included, or a promise is refused rather
1024
- than guessed at.
1025
- - `insecureIgnoreHostKey: true`: accept any key. It gives up what the host key
1026
- is for, and is for a test or a throwaway server.
84
+ The full API reference is at [honeybbq.github.io/teamspeak-js](https://honeybbq.github.io/teamspeak-js/). The pages below are the long-form guides.
1027
85
 
1028
- A key that is refused fails the connection with an `SshHostKeyError`, whose
1029
- `cause` says why, and nothing was sent: the password has not left the process.
1030
- Reconnection does not retry it. A connection you supply yourself through
1031
- `connection` is checked by whoever made it, so these options are refused there.
1032
-
1033
- **`@experimental`**: this entry point is new, and its API may change in a
1034
- minor release — the options, the transport union and the event map are all
1035
- still settling. Pin a version and read the changelog if you depend on it.
86
+ - [Guide](https://honeybbq.github.io/teamspeak-js/documents/guide.html): connecting, events, commands, file transfers and identities.
87
+ - [Configuration](https://honeybbq.github.io/teamspeak-js/documents/configuration.html): options, client version, tracing, diagnostics, reconnection, custom transports and server capabilities.
88
+ - [Streams](https://honeybbq.github.io/teamspeak-js/documents/streams.html): TeamSpeak 6 screen-share signalling, and interop with the TeamSpeak 6 client.
89
+ - [ServerQuery](https://honeybbq.github.io/teamspeak-js/documents/serverquery.html): the query client and its SSH host-key policy.
90
+ - [Design](https://honeybbq.github.io/teamspeak-js/documents/design.html): errors, ids, absent values, events, stability, and the server behaviour the SDK works around.
91
+ - [Compatibility](https://honeybbq.github.io/teamspeak-js/documents/compatibility.html): supported environments and the differences from the Go SDK.
92
+ - [Architecture](https://honeybbq.github.io/teamspeak-js/documents/architecture.html): source layout and entry points.
93
+ - [Migrating to 1.0](https://honeybbq.github.io/teamspeak-js/documents/migrating-to-1.0.html), [voice protocol responses](https://honeybbq.github.io/teamspeak-js/documents/voice-protocol-responses.html) and [ServerQuery responses](https://honeybbq.github.io/teamspeak-js/documents/serverquery-responses.html): reference material.
1036
94
 
1037
95
  ## Compatibility
1038
96
 
1039
- The package follows semver from 1.0. A few sets are open by nature and may
1040
- gain members in a minor release: the events (`EventMap`, `ServerEventName`),
1041
- `ClientStatus`, `ServerFamily`, `UIDAlgorithm`, `ErrorCode`, `Reason` and the
1042
- enumeration tables. Write a `switch` over any of them with a default case. A
1043
- field the server sends as a number that one of those tables names — a codec, a
1044
- text message's target mode, a group type — is a plain `number`, since a server
1045
- may send a value newer than the table.
1046
-
1047
- `DatagramSocket` describes the part of a socket the client uses; should a
1048
- later release use more, the new members will be optional.
1049
-
1050
- ### Supported environments
1051
-
1052
- | Environment | Supported |
1053
- | ----------- | ------------------------------------------------------------------------------ |
1054
- | Node.js | The active and maintenance LTS releases, from 22.12; CI tests 22.12, 24 and 26 |
1055
- | TypeScript | 5.2 and newer, for the published types |
1056
- | Modules | ESM only |
1057
-
1058
- When a Node.js release reaches end of life, a minor release may raise
1059
- `engines.node` past it; that is not a breaking change. The published types use
1060
- `Disposable`, so the `lib` option must include `esnext.disposable` (or `es2026`
1061
- or `esnext`), and the subpath exports need `moduleResolution` set to `node16`,
1062
- `nodenext` or `bundler`. CI type-checks a consumer of the packed package with
1063
- TypeScript 5.2. Raising that minimum follows TypeScript's own support window,
1064
- happens in a minor release, and comes with a changelog note.
1065
-
1066
- ### Differences from the Go SDK
1067
-
1068
- The [Go SDK](https://github.com/honeybbq/teamspeak-go) makes the same calls
1069
- with the same semantics, apart from what the languages make differ:
1070
-
1071
- - Backoff fields: JavaScript takes `undefined` as "the default" and `0` as 0.
1072
- Go cannot tell an unset field from 0, so there a zero field takes the default
1073
- and a negative jitter disables it.
1074
- - A listener that throws: here the exception is rethrown from a microtask, an
1075
- uncaught exception as an EventEmitter listener's would be, and the other
1076
- listeners still get the event. Go recovers a panicking handler, logs it at
1077
- Error, and keeps delivering.
1078
- - Durations are milliseconds with an `Ms` suffix here, `time.Duration` in Go;
1079
- `listClients`' `uid` flag (on by default) is Go's `OmitUID`.
97
+ | Target | Supported |
98
+ | ----------- | --------------------------------------------------------------------- |
99
+ | TeamSpeak 3 | Voice protocol and ServerQuery |
100
+ | TeamSpeak 6 | Voice protocol, ServerQuery and screen-share signalling |
101
+ | Node.js | 22.12 and newer; CI tests 22.12, 24 and 26. ESM only; TypeScript 5.2+ |
1080
102
 
1081
- ## Entry points
1082
-
1083
- | Import from | What it is |
1084
- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
1085
- | `@honeybbq/teamspeak` | What every TeamSpeak connection shares: the command language, ids, errors, the typed commands, the event types and the status and reconnection types |
1086
- | `@honeybbq/teamspeak/client` | The voice client, identities, file transfers, diagnostics and its retry policy |
1087
- | `@honeybbq/teamspeak/versions` | Client versions a server accepts, for the client's `version` option; data only, one export each |
1088
- | `@honeybbq/teamspeak/stream` | TeamSpeak 6 streams, the screen share: finding, publishing and watching them; signalling only, no WebRTC code |
1089
- | `@honeybbq/teamspeak/query` | The ServerQuery client, running the same typed commands over raw TCP, SSH or WebQuery; `@experimental` |
1090
-
1091
- Each entry exports only its own symbols. The client entry does not re-export
1092
- the core, so a program imports the `Client` from `@honeybbq/teamspeak/client`
1093
- and the typed commands, ids and errors from `@honeybbq/teamspeak`.
1094
-
1095
- The protocol internals (crypto, handshake, transport) are not exported. They
1096
- change with the protocol, and exporting them would have made every change a
1097
- breaking one.
1098
-
1099
- `@honeybbq/teamspeak-client` and `@honeybbq/teamspeak-core` are deprecated and
1100
- get no further releases. Coming from 0.x? The
1101
- [migration guide](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/migrating-to-1.0.md) lists every change.
1102
-
1103
- ## Architecture
1104
-
1105
- ```
1106
- src/
1107
- ├── core/ # Shared (@honeybbq/teamspeak): command language, ids, errors, typed commands
1108
- ├── versions/ # Client versions (@honeybbq/teamspeak/versions)
1109
- ├── client/ # The voice client (@honeybbq/teamspeak/client)
1110
- │ ├── client.ts # Client lifecycle, connection management
1111
- │ ├── commands.ts # Command sending and response tracking
1112
- │ ├── api/voice.ts # Commands only the voice protocol has
1113
- │ ├── capabilities.ts # Server version and uid hash detection
1114
- │ ├── reconnect.ts # Backoff policy and permanent-failure detection
1115
- │ ├── events.ts # Event handler registration and middleware
1116
- │ ├── notifications.ts # Server notification parsing and dispatch
1117
- │ ├── handshake.ts # Protocol handshake orchestration
1118
- │ ├── transfer.ts # File transfer data connections
1119
- │ ├── throttle.ts # Token-bucket rate limiter
1120
- │ ├── crypto/ # ECDH, EAX encryption, identity management
1121
- │ ├── handshake/ # Crypto handshake and license verification
1122
- │ ├── transport/ # UDP packet framing, ACK, compression
1123
- │ └── discovery/ # SRV / TSDNS / direct address resolution
1124
- ├── stream/ # TeamSpeak 6 streams (@honeybbq/teamspeak/stream)
1125
- └── query/ # The ServerQuery client (@honeybbq/teamspeak/query)
1126
- examples/
1127
- └── connect.ts # Minimal connection example
1128
- ```
103
+ Details in [the compatibility page](https://honeybbq.github.io/teamspeak-js/documents/compatibility.html).
1129
104
 
1130
105
  ## Related
1131
106
 
@@ -1135,14 +110,6 @@ examples/
1135
110
 
1136
111
  Protocol knowledge was primarily informed by the [TSLib](https://github.com/Splamy/TS3AudioBot) implementation in [TS3AudioBot](https://github.com/Splamy/TS3AudioBot) by Splamy. Huge thanks to the TS3AudioBot project and its contributors.
1137
112
 
1138
- ## Disclaimer
1139
-
1140
- TeamSpeak is a registered trademark of [TeamSpeak Systems, Inc.](https://teamspeak.com/). This project is not affiliated with, endorsed by, or associated with TeamSpeak Systems, Inc. in any way.
1141
-
1142
- This library is an **independent implementation** built from public documentation, observation of network traffic, and independent research. It contains no TeamSpeak proprietary code, headers, libraries, or binaries.
1143
-
1144
- The project implements the client side only and does not implement a TeamSpeak server.
1145
-
1146
113
  ## Contributing
1147
114
 
1148
115
  See [CONTRIBUTING.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/.github/CONTRIBUTING.md) for setup, checks and the integration suite.
@@ -1150,3 +117,11 @@ See [CONTRIBUTING.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/.github
1150
117
  ## License
1151
118
 
1152
119
  [MIT](https://github.com/HoneyBBQ/teamspeak-js/blob/main/LICENSE)
120
+
121
+ ---
122
+
123
+ <sub>TeamSpeak is a registered trademark of [TeamSpeak Systems, Inc.](https://teamspeak.com/). This project is not affiliated with, endorsed by, or associated with TeamSpeak Systems, Inc. in any way.</sub>
124
+
125
+ <sub>This library is an **independent implementation** built from public documentation, observation of network traffic, and independent research. It contains no TeamSpeak proprietary code, headers, libraries, or binaries.</sub>
126
+
127
+ <sub>The project implements the client side only and does not implement a TeamSpeak server.</sub>