yoto-nodejs-client 0.0.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 (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +736 -0
  3. package/bin/auth.d.ts +3 -0
  4. package/bin/auth.d.ts.map +1 -0
  5. package/bin/auth.js +130 -0
  6. package/bin/content.d.ts +3 -0
  7. package/bin/content.d.ts.map +1 -0
  8. package/bin/content.js +117 -0
  9. package/bin/devices.d.ts +3 -0
  10. package/bin/devices.d.ts.map +1 -0
  11. package/bin/devices.js +239 -0
  12. package/bin/groups.d.ts +3 -0
  13. package/bin/groups.d.ts.map +1 -0
  14. package/bin/groups.js +80 -0
  15. package/bin/icons.d.ts +3 -0
  16. package/bin/icons.d.ts.map +1 -0
  17. package/bin/icons.js +100 -0
  18. package/bin/lib/cli-helpers.d.ts +21 -0
  19. package/bin/lib/cli-helpers.d.ts.map +1 -0
  20. package/bin/lib/cli-helpers.js +140 -0
  21. package/bin/lib/token-helpers.d.ts +14 -0
  22. package/bin/lib/token-helpers.d.ts.map +1 -0
  23. package/bin/lib/token-helpers.js +151 -0
  24. package/bin/refresh-token.d.ts +3 -0
  25. package/bin/refresh-token.d.ts.map +1 -0
  26. package/bin/refresh-token.js +168 -0
  27. package/bin/token-info.d.ts +3 -0
  28. package/bin/token-info.d.ts.map +1 -0
  29. package/bin/token-info.js +351 -0
  30. package/index.d.ts +218 -0
  31. package/index.d.ts.map +1 -0
  32. package/index.js +689 -0
  33. package/lib/api-endpoints/auth.d.ts +56 -0
  34. package/lib/api-endpoints/auth.d.ts.map +1 -0
  35. package/lib/api-endpoints/auth.js +209 -0
  36. package/lib/api-endpoints/auth.test.js +27 -0
  37. package/lib/api-endpoints/constants.d.ts +6 -0
  38. package/lib/api-endpoints/constants.d.ts.map +1 -0
  39. package/lib/api-endpoints/constants.js +31 -0
  40. package/lib/api-endpoints/content.d.ts +275 -0
  41. package/lib/api-endpoints/content.d.ts.map +1 -0
  42. package/lib/api-endpoints/content.js +518 -0
  43. package/lib/api-endpoints/content.test.js +250 -0
  44. package/lib/api-endpoints/devices.d.ts +202 -0
  45. package/lib/api-endpoints/devices.d.ts.map +1 -0
  46. package/lib/api-endpoints/devices.js +404 -0
  47. package/lib/api-endpoints/devices.test.js +483 -0
  48. package/lib/api-endpoints/family-library-groups.d.ts +75 -0
  49. package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
  50. package/lib/api-endpoints/family-library-groups.js +247 -0
  51. package/lib/api-endpoints/family-library-groups.test.js +272 -0
  52. package/lib/api-endpoints/family.d.ts +39 -0
  53. package/lib/api-endpoints/family.d.ts.map +1 -0
  54. package/lib/api-endpoints/family.js +166 -0
  55. package/lib/api-endpoints/family.test.js +184 -0
  56. package/lib/api-endpoints/helpers.d.ts +29 -0
  57. package/lib/api-endpoints/helpers.d.ts.map +1 -0
  58. package/lib/api-endpoints/helpers.js +104 -0
  59. package/lib/api-endpoints/icons.d.ts +62 -0
  60. package/lib/api-endpoints/icons.d.ts.map +1 -0
  61. package/lib/api-endpoints/icons.js +201 -0
  62. package/lib/api-endpoints/icons.test.js +118 -0
  63. package/lib/api-endpoints/media.d.ts +37 -0
  64. package/lib/api-endpoints/media.d.ts.map +1 -0
  65. package/lib/api-endpoints/media.js +155 -0
  66. package/lib/api-endpoints/test-helpers.d.ts +7 -0
  67. package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
  68. package/lib/api-endpoints/test-helpers.js +64 -0
  69. package/lib/mqtt/client.d.ts +124 -0
  70. package/lib/mqtt/client.d.ts.map +1 -0
  71. package/lib/mqtt/client.js +558 -0
  72. package/lib/mqtt/commands.d.ts +69 -0
  73. package/lib/mqtt/commands.d.ts.map +1 -0
  74. package/lib/mqtt/commands.js +238 -0
  75. package/lib/mqtt/factory.d.ts +12 -0
  76. package/lib/mqtt/factory.d.ts.map +1 -0
  77. package/lib/mqtt/factory.js +107 -0
  78. package/lib/mqtt/index.d.ts +5 -0
  79. package/lib/mqtt/index.d.ts.map +1 -0
  80. package/lib/mqtt/index.js +81 -0
  81. package/lib/mqtt/mqtt.test.js +168 -0
  82. package/lib/mqtt/topics.d.ts +34 -0
  83. package/lib/mqtt/topics.d.ts.map +1 -0
  84. package/lib/mqtt/topics.js +295 -0
  85. package/lib/pkg.cjs +3 -0
  86. package/lib/pkg.d.cts +70 -0
  87. package/lib/pkg.d.cts.map +1 -0
  88. package/lib/token.d.ts +29 -0
  89. package/lib/token.d.ts.map +1 -0
  90. package/lib/token.js +240 -0
  91. package/package.json +91 -0
  92. package/yoto.png +0 -0
package/README.md ADDED
@@ -0,0 +1,736 @@
1
+ # yoto-nodejs-client
2
+ [![latest version](https://img.shields.io/npm/v/yoto-nodejs-client.svg)](https://www.npmjs.com/package/yoto-nodejs-client)
3
+ [![Actions Status](https://github.com/bcomnes/yoto-nodejs-client/workflows/tests/badge.svg)](https://github.com/bcomnes/yoto-nodejs-client/actions)
4
+
5
+ [![downloads](https://img.shields.io/npm/dm/yoto-nodejs-client.svg)](https://npmtrends.com/yoto-nodejs-client)
6
+ ![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)
7
+ [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-7fffff?style=flat&labelColor=ff80ff)](https://github.com/neostandard/neostandard)
8
+ [![Socket Badge](https://socket.dev/api/badge/npm/package/yoto-nodejs-client)](https://socket.dev/npm/package/yoto-nodejs-client)
9
+
10
+ A comprehensive Node.js client for the [Yoto API][yoto-api] with automatic token refresh, MQTT device communication, and full TypeScript support.
11
+
12
+ <p align="center">
13
+ <img src="yoto.png" alt="Yoto" width="200">
14
+ </p>
15
+
16
+ ```console
17
+ npm install yoto-nodejs-client
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```js
23
+ import { YotoClient } from 'yoto-nodejs-client'
24
+
25
+ // Authenticate using device flow (CLI/server applications)
26
+ const deviceCodeResponse = await YotoClient.requestDeviceCode({
27
+ clientId: 'your-client-id'
28
+ })
29
+
30
+ console.log(`Visit ${deviceCodeResponse.verification_uri_complete}`)
31
+ console.log(`Enter code: ${deviceCodeResponse.user_code}`)
32
+
33
+ // Poll for token
34
+ const tokenResponse = await YotoClient.exchangeToken({
35
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code',
36
+ deviceCode: deviceCodeResponse.device_code,
37
+ clientId: 'your-client-id'
38
+ })
39
+
40
+ // Create client with automatic token refresh
41
+ const client = new YotoClient({
42
+ clientId: 'your-client-id',
43
+ refreshToken: tokenResponse.refresh_token,
44
+ accessToken: tokenResponse.access_token,
45
+ onTokenRefresh: async (event) => {
46
+ // REQUIRED: Persist tokens when they refresh
47
+ await saveTokens({
48
+ accessToken: event.accessToken,
49
+ refreshToken: event.refreshToken,
50
+ expiresAt: event.expiresAt
51
+ })
52
+ }
53
+ })
54
+
55
+ // Get devices
56
+ const { devices } = await client.getDevices()
57
+ console.log('Your devices:', devices)
58
+
59
+ // Get device status
60
+ const status = await client.getDeviceStatus({
61
+ deviceId: devices[0].deviceId
62
+ })
63
+ console.log('Battery:', status.batteryLevelPercentage, '%')
64
+
65
+ // Get user's MYO content
66
+ const myoContent = await client.getUserMyoContent()
67
+ console.log('Your content:', myoContent)
68
+
69
+ // Connect to device via MQTT for real-time control
70
+ const mqtt = await client.createMqttClient({
71
+ deviceId: devices[0].deviceId
72
+ })
73
+
74
+ mqtt.on('events', (message) => {
75
+ console.log('Playing:', message.trackTitle)
76
+ })
77
+
78
+ mqtt.on('status', (message) => {
79
+ console.log('Volume:', message.volume, '%')
80
+ })
81
+
82
+ await mqtt.connect()
83
+ await mqtt.setVolume(50)
84
+ await mqtt.setAmbientHex('#FF0000')
85
+ ```
86
+
87
+ ## API
88
+
89
+ ### Authentication
90
+
91
+ #### `YotoClient.requestDeviceCode({ clientId, [scope], [audience] })`
92
+
93
+ Start the OAuth2 Device Authorization flow for CLI/server applications. Returns a device code and user verification URL.
94
+
95
+ - **clientId** - Your OAuth client ID
96
+ - **scope** - OAuth scopes (default: `'openid profile offline_access'`)
97
+ - **audience** - Token audience (default: `'https://api.yotoplay.com'`)
98
+
99
+ See [Yoto API: Device Code][api-device-code]
100
+
101
+ ```js
102
+ const response = await YotoClient.requestDeviceCode({
103
+ clientId: 'your-client-id'
104
+ })
105
+
106
+ console.log(`Visit: ${response.verification_uri_complete}`)
107
+ console.log(`Or go to ${response.verification_uri} and enter: ${response.user_code}`)
108
+ ```
109
+
110
+ #### `YotoClient.exchangeToken({ grantType, ...params })`
111
+
112
+ Exchange authorization code, refresh token, or device code for access tokens.
113
+
114
+ - **grantType** - `'authorization_code'`, `'refresh_token'`, or `'urn:ietf:params:oauth:grant-type:device_code'`
115
+ - **code** - Authorization code (for `authorization_code` grant)
116
+ - **refreshToken** - Refresh token (for `refresh_token` grant)
117
+ - **deviceCode** - Device code (for device code grant)
118
+ - **clientId** - OAuth client ID
119
+ - **redirectUri** - Redirect URI (for `authorization_code` grant)
120
+ - **codeVerifier** - PKCE code verifier (optional)
121
+
122
+ See [Yoto API: Token Exchange][api-token]
123
+
124
+ ```js
125
+ // Exchange device code
126
+ const tokens = await YotoClient.exchangeToken({
127
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code',
128
+ deviceCode: response.device_code,
129
+ clientId: 'your-client-id'
130
+ })
131
+
132
+ // Refresh token
133
+ const refreshed = await YotoClient.exchangeToken({
134
+ grantType: 'refresh_token',
135
+ refreshToken: tokens.refresh_token,
136
+ clientId: 'your-client-id'
137
+ })
138
+ ```
139
+
140
+ #### `YotoClient.getAuthorizeUrl({ clientId, redirectUri, responseType, state, ...params })`
141
+
142
+ Get authorization URL for browser-based OAuth flow. Returns a URL string to redirect users to.
143
+
144
+ See [Yoto API: Authorization][api-authorize]
145
+
146
+ ### Client Instance
147
+
148
+ #### `new YotoClient({ clientId, refreshToken, accessToken, onTokenRefresh, [options] })`
149
+
150
+ Create a new Yoto API client with automatic token refresh.
151
+
152
+ - **clientId** - OAuth client ID
153
+ - **refreshToken** - OAuth refresh token
154
+ - **accessToken** - Initial access token (JWT)
155
+ - **onTokenRefresh** - **REQUIRED** callback for token refresh events. You MUST persist tokens here.
156
+ - **bufferSeconds** - Seconds before expiration to refresh (default: 30)
157
+ - **userAgent** - Optional user agent string to identify your application
158
+ - **defaultRequestOptions** - Optional default undici request options (dispatcher, timeouts, etc.) applied to all requests
159
+ - **onRefreshStart** - Optional callback when refresh starts
160
+ - **onRefreshError** - Optional callback for transient refresh errors
161
+ - **onInvalid** - Optional callback when refresh token is permanently invalid
162
+
163
+ ```js
164
+ const client = new YotoClient({
165
+ clientId: 'your-client-id',
166
+ refreshToken: 'stored-refresh-token',
167
+ accessToken: 'stored-access-token',
168
+ userAgent: 'MyApp/1.0.0', // Optional - identifies your application
169
+ defaultRequestOptions: { // Optional - undici request options for all requests
170
+ bodyTimeout: 30000, // 30 second timeout
171
+ headersTimeout: 10000, // 10 second header timeout
172
+ // dispatcher, signal, etc.
173
+ },
174
+ onTokenRefresh: async ({ accessToken, refreshToken, expiresAt }) => {
175
+ // Save to database, file, etc.
176
+ await db.saveTokens({ accessToken, refreshToken, expiresAt })
177
+ }
178
+ })
179
+ ```
180
+
181
+ #### Request Options
182
+
183
+ All API methods accept an optional `requestOptions` parameter that allows you to override the default undici request options for individual requests. This is useful for setting custom timeouts, using a specific dispatcher, or aborting requests.
184
+
185
+ ```js
186
+ // Override timeout for a specific request
187
+ const content = await client.getContent({
188
+ cardId: '5WsQg',
189
+ requestOptions: {
190
+ bodyTimeout: 60000, // 60 second timeout for this request only
191
+ headersTimeout: 20000
192
+ }
193
+ })
194
+
195
+ // Use AbortSignal to cancel requests
196
+ const controller = new AbortController()
197
+ setTimeout(() => controller.abort(), 5000)
198
+
199
+ try {
200
+ const devices = await client.getDevices({
201
+ requestOptions: { signal: controller.signal }
202
+ })
203
+ } catch (err) {
204
+ if (err.name === 'AbortError') {
205
+ console.log('Request was aborted')
206
+ }
207
+ }
208
+
209
+ // Use custom dispatcher for connection pooling
210
+ import { Agent } from 'undici'
211
+ const agent = new Agent({ connections: 10 })
212
+
213
+ const status = await client.getDeviceStatus({
214
+ deviceId: 'abc123',
215
+ requestOptions: { dispatcher: agent }
216
+ })
217
+ ```
218
+
219
+ **Available Request Options:**
220
+ - `bodyTimeout` - Body timeout in milliseconds
221
+ - `headersTimeout` - Headers timeout in milliseconds
222
+ - `signal` - AbortSignal to cancel the request
223
+ - `dispatcher` - Custom undici dispatcher (for connection pooling, proxies, etc.)
224
+ - `reset` - Reset connection after request
225
+ - `throwOnError` - Throw on HTTP error status codes
226
+ - `idempotent` - Whether the requests can be safely retried
227
+ - `blocking` - Whether the response is expected to take a long time
228
+ - Other undici RequestOptions (see [undici documentation](https://undici.nodejs.org/))
229
+
230
+ ### Content API
231
+
232
+ #### `await client.getContent({ cardId, [timezone], [signingType], [playable] })`
233
+
234
+ Get content/card details including metadata, chapters, and optionally playback URLs.
235
+
236
+ See [Yoto API: Get Content][api-get-content]
237
+
238
+ ```js
239
+ const content = await client.getContent({
240
+ cardId: '5WsQg',
241
+ playable: true // Include signed playback URLs
242
+ })
243
+
244
+ console.log(content.title)
245
+ console.log(content.chapters)
246
+ ```
247
+
248
+ #### `await client.getUserMyoContent({ [showDeleted] })`
249
+
250
+ Get user's MYO (Make Your Own) content library.
251
+
252
+ See [Yoto API: Get User's MYO Content][api-get-myo]
253
+
254
+ ```js
255
+ const myoContent = await client.getUserMyoContent({
256
+ showDeleted: false
257
+ })
258
+
259
+ myoContent.cards.forEach(card => {
260
+ console.log(`${card.metadata.title} - ${card.chapters.length} chapters`)
261
+ })
262
+ ```
263
+
264
+ #### `await client.createOrUpdateContent({ content })`
265
+
266
+ Create new content or update existing content by cardId.
267
+
268
+ See [Yoto API: Create or Update Content][api-create-content]
269
+
270
+ ```js
271
+ const newCard = await client.createOrUpdateContent({
272
+ content: {
273
+ title: 'My Story',
274
+ chapters: [
275
+ {
276
+ title: 'Chapter 1',
277
+ tracks: [
278
+ {
279
+ title: 'Part 1',
280
+ key: 'upload-id-from-audio-upload'
281
+ }
282
+ ]
283
+ }
284
+ ]
285
+ }
286
+ })
287
+
288
+ console.log('Created card:', newCard.cardId)
289
+ ```
290
+
291
+ #### `await client.deleteContent({ cardId })`
292
+
293
+ Delete content/card.
294
+
295
+ See [Yoto API: Delete Content][api-delete-content]
296
+
297
+ ```js
298
+ await client.deleteContent({ cardId: '5WsQg' })
299
+ ```
300
+
301
+ ### Devices API
302
+
303
+ #### `await client.getDevices()`
304
+
305
+ Get all devices for authenticated user.
306
+
307
+ See [Yoto API: Get Devices][api-get-devices]
308
+
309
+ ```js
310
+ const { devices } = await client.getDevices()
311
+
312
+ devices.forEach(device => {
313
+ console.log(`${device.name} (${device.deviceId}) - ${device.online ? 'online' : 'offline'}`)
314
+ })
315
+ ```
316
+
317
+ #### `await client.getDeviceStatus({ deviceId })`
318
+
319
+ Get current status of a specific device including battery, volume, active card, etc.
320
+
321
+ See [Yoto API: Get Device Status][api-device-status]
322
+
323
+ ```js
324
+ const status = await client.getDeviceStatus({
325
+ deviceId: 'abc123'
326
+ })
327
+
328
+ console.log('Battery:', status.batteryLevelPercentage, '%')
329
+ console.log('Charging:', status.isCharging)
330
+ console.log('Volume:', status.userVolumePercentage, '%')
331
+ console.log('Active card:', status.activeCard)
332
+ ```
333
+
334
+ #### `await client.getDeviceConfig({ deviceId })`
335
+
336
+ Get device configuration including settings, timezone, shortcuts, etc.
337
+
338
+ See [Yoto API: Get Device Config][api-device-config]
339
+
340
+ ```js
341
+ const config = await client.getDeviceConfig({
342
+ deviceId: 'abc123'
343
+ })
344
+
345
+ console.log('Name:', config.device.name)
346
+ console.log('Day time:', config.device.config.dayTime)
347
+ console.log('Night time:', config.device.config.nightTime)
348
+ console.log('Max volume:', config.device.config.maxVolumeLimit)
349
+ ```
350
+
351
+ #### `await client.updateDeviceConfig({ deviceId, configUpdate })`
352
+
353
+ Update device configuration settings.
354
+
355
+ See [Yoto API: Update Device Config][api-update-config]
356
+
357
+ ```js
358
+ await client.updateDeviceConfig({
359
+ deviceId: 'abc123',
360
+ configUpdate: {
361
+ name: 'Bedroom Player',
362
+ config: {
363
+ dayTime: '07:00',
364
+ nightTime: '19:00',
365
+ maxVolumeLimit: '80'
366
+ }
367
+ }
368
+ })
369
+ ```
370
+
371
+ #### `await client.updateDeviceShortcuts({ deviceId, shortcutsUpdate })`
372
+
373
+ Update device shortcuts configuration (beta feature).
374
+
375
+ See [Yoto API: Update Shortcuts][api-update-shortcuts]
376
+
377
+ #### `await client.sendDeviceCommand({ deviceId, command })`
378
+
379
+ Send MQTT command to device via HTTP API (alternative to MQTT client).
380
+
381
+ See [Yoto API: Send Device Command][api-send-command]
382
+
383
+ ```js
384
+ await client.sendDeviceCommand({
385
+ deviceId: 'abc123',
386
+ command: {
387
+ volume: 50
388
+ }
389
+ })
390
+ ```
391
+
392
+ ### Family Library Groups API
393
+
394
+ #### `await client.getGroups()`
395
+
396
+ Get all family library groups.
397
+
398
+ See [Yoto API: Get Groups][api-get-groups]
399
+
400
+ ```js
401
+ const groups = await client.getGroups()
402
+
403
+ groups.forEach(group => {
404
+ console.log(`${group.name}: ${group.items.length} items`)
405
+ })
406
+ ```
407
+
408
+ #### `await client.createGroup({ group })`
409
+
410
+ Create a new family library group.
411
+
412
+ See [Yoto API: Create Group][api-create-group]
413
+
414
+ ```js
415
+ const group = await client.createGroup({
416
+ group: {
417
+ name: 'Bedtime Stories',
418
+ imageId: 'fp-cards',
419
+ items: [
420
+ { contentId: '5WsQg' },
421
+ { contentId: '7KpLq' }
422
+ ]
423
+ }
424
+ })
425
+ ```
426
+
427
+ #### `await client.getGroup({ groupId })`
428
+
429
+ Get a specific group by ID.
430
+
431
+ See [Yoto API: Get a Group][api-get-group]
432
+
433
+ #### `await client.updateGroup({ groupId, group })`
434
+
435
+ Update an existing group.
436
+
437
+ See [Yoto API: Update Group][api-update-group]
438
+
439
+ #### `await client.deleteGroup({ groupId })`
440
+
441
+ Delete a group permanently.
442
+
443
+ See [Yoto API: Delete Group][api-delete-group]
444
+
445
+ ### Family API
446
+
447
+ #### `await client.getFamilyImages()`
448
+
449
+ Get list of uploaded family images.
450
+
451
+ See [Yoto API: Get Family Images][api-family-images]
452
+
453
+ ```js
454
+ const { images } = await client.getFamilyImages()
455
+
456
+ images.forEach(image => {
457
+ console.log(`${image.name || 'Unnamed'}: ${image.imageId}`)
458
+ })
459
+ ```
460
+
461
+ #### `await client.getAFamilyImage({ imageId, size })`
462
+
463
+ Get signed URL for a family image.
464
+
465
+ See [Yoto API: Get a Family Image][api-get-family-image]
466
+
467
+ ```js
468
+ const { imageUrl } = await client.getAFamilyImage({
469
+ imageId: 'abc123hash',
470
+ size: '640x480' // or '320x320'
471
+ })
472
+
473
+ console.log('Image URL:', imageUrl)
474
+ ```
475
+
476
+ #### `await client.uploadAFamilyImage({ imageData })`
477
+
478
+ Upload a family image for use across Yoto features.
479
+
480
+ See [Yoto API: Upload Family Image][api-upload-family-image]
481
+
482
+ ```js
483
+ import { readFile } from 'fs/promises'
484
+
485
+ const imageData = await readFile('./family-photo.jpg')
486
+ const result = await client.uploadAFamilyImage({ imageData })
487
+
488
+ console.log('Image ID:', result.imageId)
489
+ ```
490
+
491
+ ### Icons API
492
+
493
+ #### `await client.getPublicIcons()`
494
+
495
+ Get list of public display icons available to all users.
496
+
497
+ See [Yoto API: Get Public Icons][api-public-icons]
498
+
499
+ ```js
500
+ const { displayIcons } = await client.getPublicIcons()
501
+
502
+ displayIcons.forEach(icon => {
503
+ console.log(`${icon.title}: ${icon.displayIconId}`)
504
+ })
505
+ ```
506
+
507
+ #### `await client.getUserIcons()`
508
+
509
+ Get user's custom uploaded icons.
510
+
511
+ See [Yoto API: Get User Icons][api-user-icons]
512
+
513
+ #### `await client.uploadIcon({ imageData, [autoConvert], [filename] })`
514
+
515
+ Upload a custom 16×16px display icon.
516
+
517
+ See [Yoto API: Upload Custom Icon][api-upload-icon]
518
+
519
+ ```js
520
+ import { readFile } from 'fs/promises'
521
+
522
+ const imageData = await readFile('./my-icon.png')
523
+ const result = await client.uploadIcon({
524
+ imageData,
525
+ autoConvert: true, // Auto-resize and process
526
+ filename: 'my-custom-icon'
527
+ })
528
+
529
+ console.log('Icon ID:', result.displayIcon.displayIconId)
530
+ ```
531
+
532
+ ### Media API
533
+
534
+ #### `await client.getAudioUploadUrl({ sha256, [filename] })`
535
+
536
+ Get signed URL for uploading audio files. Files are deduplicated by SHA256 hash.
537
+
538
+ See [Yoto API: Get Audio Upload URL][api-audio-upload]
539
+
540
+ ```js
541
+ import { createHash } from 'crypto'
542
+ import { readFile } from 'fs/promises'
543
+
544
+ const audioData = await readFile('./story.mp3')
545
+ const sha256 = createHash('sha256').update(audioData).digest('hex')
546
+
547
+ const { upload } = await client.getAudioUploadUrl({
548
+ sha256,
549
+ filename: 'story.mp3'
550
+ })
551
+
552
+ if (upload.uploadUrl) {
553
+ // File doesn't exist, upload it
554
+ await fetch(upload.uploadUrl, {
555
+ method: 'PUT',
556
+ body: audioData
557
+ })
558
+ }
559
+
560
+ // Use upload.uploadId in content creation
561
+ ```
562
+
563
+ #### `await client.uploadCoverImage({ [imageData], [imageUrl], [coverType], [autoConvert], [filename] })`
564
+
565
+ Upload a cover image for content cards.
566
+
567
+ See [Yoto API: Upload Cover Image][api-cover-image]
568
+
569
+ ```js
570
+ import { readFile } from 'fs/promises'
571
+
572
+ const imageData = await readFile('./cover.jpg')
573
+ const { coverImage } = await client.uploadCoverImage({
574
+ imageData,
575
+ coverType: 'default', // 638×1011px
576
+ autoConvert: true
577
+ })
578
+
579
+ console.log('Cover image ID:', coverImage.mediaId)
580
+ ```
581
+
582
+ ### MQTT Client
583
+
584
+ #### `await client.createMqttClient({ deviceId, [options] })`
585
+
586
+ Create an MQTT client for real-time device communication and control.
587
+
588
+ See [Yoto MQTT Documentation][mqtt-docs]
589
+
590
+ ```js
591
+ const mqtt = await client.createMqttClient({
592
+ deviceId: 'abc123',
593
+ autoResubscribe: true,
594
+ keepAliveSeconds: 1200
595
+ })
596
+
597
+ // Listen for real-time events
598
+ mqtt.on('events', (message) => {
599
+ console.log('Track:', message.trackTitle)
600
+ console.log('Card:', message.cardTitle)
601
+ console.log('Status:', message.playbackStatus)
602
+ })
603
+
604
+ // Listen for status updates
605
+ mqtt.on('status', (message) => {
606
+ console.log('Volume:', message.volume)
607
+ console.log('Battery:', message.batteryLevel)
608
+ console.log('Charging:', message.charging)
609
+ })
610
+
611
+ // Listen for command responses
612
+ mqtt.on('response', (message) => {
613
+ console.log('Command response:', message)
614
+ })
615
+
616
+ // Connect to device
617
+ await mqtt.connect()
618
+
619
+ // Control device
620
+ await mqtt.setVolume(50)
621
+ await mqtt.setAmbientHex('#FF0000')
622
+ await mqtt.setSleepTimer(30) // 30 minutes
623
+ await mqtt.startCard({ cardId: '5WsQg' })
624
+ await mqtt.pauseCard()
625
+ await mqtt.resumeCard()
626
+ await mqtt.stopCard()
627
+
628
+ // Disconnect when done
629
+ await mqtt.disconnect()
630
+ ```
631
+
632
+ #### MQTT Events
633
+
634
+ The MQTT client emits three types of messages:
635
+
636
+ - **`events`** - Real-time playback events (track changes, play/pause, volume adjustments)
637
+ - **`status`** - Device status updates (battery, configuration, online state)
638
+ - **`response`** - Command confirmation responses
639
+
640
+ #### MQTT Methods
641
+
642
+ - `await mqtt.connect()` - Connect to device MQTT broker
643
+ - `await mqtt.disconnect()` - Disconnect from broker
644
+ - `await mqtt.setVolume(volume)` - Set volume (0-100)
645
+ - `await mqtt.setAmbientHex(hex)` - Set ambient light color (e.g., '#FF0000')
646
+ - `await mqtt.setSleepTimer(minutes)` - Set sleep timer (0 to disable)
647
+ - `await mqtt.startCard({ cardId, chapterKey, trackKey })` - Start playing a card
648
+ - `await mqtt.pauseCard()` - Pause current playback
649
+ - `await mqtt.resumeCard()` - Resume playback
650
+ - `await mqtt.stopCard()` - Stop playback
651
+ - `await mqtt.reboot()` - Reboot device
652
+
653
+ ## CLI Tools
654
+
655
+ The library includes CLI tools for authentication and data inspection:
656
+
657
+ ### Authentication
658
+
659
+ ```bash
660
+ # Get initial tokens (device flow)
661
+ node bin/auth.js --output .env
662
+
663
+ # Refresh tokens
664
+ node bin/refresh-token.js
665
+
666
+ # Show token info
667
+ node bin/token-info.js
668
+ ```
669
+
670
+ ### Data Inspection
671
+
672
+ ```bash
673
+ # List all devices
674
+ node bin/devices.js
675
+
676
+ # Get device details with config
677
+ node bin/devices.js --device-id abc123
678
+
679
+ # Get device status only
680
+ node bin/devices.js --device-id abc123 --status
681
+
682
+ # Connect to MQTT and listen for messages
683
+ node bin/devices.js --device-id abc123 --mqtt
684
+
685
+ # Sample MQTT messages for 10 seconds
686
+ node bin/devices.js --device-id abc123 --mqtt --mqtt-stop-after-seconds 10
687
+
688
+ # List all MYO content
689
+ node bin/content.js
690
+
691
+ # Get specific card details
692
+ node bin/content.js --card-id 5WsQg
693
+
694
+ # Get card with playable URLs
695
+ node bin/content.js --card-id 5WsQg --playable
696
+ ```
697
+
698
+ ## See also
699
+
700
+ - [Yoto API Documentation][yoto-api]
701
+ - [Yoto MQTT Documentation][mqtt-docs]
702
+ - [Yoto Developer Portal][yoto-dev]
703
+
704
+ ## License
705
+
706
+ MIT
707
+
708
+ [yoto-api]: https://yoto.dev/api/
709
+ [yoto-dev]: https://yoto.dev/
710
+ [mqtt-docs]: https://yoto.dev/players-mqtt/mqtt-docs/
711
+ [api-device-code]: https://yoto.dev/api/post-oauth-device-code/
712
+ [api-token]: https://yoto.dev/api/post-oauth-token/
713
+ [api-authorize]: https://yoto.dev/api/get-authorize/
714
+ [api-get-content]: https://yoto.dev/api/getcontent/
715
+ [api-get-myo]: https://yoto.dev/api/getusersmyocontent/
716
+ [api-create-content]: https://yoto.dev/api/createorupdatecontent/
717
+ [api-delete-content]: https://yoto.dev/api/deletecontent/
718
+ [api-get-devices]: https://yoto.dev/api/getdevices/
719
+ [api-device-status]: https://yoto.dev/api/getdevicestatus/
720
+ [api-device-config]: https://yoto.dev/api/getdeviceconfig/
721
+ [api-update-config]: https://yoto.dev/api/updatedeviceconfig/
722
+ [api-update-shortcuts]: https://yoto.dev/api/updateshortcutsbeta/
723
+ [api-send-command]: https://yoto.dev/api/senddevicecommand/
724
+ [api-get-groups]: https://yoto.dev/api/getgroups/
725
+ [api-create-group]: https://yoto.dev/api/createagroup/
726
+ [api-get-group]: https://yoto.dev/api/getagroup/
727
+ [api-update-group]: https://yoto.dev/api/updateagroup/
728
+ [api-delete-group]: https://yoto.dev/api/deleteagroup/
729
+ [api-family-images]: https://yoto.dev/api/getfamilyimages/
730
+ [api-get-family-image]: https://yoto.dev/api/getafamilyimage/
731
+ [api-upload-family-image]: https://yoto.dev/api/uploadafamilyimage/
732
+ [api-public-icons]: https://yoto.dev/api/getpublicicons/
733
+ [api-user-icons]: https://yoto.dev/api/getusericons/
734
+ [api-upload-icon]: https://yoto.dev/api/uploadcustomicon/
735
+ [api-audio-upload]: https://yoto.dev/api/getanuploadurl/
736
+ [api-cover-image]: https://yoto.dev/api/uploadcoverimage/