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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +322 -60
  2. package/dist/abort.d.ts +5 -0
  3. package/dist/abort.d.ts.map +1 -0
  4. package/dist/address.d.ts +8 -0
  5. package/dist/address.d.ts.map +1 -1
  6. package/dist/api/voice.d.ts +24 -0
  7. package/dist/api/voice.d.ts.map +1 -0
  8. package/dist/capabilities.d.ts +62 -0
  9. package/dist/capabilities.d.ts.map +1 -0
  10. package/dist/client.d.ts +172 -39
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/commands.d.ts +45 -27
  13. package/dist/commands.d.ts.map +1 -1
  14. package/dist/crypto/crypt.d.ts +19 -4
  15. package/dist/crypto/crypt.d.ts.map +1 -1
  16. package/dist/crypto/eax.d.ts.map +1 -1
  17. package/dist/crypto/identity.d.ts +87 -8
  18. package/dist/crypto/identity.d.ts.map +1 -1
  19. package/dist/crypto/index.d.ts +2 -1
  20. package/dist/crypto/index.d.ts.map +1 -1
  21. package/dist/diagnostics.d.ts +45 -0
  22. package/dist/diagnostics.d.ts.map +1 -0
  23. package/dist/discovery/resolver.d.ts +21 -3
  24. package/dist/discovery/resolver.d.ts.map +1 -1
  25. package/dist/errors.d.ts +70 -16
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/events.d.ts +4 -5
  28. package/dist/events.d.ts.map +1 -1
  29. package/dist/handshake/crypt-handshake.d.ts +1 -0
  30. package/dist/handshake/crypt-handshake.d.ts.map +1 -1
  31. package/dist/handshake/crypt-init2.d.ts.map +1 -1
  32. package/dist/handshake/license.d.ts +7 -0
  33. package/dist/handshake/license.d.ts.map +1 -1
  34. package/dist/handshake.d.ts +14 -5
  35. package/dist/handshake.d.ts.map +1 -1
  36. package/dist/helpers.d.ts +0 -8
  37. package/dist/helpers.d.ts.map +1 -1
  38. package/dist/index.d.ts +20 -6
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3967 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/notifications.d.ts +7 -15
  43. package/dist/notifications.d.ts.map +1 -1
  44. package/dist/reconnect.d.ts +69 -0
  45. package/dist/reconnect.d.ts.map +1 -0
  46. package/dist/throttle.d.ts +9 -0
  47. package/dist/throttle.d.ts.map +1 -1
  48. package/dist/transfer.d.ts +27 -25
  49. package/dist/transfer.d.ts.map +1 -1
  50. package/dist/transport/generation-window.d.ts.map +1 -1
  51. package/dist/transport/handler.d.ts +37 -5
  52. package/dist/transport/handler.d.ts.map +1 -1
  53. package/dist/transport/quicklz.d.ts +19 -0
  54. package/dist/transport/quicklz.d.ts.map +1 -1
  55. package/dist/types.d.ts +277 -97
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +21 -57
  58. package/src/abort.ts +24 -0
  59. package/src/address.ts +74 -0
  60. package/src/api/voice.ts +61 -0
  61. package/src/capabilities.ts +92 -0
  62. package/src/client.ts +1392 -0
  63. package/src/commands.ts +193 -0
  64. package/src/crypto/crypt-ops.ts +6 -0
  65. package/src/crypto/crypt.ts +274 -0
  66. package/src/crypto/eax.ts +152 -0
  67. package/src/crypto/identity.ts +484 -0
  68. package/src/crypto/index.ts +21 -0
  69. package/src/crypto/primitives.ts +141 -0
  70. package/src/diagnostics.ts +58 -0
  71. package/src/discovery/index.ts +1 -0
  72. package/src/discovery/resolver.ts +272 -0
  73. package/src/errors.ts +156 -0
  74. package/src/events.ts +28 -0
  75. package/src/handshake/crypt-handshake.ts +135 -0
  76. package/src/handshake/crypt-init2.ts +48 -0
  77. package/src/handshake/index.ts +3 -0
  78. package/src/handshake/license.test.ts +49 -0
  79. package/src/handshake/license.ts +208 -0
  80. package/src/handshake.ts +136 -0
  81. package/src/helpers.ts +10 -0
  82. package/src/index.ts +74 -0
  83. package/src/notifications.ts +136 -0
  84. package/src/reconnect.ts +141 -0
  85. package/src/throttle.ts +92 -0
  86. package/src/transfer.ts +127 -0
  87. package/src/transport/generation-window.ts +77 -0
  88. package/src/transport/handler.ts +914 -0
  89. package/src/transport/index.ts +14 -0
  90. package/src/transport/packet.ts +69 -0
  91. package/src/transport/quicklz.ts +209 -0
  92. package/src/types.ts +349 -0
  93. package/dist/address-BR-5unVo.cjs +0 -2
  94. package/dist/address-BR-5unVo.cjs.map +0 -1
  95. package/dist/address-BfmovVpp.js +0 -65
  96. package/dist/address-BfmovVpp.js.map +0 -1
  97. package/dist/address.test.d.ts +0 -2
  98. package/dist/address.test.d.ts.map +0 -1
  99. package/dist/api.d.ts +0 -17
  100. package/dist/api.d.ts.map +0 -1
  101. package/dist/api.test.d.ts +0 -2
  102. package/dist/api.test.d.ts.map +0 -1
  103. package/dist/command/command.d.ts +0 -12
  104. package/dist/command/command.d.ts.map +0 -1
  105. package/dist/command/command.test.d.ts +0 -2
  106. package/dist/command/command.test.d.ts.map +0 -1
  107. package/dist/command/index.cjs +0 -1
  108. package/dist/command/index.d.ts +0 -4
  109. package/dist/command/index.d.ts.map +0 -1
  110. package/dist/command/index.mjs +0 -3
  111. package/dist/command/parser.d.ts +0 -3
  112. package/dist/command/parser.d.ts.map +0 -1
  113. package/dist/command-BLe-C2L2.js +0 -74
  114. package/dist/command-BLe-C2L2.js.map +0 -1
  115. package/dist/command-DAK4pUBP.cjs +0 -4
  116. package/dist/command-DAK4pUBP.cjs.map +0 -1
  117. package/dist/crypt-handshake-CQkQreLx.cjs +0 -2
  118. package/dist/crypt-handshake-CQkQreLx.cjs.map +0 -1
  119. package/dist/crypt-handshake-DziOY4Lq.js +0 -55
  120. package/dist/crypt-handshake-DziOY4Lq.js.map +0 -1
  121. package/dist/crypt-init2-D70ha4LB.js +0 -158
  122. package/dist/crypt-init2-D70ha4LB.js.map +0 -1
  123. package/dist/crypt-init2-DparLekm.cjs +0 -2
  124. package/dist/crypt-init2-DparLekm.cjs.map +0 -1
  125. package/dist/crypto/eax.test.d.ts +0 -2
  126. package/dist/crypto/eax.test.d.ts.map +0 -1
  127. package/dist/crypto/identity.test.d.ts +0 -2
  128. package/dist/crypto/identity.test.d.ts.map +0 -1
  129. package/dist/crypto/index.cjs +0 -1
  130. package/dist/crypto/index.mjs +0 -3
  131. package/dist/crypto-Bg02m4ZE.js +0 -165
  132. package/dist/crypto-Bg02m4ZE.js.map +0 -1
  133. package/dist/crypto-CDlweUL3.cjs +0 -2
  134. package/dist/crypto-CDlweUL3.cjs.map +0 -1
  135. package/dist/discovery/index.cjs +0 -1
  136. package/dist/discovery/index.mjs +0 -2
  137. package/dist/handler-DI_abe9c.js +0 -483
  138. package/dist/handler-DI_abe9c.js.map +0 -1
  139. package/dist/handler-DuwoO5RC.cjs +0 -2
  140. package/dist/handler-DuwoO5RC.cjs.map +0 -1
  141. package/dist/handshake/index.cjs +0 -1
  142. package/dist/handshake/index.mjs +0 -3
  143. package/dist/handshake.test.d.ts +0 -2
  144. package/dist/handshake.test.d.ts.map +0 -1
  145. package/dist/helpers.test.d.ts +0 -2
  146. package/dist/helpers.test.d.ts.map +0 -1
  147. package/dist/index.cjs +0 -4
  148. package/dist/index.cjs.map +0 -1
  149. package/dist/index.mjs +0 -796
  150. package/dist/index.mjs.map +0 -1
  151. package/dist/integration.test.d.ts +0 -12
  152. package/dist/integration.test.d.ts.map +0 -1
  153. package/dist/notifications.test.d.ts +0 -2
  154. package/dist/notifications.test.d.ts.map +0 -1
  155. package/dist/parser-CL33R9tr.cjs +0 -2
  156. package/dist/parser-CL33R9tr.cjs.map +0 -1
  157. package/dist/parser-DZMv3Jm-.js +0 -33
  158. package/dist/parser-DZMv3Jm-.js.map +0 -1
  159. package/dist/primitives-cNNAeFCo.cjs +0 -2
  160. package/dist/primitives-cNNAeFCo.cjs.map +0 -1
  161. package/dist/primitives-xIr6Tjk4.js +0 -1457
  162. package/dist/primitives-xIr6Tjk4.js.map +0 -1
  163. package/dist/resolver-6B8dI9gr.js +0 -141
  164. package/dist/resolver-6B8dI9gr.js.map +0 -1
  165. package/dist/resolver-DCcmyWSK.cjs +0 -4
  166. package/dist/resolver-DCcmyWSK.cjs.map +0 -1
  167. package/dist/throttle.test.d.ts +0 -2
  168. package/dist/throttle.test.d.ts.map +0 -1
  169. package/dist/transport/generation-window.test.d.ts +0 -2
  170. package/dist/transport/generation-window.test.d.ts.map +0 -1
  171. package/dist/transport/index.cjs +0 -1
  172. package/dist/transport/index.mjs +0 -2
  173. package/dist/transport/packet.test.d.ts +0 -2
  174. package/dist/transport/packet.test.d.ts.map +0 -1
  175. package/dist/transport/quicklz.test.d.ts +0 -2
  176. package/dist/transport/quicklz.test.d.ts.map +0 -1
  177. package/dist/transport/whisper.test.d.ts +0 -2
  178. package/dist/transport/whisper.test.d.ts.map +0 -1
package/README.md CHANGED
@@ -13,6 +13,8 @@ Compatible with TeamSpeak 3, 5 & 6. No proprietary SDK. No copy-pasted code.
13
13
 
14
14
  </div>
15
15
 
16
+ Upgrading from 0.x? See the [migration guide](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/migrating-to-1.0.md).
17
+
16
18
  ## Features
17
19
 
18
20
  - **Full protocol handshake** — ECDH key exchange, RSA puzzle, EAX-encrypted transport
@@ -20,11 +22,15 @@ Compatible with TeamSpeak 3, 5 & 6. No proprietary SDK. No copy-pasted code.
20
22
  - **Event-driven API** — Register handlers for text messages, client enter/leave, channel moves, kicks, etc.
21
23
  - **Voice data** — Send Opus voice packets (codec 4 & 5)
22
24
  - **File transfers** — Upload, download, and delete files on the server
23
- - **Address resolution** — SRV records, TSDNS, and direct address support
25
+ - **Address resolution** — SRV records, TSDNS, and direct addresses; server nicknames on request (`resolveNicknames: true`), since that lookup goes to named.myteamspeak.com
24
26
  - **Middleware** — Pluggable command and event middleware chains
25
27
  - **Built-in rate limiter** — Token-bucket throttling to prevent server-side flood kicks
26
28
  - **Identity management** — Generate, import/export, and upgrade security level of identities
27
- - **Dual format** — Ships ESM and CJS with full TypeScript declarations
29
+ - **Full command set** — 111 typed functions covering every command the voice protocol answers
30
+ - **Reconnection** — Opt-in retry with backoff, tuned for TeamSpeak's antiflood ban
31
+ - **Server capabilities** — Reports the server version and the uid hash it uses (SHA-1 on TS3, SHA-256 on TS6)
32
+ - **Custom transports** — Supply your own socket, for proxies, tunnels or tests
33
+ - **ESM with types** — Ships ES modules with full TypeScript declarations
28
34
  - **Zero native deps** — Pure TypeScript, no native addons required
29
35
 
30
36
  ## Installation
@@ -43,7 +49,7 @@ Requires **Node.js 20.19** or later.
43
49
  import { Client, generateIdentity } from "@honeybbq/teamspeak-client";
44
50
 
45
51
  // Generate a new identity (or load an existing one)
46
- const identity = generateIdentity(8);
52
+ const identity = await generateIdentity(8);
47
53
 
48
54
  // Create the client
49
55
  const client = new Client(identity, "localhost", "TSBot", {
@@ -65,11 +71,8 @@ client.on("disconnected", (err) => {
65
71
  console.log("Disconnected:", err?.message ?? "clean");
66
72
  });
67
73
 
68
- // Connect
69
- await client.connect();
70
-
71
- // Wait until connected (with 15s timeout)
72
- await client.waitConnected(AbortSignal.timeout(15_000));
74
+ // Connect; resolves once the handshake completes.
75
+ await client.connect({ signal: AbortSignal.timeout(15_000) });
73
76
 
74
77
  // Stay connected until interrupted
75
78
  process.on("SIGINT", async () => {
@@ -81,12 +84,26 @@ process.on("SIGINT", async () => {
81
84
 
82
85
  ### Client Lifecycle
83
86
 
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 |
87
+ | Method | Description |
88
+ | --------------------------------------------- | ------------------------------------------------- |
89
+ | `new Client(identity, addr, nickname, opts?)` | Create a new client |
90
+ | `connect({ signal? })` | Connect; resolves once the handshake completes |
91
+ | `disconnect({ signal? })` | Leave the server and close the connection |
92
+ | `status` | `"disconnected"`, `"connecting"` or `"connected"` |
93
+ | `clientID` | Our clid, assigned by the server |
94
+ | `channelID` | The channel we are in |
95
+ | `serverCapabilities` | Server version, family and uid hash |
96
+ | `uid` | Our own uid, hashed as the server does |
97
+
98
+ `connect()` rejects with an `InvalidAddressError` for an address that cannot be
99
+ one — no host, or a port outside 1–65535 — before anything starts; a host name
100
+ that does not resolve fails with a `TeamspeakError` whose `cause` is the lookup's
101
+ error. A `connect()` called while `disconnect()` is still saying goodbye waits
102
+ for it to finish, then connects.
103
+
104
+ Commands, raw `exec`/`execNoWait`, voice and whispers need a connected client:
105
+ until `connect()` resolves, and from the moment `disconnect()` begins, they fail
106
+ with a `NotConnectedError` and send nothing.
90
107
 
91
108
  ### Events
92
109
 
@@ -99,59 +116,194 @@ process.on("SIGINT", async () => {
99
116
  | `on("clientLeave", handler)` | Fires when a client leaves |
100
117
  | `on("clientMoved", handler)` | Fires when a client moves channels |
101
118
  | `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 |
119
+ | `on("poked", handler)` | Fires when poked by a client |
120
+ | `on("voiceData", handler)` | Fires on incoming voice data |
121
+
122
+ `on()` and `once()` return a function that removes the listener, which is also
123
+ a `Disposable` (`using listener = client.on(...)`). Listeners run one at a
124
+ time, in the order their events arrived, so a listener may call the client and
125
+ await an answer. `disconnected` delivers `undefined` after `disconnect()`, and
126
+ the cause otherwise — a `ConnectionTimeoutError`, or a `RemovedError` for a kick, a ban
127
+ or a server stopping. `await using client = new Client(...)` disconnects when
128
+ the scope ends.
129
+
130
+ `connected` is delivered after `connect()` resolves, so a listener added right
131
+ after `await client.connect()` still hears it. `disconnect()` waits a second at
132
+ most for the server to acknowledge its goodbye, less if its `signal` aborts
133
+ sooner, and resolves once `disconnected` has been delivered. A `connect()` waits
134
+ for an earlier session's `disconnected` to be delivered before it starts, so no
135
+ listener or `events()` iterator of the new session hears the old one end.
136
+
137
+ A listener that throws does not stop the others: the event still reaches the
138
+ listeners after it, and the exception surfaces as an `uncaughtException`, as an
139
+ EventEmitter listener's would.
140
+
141
+ Voice is the exception to "every event in order": while more than 256
142
+ deliveries are waiting, voice frames are dropped (and counted in a warning at
143
+ most every ten seconds) rather than queued without bound.
144
+
145
+ `events(name, { signal })` is the same as an async iterator, buffering events
146
+ that arrive while the loop body runs:
147
+
148
+ ```typescript
149
+ for await (const msg of client.events("textMessage", { signal })) {
150
+ await reply(msg);
151
+ }
152
+ ```
153
+
154
+ `notification` delivers every notification as it arrived, for the ones the SDK
155
+ does not model.
104
156
 
105
157
  ### Commands
106
158
 
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 |
159
+ Every command the voice protocol answers is a typed function, grouped by area.
160
+ Each is addressed by branded ids, so a `ChannelID` cannot be passed where a
161
+ `ClientID` is meant. The constructors — `asChannelID(5)`, `asClientID(3)` and
162
+ the rest — throw a `RangeError` for anything but an integer in the id's wire
163
+ range: 0–65535 for a `ClientID` and the file transfer ids, 0–2^32−1 for a
164
+ `PermissionID` (an `IconID` also takes the signed form), 0–2^64−1 for the
165
+ `bigint` ids. Required arguments are positional; everything optional
166
+ goes in one options object, always the last argument, which also carries
167
+ `signal` and `timeout` to abandon the command or bound how long it waits.
168
+
169
+ ```typescript
170
+ import {
171
+ listChannels,
172
+ createChannel,
173
+ moveClient,
174
+ kickClient,
175
+ sendChannelMessage,
176
+ addServerGroupPermission,
177
+ iterateClientDatabase,
178
+ permissionByName,
179
+ asClientID,
180
+ Codec,
181
+ Reason,
182
+ } from "@honeybbq/teamspeak-client";
183
+
184
+ const channels = await listChannels(client, { signal: AbortSignal.timeout(5_000) });
185
+
186
+ const cid = await createChannel(client, {
187
+ name: "Lounge",
188
+ topic: "the quiet one",
189
+ codec: Codec.OpusVoice,
190
+ isPermanent: true,
191
+ });
192
+
193
+ await moveClient(client, client.clientID, cid, { password: "channel-password" });
194
+ await sendChannelMessage(client, "hello");
195
+ await kickClient(client, someone, Reason.KickChannel, { reasonMessage: "take a break" });
196
+
197
+ await addServerGroupPermission(client, sgid, {
198
+ permission: permissionByName("i_client_talk_power"),
199
+ value: 50,
200
+ });
201
+
202
+ for await (const entry of iterateClientDatabase(client)) {
203
+ console.log(entry.nickname, entry.lastConnectedAt);
204
+ }
205
+ ```
206
+
207
+ A command that sends content — `createChannel`'s channel, `editChannel`'s
208
+ settings, `updateClient`, `editClient`, `editClientDatabase`,
209
+ `addServerGroupPermission`'s grant, `addBan`'s `BanFields` — takes it as its own
210
+ argument, and `{ signal, timeout }` as a separate one after it: set among the
211
+ content, they are not read.
212
+
213
+ Options objects use camelCase names (`maxClients` for `channel_maxclients`),
214
+ and only the fields you set are sent: an unset field leaves the setting alone,
215
+ while `""` sends it empty — `editChannel(client, cid, { password: "" })` clears
216
+ a password. A key an options type does not name is never sent. The exceptions
217
+ are `editServer` and `editInstance`, which take the raw `virtualserver_*` and
218
+ `serverinstance_*` properties as `getServerInfo` reports them.
219
+
220
+ Results are camelCase too. Timestamps are `Date`s (`undefined` where the server
221
+ sends 0 for "never"), durations are milliseconds with an `Ms` suffix
222
+ (`idleTimeMs`, `durationMs`), and a permission is a `Permission`, built with
223
+ `permissionByID` or `permissionByName`. Small enumerations — `GroupType`,
224
+ `Codec`, `PrivilegeKeyType`, `LogLevel` and the rest — are objects of named values with
225
+ a type of the same name.
226
+
227
+ | Module | Covers |
228
+ | ------------ | -------------------------------------------------------------- |
229
+ | `channels` | Create, edit, move, delete, subscribe, channel permissions |
230
+ | `clients` | List, move, kick, poke, edit, the client database, permissions |
231
+ | `server` | Server and instance settings, groups, temp passwords, bindings |
232
+ | `groups` | Channel groups, the permission catalogue, privilege keys |
233
+ | `moderation` | Bans, complaints, offline messages, text messages, the log |
234
+ | `files` | List, delete, rename, create directories, transfer negotiation |
235
+
236
+ Many of these commands are answered over the voice protocol with a
237
+ notification rather than data rows — `servergrouplist` with
238
+ `notifyservergrouplist`, `ftinitupload` with `notifystartupload` after the
239
+ command has already succeeded. The client pairs each answer with its command,
240
+ so this is invisible to callers; [docs/voice-protocol-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/voice-protocol-responses.md)
241
+ has the measured table. A refusal is always thrown, never returned as an empty
242
+ result. A list or find with nothing to report answers `[]`. A command that
243
+ exists to return something — `createChannel`'s id, `initFileUpload`'s key,
244
+ `getClientInfo`'s fields — throws a `NoDataError` when the server answers
245
+ without it, rather than returning an id of 0 or an empty object.
246
+
247
+ For anything outside the typed set, `client.exec(line, { signal?, timeout? })`
248
+ sends a raw command line and resolves to the rows that answer it.
119
249
 
120
250
  ### File Transfers
121
251
 
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 |
252
+ Negotiate with the `files` module, then move the bytes over TCP:
253
+
254
+ ```typescript
255
+ import { Readable } from "node:stream";
256
+ import { initFileUpload } from "@honeybbq/teamspeak-client";
257
+
258
+ const info = await initFileUpload(client, cid, "/notes.txt", BigInt(data.length));
259
+ await client.uploadFileData(host, info, Readable.from([data]));
260
+ ```
261
+
262
+ An upload does not replace a file already at the path unless passed
263
+ `{ overwrite: true }`.
264
+
265
+ | Function | Description |
266
+ | ------------------------------------------------------- | --------------------------------- |
267
+ | `initFileUpload(client, ...)` | Negotiate an upload |
268
+ | `initFileDownload(client, ...)` | Negotiate a download |
269
+ | `client.uploadFileData(host, info, data, { signal? })` | Transfer file data to the server |
270
+ | `client.downloadFileData(host, info, out, { signal? })` | Receive file data from the server |
129
271
 
130
272
  ### Identity
131
273
 
132
274
  ```typescript
133
- import { generateIdentity, identityFromString } from "@honeybbq/teamspeak-client";
275
+ import { generateIdentity, parseIdentity } from "@honeybbq/teamspeak-client";
134
276
 
135
277
  // Generate a new identity with security level 8
136
- const identity = generateIdentity(8);
278
+ const identity = await generateIdentity(8);
137
279
 
138
280
  // Export to string for persistent storage
139
- const exported = identity.exportString();
281
+ const exported = identity.export();
140
282
 
141
283
  // Import from a previously exported string
142
- const restored = identityFromString(exported);
284
+ const restored = parseIdentity(exported);
143
285
 
144
- // Upgrade security level (CPU-intensive)
145
- identity.upgradeToLevel(10);
286
+ // Raise the security level (CPU-intensive). Identities are immutable: this
287
+ // returns a new one with the same key.
288
+ const stronger = await identity.upgradeToLevel(10, { signal });
289
+
290
+ // The unique identifier, hashed as a TS3 (sha1) or TS6 (sha256) server does
291
+ const uid = identity.uid("sha1");
146
292
  ```
147
293
 
294
+ The private key never leaves the identity except through `export()`:
295
+ `toString()`, `toJSON()` and `util.inspect()` describe it without the key.
296
+ `parseIdentity` takes exactly what `export()` writes — the key as canonical
297
+ base64 of at most 32 bytes, a colon, and a decimal offset that fits in 64 bits
298
+ — and throws an `InvalidIdentityError` for anything else.
299
+
148
300
  ### Options
149
301
 
150
302
  ```typescript
151
303
  const client = new Client(identity, "ts.example.com", "Bot", {
152
- logger: consoleLogger,
304
+ logger: consoleLogger, // silent unless set
153
305
  resolver: customResolver,
154
- commandMiddleware: [loggingMiddleware],
306
+ commandMiddleware: [timing],
155
307
  eventMiddleware: [filterMiddleware],
156
308
  serverPassword: process.env.TEAMSPEAK_SERVER_PASSWORD,
157
309
  defaultChannel: "Lobby",
@@ -159,48 +311,158 @@ const client = new Client(identity, "ts.example.com", "Bot", {
159
311
  });
160
312
  ```
161
313
 
314
+ Address resolution — the built-in one or a custom `resolver` — has ten seconds
315
+ in all: the `signal` a resolver is given aborts then, and the client connects
316
+ to the address as given.
317
+
318
+ `commandMiddleware` wraps the typed commands: each middleware receives the
319
+ request (its command name and parameters) with its options and returns the
320
+ answering rows, so it can time, retry or answer a command itself. Raw `exec`
321
+ and `execNoWait` lines do not pass through it.
322
+
323
+ ```typescript
324
+ const timing: CommandMiddleware = (next) => async (request, options) => {
325
+ const started = Date.now();
326
+ try {
327
+ return await next(request, options);
328
+ } finally {
329
+ console.log(request.command, Date.now() - started, "ms");
330
+ }
331
+ };
332
+ ```
333
+
162
334
  Connection auth options:
163
335
 
164
336
  - `serverPassword`: plain-text server password; sent as the TeamSpeak protocol hash during `clientinit`
165
337
  - `defaultChannel`: default channel name requested during `clientinit`
166
338
  - `defaultChannelPassword`: plain-text password for `defaultChannel`; sent as the TeamSpeak protocol hash
167
339
 
168
- ## Subpath Exports
340
+ ### Diagnostics
169
341
 
170
- The package provides granular subpath exports for advanced use cases:
342
+ The client logs nothing unless given a `logger`. For tracing, it publishes on
343
+ two [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
344
+ channels, which cost nothing without a subscriber:
345
+
346
+ - `honeybbq:teamspeak:command` (`commandChannel`), a tracing channel: `start`
347
+ when a command is sent, `asyncEnd` when its answer arrives, `error` when it
348
+ fails.
349
+ - `honeybbq:teamspeak:packet` (`packetChannel`): every command the server sends.
350
+
351
+ Both carry the command's name, never its parameters, which hold password hashes
352
+ and privilege keys.
353
+
354
+ ### Reconnection
355
+
356
+ Off by default: a silent reconnect makes an outage look like a healthy session.
357
+ An explicit `disconnect()` never reconnects, whatever the policy says.
358
+
359
+ ```typescript
360
+ const client = new Client(identity, "ts.example.com", "Bot", {
361
+ reconnect: {
362
+ backoff: { initialDelayMs: 1_000, maxDelayMs: 60_000, factor: 2, jitter: 0.2 },
363
+ // Optional: decide for yourself which failures are worth retrying.
364
+ shouldRetry: (err, attempt) => attempt < 10,
365
+ },
366
+ });
367
+ ```
368
+
369
+ The defaults are conservative because TeamSpeak **bans an IP after roughly
370
+ three rapid connects** — a retry loop without a delay bans itself, and the ban
371
+ outlasts the outage it was recovering from. Recovery is also not instant even
372
+ when it succeeds: the server holds the dropped session for a while and refuses a
373
+ reconnect with `521 too many clones already connected` until it reaps it, so the
374
+ loop keeps trying rather than giving up on the first refusal. It stops once a
375
+ `connect()` of your own has connected the client, and it does not retry an
376
+ address that cannot be one (`InvalidAddressError`).
377
+
378
+ ### Custom transports
379
+
380
+ `fileTransferSocketFactory` and `socketFactory` are the only seams into the
381
+ network layer. Without them the SDK always creates its own sockets, so an
382
+ application running behind a proxy has no way through. `socketFactory` returns
383
+ a `DatagramSocket` — a `node:dgram` socket, or anything with the few methods
384
+ it names — and `fileTransferSocketFactory` any `Duplex` stream, such as the
385
+ socket a SOCKS library returns.
171
386
 
172
387
  ```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";
388
+ const client = new Client(identity, "ts.example.com", "Bot", {
389
+ // UDP, for the voice and command channel.
390
+ socketFactory: (family) => createSocket(family === 6 ? "udp6" : "udp4"),
391
+ // TCP, dialled separately by each file transfer.
392
+ fileTransferSocketFactory: (host, port) => createConnection({ host, port }),
393
+ });
178
394
  ```
179
395
 
396
+ ### Server capabilities
397
+
398
+ ```typescript
399
+ const caps = client.serverCapabilities;
400
+ // { version: "6.0.0-beta13.1", major: 6, family: "ts6", uidAlgorithm: "sha256" }
401
+ ```
402
+
403
+ Read from `initserver` during the handshake, so it costs no extra round trip.
404
+ The uid hash matters because TS3 hashes a client's public key with SHA-1 and TS6
405
+ with SHA-256 — a client that assumes either one computes an identifier the other
406
+ server never sends.
407
+
408
+ ## Compatibility
409
+
410
+ The packages follow semver from 1.0. A few sets are open by nature and may
411
+ gain members in a minor release: the events (`EventMap`, `ServerEventName`),
412
+ `ClientStatus`, `ServerFamily`, `UIDAlgorithm`, `ErrorCode`, `Reason` and the
413
+ enumeration tables. Write a `switch` over any of them with a default case. A
414
+ field the server sends as a number that one of those tables names — a codec, a
415
+ text message's target mode, a group type — is a plain `number`, since a server
416
+ may send a value newer than the table.
417
+
418
+ `DatagramSocket` describes the part of a socket the client uses; should a
419
+ later release use more, the new members will be optional.
420
+
421
+ The [Go SDK](https://github.com/honeybbq/teamspeak-go) makes the same calls
422
+ with the same semantics, apart from what the languages make differ:
423
+
424
+ - Backoff fields: JavaScript takes `undefined` as "the default" and `0` as 0.
425
+ Go cannot tell an unset field from 0, so there a zero field takes the default
426
+ and a negative jitter disables it.
427
+ - A listener that throws: here the exception is rethrown from a microtask, an
428
+ uncaught exception as an EventEmitter listener's would be, and the other
429
+ listeners still get the event. Go recovers a panicking handler, logs it at
430
+ Error, and keeps delivering.
431
+ - Durations are milliseconds with an `Ms` suffix here, `time.Duration` in Go;
432
+ `listClients`' `uid` flag (on by default) is Go's `OmitUID`.
433
+
434
+ ## Packages
435
+
436
+ This package is the voice client. It depends on
437
+ [`@honeybbq/teamspeak-core`](https://github.com/HoneyBBQ/teamspeak-js/tree/main/packages/core) — the command language, ids, errors and
438
+ typed commands it shares with the ServerQuery client — and re-exports all of
439
+ it, so this is the only package to install.
440
+
441
+ The protocol internals (crypto, handshake, transport) are not exported. They
442
+ change with the protocol, and exporting them would have made every change a
443
+ breaking one.
444
+
180
445
  ## Architecture
181
446
 
182
447
  ```
183
- teamspeak-js/
448
+ packages/client/
184
449
  ├── src/
185
450
  │ ├── client.ts # Client lifecycle, connection management
186
- │ ├── api.ts # High-level API (messages, channels, clients)
187
451
  │ ├── commands.ts # Command sending and response tracking
452
+ │ ├── api/voice.ts # Commands only the voice protocol has
453
+ │ ├── capabilities.ts # Server version and uid hash detection
454
+ │ ├── reconnect.ts # Backoff policy and permanent-failure detection
188
455
  │ ├── events.ts # Event handler registration and middleware
189
456
  │ ├── notifications.ts # Server notification parsing and dispatch
190
457
  │ ├── handshake.ts # Protocol handshake orchestration
191
- │ ├── transfer.ts # File transfer operations
458
+ │ ├── transfer.ts # File transfer data connections
192
459
  │ ├── throttle.ts # Token-bucket rate limiter
193
- │ ├── types.ts # Public type definitions
194
- │ ├── errors.ts # Error classes
195
460
  │ ├── crypto/ # ECDH, EAX encryption, identity management
196
461
  │ ├── handshake/ # Crypto handshake and license verification
197
462
  │ ├── transport/ # UDP packet framing, ACK, compression
198
- │ ├── command/ # Command builder and parser
199
463
  │ └── 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
464
+ └── examples/
465
+ └── connect.ts # Minimal connection example
204
466
  ```
205
467
 
206
468
  ## 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"}