yoto-nodejs-client 0.0.2 → 0.0.4

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 (75) hide show
  1. package/README.md +523 -30
  2. package/bin/auth.js +36 -46
  3. package/bin/content.js +0 -0
  4. package/bin/device-model.d.ts +3 -0
  5. package/bin/device-model.d.ts.map +1 -0
  6. package/bin/device-model.js +360 -0
  7. package/bin/device-tui.TODO.md +125 -0
  8. package/bin/device-tui.d.ts +31 -0
  9. package/bin/device-tui.d.ts.map +1 -0
  10. package/bin/device-tui.js +1123 -0
  11. package/bin/devices.js +166 -28
  12. package/bin/groups.js +0 -0
  13. package/bin/icons.js +0 -0
  14. package/bin/lib/cli-helpers.d.ts +1 -1
  15. package/bin/lib/cli-helpers.d.ts.map +1 -1
  16. package/bin/lib/cli-helpers.js +5 -5
  17. package/bin/refresh-token.js +6 -6
  18. package/bin/token-info.js +3 -3
  19. package/index.d.ts +4 -585
  20. package/index.d.ts.map +1 -1
  21. package/index.js +11 -689
  22. package/lib/api-client.d.ts +576 -0
  23. package/lib/api-client.d.ts.map +1 -0
  24. package/lib/api-client.js +681 -0
  25. package/lib/api-endpoints/auth.d.ts +199 -8
  26. package/lib/api-endpoints/auth.d.ts.map +1 -1
  27. package/lib/api-endpoints/auth.js +224 -7
  28. package/lib/api-endpoints/auth.test.js +54 -2
  29. package/lib/api-endpoints/constants.d.ts +14 -8
  30. package/lib/api-endpoints/constants.d.ts.map +1 -1
  31. package/lib/api-endpoints/constants.js +17 -10
  32. package/lib/api-endpoints/content.test.js +1 -1
  33. package/lib/api-endpoints/devices.d.ts +405 -117
  34. package/lib/api-endpoints/devices.d.ts.map +1 -1
  35. package/lib/api-endpoints/devices.js +114 -52
  36. package/lib/api-endpoints/devices.test.js +1 -1
  37. package/lib/api-endpoints/{test-helpers.d.ts → endpoint-test-helpers.d.ts} +1 -1
  38. package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -0
  39. package/lib/api-endpoints/family-library-groups.test.js +1 -1
  40. package/lib/api-endpoints/family.test.js +1 -1
  41. package/lib/api-endpoints/icons.test.js +1 -1
  42. package/lib/helpers/power-state.d.ts +53 -0
  43. package/lib/helpers/power-state.d.ts.map +1 -0
  44. package/lib/helpers/power-state.js +73 -0
  45. package/lib/helpers/power-state.test.js +100 -0
  46. package/lib/helpers/temperature.d.ts +24 -0
  47. package/lib/helpers/temperature.d.ts.map +1 -0
  48. package/lib/helpers/temperature.js +61 -0
  49. package/lib/helpers/temperature.test.js +58 -0
  50. package/lib/helpers/typed-keys.d.ts +7 -0
  51. package/lib/helpers/typed-keys.d.ts.map +1 -0
  52. package/lib/helpers/typed-keys.js +8 -0
  53. package/lib/mqtt/client.d.ts +348 -22
  54. package/lib/mqtt/client.d.ts.map +1 -1
  55. package/lib/mqtt/client.js +213 -31
  56. package/lib/mqtt/factory.d.ts +22 -4
  57. package/lib/mqtt/factory.d.ts.map +1 -1
  58. package/lib/mqtt/factory.js +27 -5
  59. package/lib/mqtt/mqtt.test.js +85 -28
  60. package/lib/mqtt/topics.d.ts +41 -13
  61. package/lib/mqtt/topics.d.ts.map +1 -1
  62. package/lib/mqtt/topics.js +54 -20
  63. package/lib/pkg.d.cts +8 -0
  64. package/lib/token.d.ts +21 -6
  65. package/lib/token.d.ts.map +1 -1
  66. package/lib/token.js +30 -23
  67. package/lib/yoto-account.d.ts +163 -0
  68. package/lib/yoto-account.d.ts.map +1 -0
  69. package/lib/yoto-account.js +340 -0
  70. package/lib/yoto-device.d.ts +656 -0
  71. package/lib/yoto-device.d.ts.map +1 -0
  72. package/lib/yoto-device.js +2850 -0
  73. package/package.json +21 -15
  74. package/lib/api-endpoints/test-helpers.d.ts.map +0 -1
  75. /package/lib/api-endpoints/{test-helpers.js → endpoint-test-helpers.js} +0 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * @import { YotoContentResponse, YotoCreateOrUpdateContentRequest, YotoMyoContentResponse, YotoCreateOrUpdateContentResponse, YotoDeleteContentResponse } from './api-endpoints/content.js'
3
+ * @import { YotoDevicesResponse, YotoDeviceStatusResponse, YotoDeviceConfigResponse, YotoUpdateDeviceConfigRequest, YotoUpdateDeviceConfigResponse, YotoUpdateShortcutsRequest, YotoUpdateShortcutsResponse, YotoDeviceCommand, YotoDeviceCommandResponse } from './api-endpoints/devices.js'
4
+ * @import { YotoGroup, YotoCreateGroupRequest, YotoUpdateGroupRequest, YotoDeleteGroupResponse } from './api-endpoints/family-library-groups.js'
5
+ * @import { YotoFamilyImagesResponse, YotoFamilyImageResponse, YotoUploadFamilyImageResponse } from './api-endpoints/family.js'
6
+ * @import { YotoPublicIconsResponse, YotoUserIconsResponse, YotoUploadIconResponse } from './api-endpoints/icons.js'
7
+ * @import { YotoAudioUploadUrlResponse, YotoUploadCoverImageResponse, YotoCoverType } from './api-endpoints/media.js'
8
+ * @import { YotoTokenResponse, YotoDeviceCodeResponse, YotoDevicePollResult } from './api-endpoints/auth.js'
9
+ * @import { YotoMqttClient } from './mqtt/client.js'
10
+ * @import { MqttClientOptions, YotoMqttOptions } from './mqtt/factory.js'
11
+ * @import { RequestOptions } from './api-endpoints/helpers.js'
12
+ * @import { RefreshSuccessEvent } from './token.js'
13
+ */
14
+
15
+ import { RefreshableToken } from './token.js'
16
+ import * as Auth from './api-endpoints/auth.js'
17
+ import * as Content from './api-endpoints/content.js'
18
+ import * as Devices from './api-endpoints/devices.js'
19
+ import * as FamilyLibraryGroups from './api-endpoints/family-library-groups.js'
20
+ import * as Family from './api-endpoints/family.js'
21
+ import * as Icons from './api-endpoints/icons.js'
22
+ import * as Media from './api-endpoints/media.js'
23
+ import { createYotoMqttClient } from './mqtt/index.js'
24
+
25
+ /**
26
+ * @typedef {Object} YotoClientConstructorOptions
27
+ * @property {string} clientId - OAuth client ID
28
+ * @property {string} refreshToken - OAuth refresh token
29
+ * @property {string} accessToken - Initial OAuth access token (JWT)
30
+ * @property {(refreshedTokenData: RefreshSuccessEvent) => void | Promise<void>} onTokenRefresh - **REQUIRED** Callback invoked when tokens are refreshed. You MUST persist these tokens (to file, database, etc.) as the refresh can happen at any time during API calls. The refresh token may be rotated by the auth server. **DO NOT STUB THIS CALLBACK** - always implement proper persistence logic.
31
+ * @property {number} [bufferSeconds=30] - Seconds before expiration to consider token expired
32
+ * @property {() => void | Promise<void>} [onRefreshStart] - Optional callback invoked when token refresh starts. Defaults to console.log.
33
+ * @property {(error: Error) => void | Promise<void>} [onRefreshError] - Optional callback invoked when token refresh fails with a transient error. Defaults to console.warn.
34
+ * @property {(error: Error) => void | Promise<void>} [onInvalid] - Optional callback invoked when refresh token is permanently invalid. Defaults to console.error.
35
+ * @property {string} [userAgent] - Optional user agent string to identify your application
36
+ * @property {RequestOptions} [defaultRequestOptions] - Default undici request options for all requests (dispatcher, timeouts, etc.)
37
+ */
38
+
39
+ /**
40
+ * Yoto API Client with automatic token refresh
41
+ */
42
+ export class YotoClient {
43
+ // ============================================================================
44
+ // Authentication Static Methods
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Get authorization URL for browser-based OAuth flow
49
+ * @see https://yoto.dev/api/get-authorize/
50
+ * @param {object} params
51
+ * @param {string} params.clientId - OAuth client ID
52
+ * @param {string} params.redirectUri - Redirect URI after authorization
53
+ * @param {'code' | 'token' | 'id_token' | 'code token' | 'code id_token' | 'token id_token' | 'code token id_token'} params.responseType - OAuth response type
54
+ * @param {string} params.state - State parameter for CSRF protection
55
+ * @param {string} [params.audience] - Audience for the token
56
+ * @param {string} [params.scope] - Requested scopes
57
+ * @param {string} [params.nonce] - Nonce for replay attack prevention
58
+ * @param {'none' | 'login' | 'consent' | 'select_account'} [params.prompt] - Authorization prompt behavior
59
+ * @param {number} [params.maxAge] - Maximum authentication age in seconds
60
+ * @param {string} [params.codeChallenge] - PKCE code challenge
61
+ * @param {'S256' | 'plain'} [params.codeChallengeMethod] - PKCE code challenge method
62
+ * @returns {string} Authorization URL
63
+ */
64
+ static getAuthorizeUrl (params) {
65
+ return Auth.getAuthorizeUrl(params)
66
+ }
67
+
68
+ /**
69
+ * Exchange authorization code or refresh token for access tokens
70
+ * @see https://yoto.dev/api/post-oauth-token/
71
+ * @param {object} params
72
+ * @param {'authorization_code' | 'refresh_token' | 'client_credentials' | 'urn:ietf:params:oauth:grant-type:device_code'} params.grantType - OAuth grant type
73
+ * @param {string} [params.code] - Authorization code (required for authorization_code grant)
74
+ * @param {string} [params.redirectUri] - Redirect URI (required for authorization_code grant if used in authorize request)
75
+ * @param {string} [params.refreshToken] - Refresh token (required for refresh_token grant)
76
+ * @param {string} [params.clientId] - OAuth client ID
77
+ * @param {string} [params.clientSecret] - OAuth client secret
78
+ * @param {string} [params.scope] - Requested scope
79
+ * @param {string} [params.codeVerifier] - PKCE code verifier
80
+ * @param {string} [params.deviceCode] - Device code (required for device_code grant)
81
+ * @param {string} [params.audience] - Audience for the token
82
+ * @returns {Promise<YotoTokenResponse>}
83
+ */
84
+ static async exchangeToken (params) {
85
+ return await Auth.exchangeToken(params)
86
+ }
87
+
88
+ /**
89
+ * Request device code for device authorization flow
90
+ * @see https://yoto.dev/api/post-oauth-device-code/
91
+ * @param {object} params
92
+ * @param {string} params.clientId - OAuth client ID
93
+ * @param {string} [params.scope] - Requested scopes
94
+ * @param {string} [params.audience] - Audience for the token
95
+ * @returns {Promise<YotoDeviceCodeResponse>}
96
+ */
97
+ static async requestDeviceCode (params) {
98
+ return await Auth.requestDeviceCode(params)
99
+ }
100
+
101
+ /**
102
+ * Poll for device authorization completion with automatic error handling (single poll attempt).
103
+ * This function handles common polling errors (authorization_pending, slow_down)
104
+ * and only throws for unrecoverable errors (expired_token, access_denied, etc).
105
+ *
106
+ * Non-blocking - returns immediately with poll result. Suitable for:
107
+ * - Manual polling loops in CLI applications
108
+ * - Server-side endpoints that poll on behalf of clients (e.g., Homebridge UI server)
109
+ * - Custom UI implementations with specific polling behavior
110
+ *
111
+ * For the simplest approach (automatic polling loop), use waitForDeviceAuthorization() instead.
112
+ *
113
+ * @see https://yoto.dev/api/post-oauth-token/
114
+ * @param {object} params
115
+ * @param {string} params.deviceCode - Device code from requestDeviceCode()
116
+ * @param {string} params.clientId - OAuth client ID
117
+ * @param {string} [params.audience] - Audience for the token
118
+ * @param {number} [params.currentInterval=5000] - Current polling interval in milliseconds
119
+ * @param {string} [params.userAgent] - Optional user agent string
120
+ * @param {RequestOptions} [params.requestOptions] - Additional undici request options
121
+ * @returns {Promise<YotoDevicePollResult>} Poll result with status and data
122
+ * @throws {YotoAPIError} For unrecoverable errors (expired_token, access_denied, invalid_grant, etc)
123
+ */
124
+ static async pollForDeviceToken (params) {
125
+ return await Auth.pollForDeviceToken(params)
126
+ }
127
+
128
+ /**
129
+ * Wait for device authorization to complete with automatic polling.
130
+ * This function wraps the entire polling loop - just call it and await the result.
131
+ * It handles all polling logic internally including interval adjustments.
132
+ *
133
+ * Designed for CLI usage where you want to block until authorization completes.
134
+ * For UI implementations with progress feedback, use pollForDeviceToken() directly.
135
+ *
136
+ * @see https://yoto.dev/api/post-oauth-token/
137
+ * @param {object} params
138
+ * @param {string} params.deviceCode - Device code from requestDeviceCode()
139
+ * @param {string} params.clientId - OAuth client ID
140
+ * @param {string} [params.audience] - Audience for the token
141
+ * @param {number} [params.initialInterval=5000] - Initial polling interval in milliseconds
142
+ * @param {number} [params.expiresIn] - Seconds until device code expires (for timeout calculation)
143
+ * @param {string} [params.userAgent] - Optional user agent string
144
+ * @param {RequestOptions} [params.requestOptions] - Additional undici request options
145
+ * @param {(result: YotoDevicePollResult) => void} [params.onPoll] - Optional callback invoked after each poll attempt
146
+ * @returns {Promise<YotoTokenResponse>} Token response on successful authorization
147
+ * @throws {YotoAPIError} For unrecoverable errors (expired_token, access_denied, invalid_grant, etc)
148
+ * @throws {Error} If device code expires (timeout)
149
+ */
150
+ static async waitForDeviceAuthorization (params) {
151
+ return await Auth.waitForDeviceAuthorization(params)
152
+ }
153
+
154
+ // ============================================================================
155
+ // Instance Properties and Constructor
156
+ // ============================================================================
157
+ /** @type {RefreshableToken} */
158
+ #token
159
+
160
+ /** @type {string | undefined} */
161
+ #userAgent
162
+
163
+ /** @type {RequestOptions | undefined} */
164
+ #defaultRequestOptions
165
+
166
+ /**
167
+ * Create a new Yoto API client
168
+ * @param {YotoClientConstructorOptions} options
169
+ */
170
+ constructor ({
171
+ clientId,
172
+ refreshToken,
173
+ accessToken,
174
+ onTokenRefresh,
175
+ bufferSeconds,
176
+ onRefreshStart,
177
+ onRefreshError,
178
+ onInvalid,
179
+ userAgent,
180
+ defaultRequestOptions
181
+ }) {
182
+ if (!onTokenRefresh) {
183
+ throw new Error('onTokenRefresh callback is required. You must persist refreshed tokens as they can be updated at any time.')
184
+ }
185
+
186
+ this.#token = new RefreshableToken({
187
+ clientId,
188
+ refreshToken,
189
+ accessToken,
190
+ ...(bufferSeconds !== undefined && { bufferSeconds })
191
+ })
192
+
193
+ this.#userAgent = userAgent
194
+ this.#defaultRequestOptions = defaultRequestOptions
195
+
196
+ // Listen for token refresh events and call the user's callbacks
197
+ this.#token.on('refresh:success', onTokenRefresh)
198
+
199
+ this.#token.on('refresh:start', onRefreshStart || (() => {
200
+ console.log('Token refresh started')
201
+ }))
202
+
203
+ this.#token.on('refresh:error', onRefreshError || ((error) => {
204
+ console.warn('Token refresh failed (transient error):', error.message)
205
+ }))
206
+
207
+ this.#token.on('invalid', onInvalid || ((error) => {
208
+ console.error('Refresh token is permanently invalid:', error.message)
209
+ }))
210
+ }
211
+
212
+ /**
213
+ * Get the underlying RefreshableToken instance
214
+ * @returns {RefreshableToken}
215
+ */
216
+ get token () {
217
+ return this.#token
218
+ }
219
+ // ============================================================================
220
+ // Content API
221
+ // ============================================================================
222
+
223
+ /**
224
+ * Get content/card details
225
+ * @see https://yoto.dev/api/getcontent/
226
+ * @param {object} params
227
+ * @param {string} params.cardId - The card/content ID
228
+ * @param {string} [params.timezone] - Timezone for schedule-based content
229
+ * @param {'full' | 'pre'} [params.signingType] - Type of URL signing
230
+ * @param {boolean} [params.playable] - Whether to include playback URLs
231
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
232
+ * @returns {Promise<YotoContentResponse>}
233
+ */
234
+ async getContent ({ cardId, timezone, signingType, playable, requestOptions }) {
235
+ const accessToken = await this.#token.getAccessToken()
236
+ return await Content.getContent({
237
+ accessToken,
238
+ userAgent: this.#userAgent,
239
+ requestOptions: requestOptions || this.#defaultRequestOptions,
240
+ cardId,
241
+ timezone,
242
+ signingType,
243
+ playable
244
+ })
245
+ }
246
+
247
+ /**
248
+ * Get user's MYO (Make Your Own) content
249
+ * @see https://yoto.dev/api/getusersmyocontent/
250
+ * @param {object} [params]
251
+ * @param {boolean} [params.showDeleted=false] - Include deleted content
252
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
253
+ * @returns {Promise<YotoMyoContentResponse>}
254
+ */
255
+ async getUserMyoContent ({ showDeleted = false, requestOptions } = {}) {
256
+ const accessToken = await this.#token.getAccessToken()
257
+ return await Content.getUserMyoContent({
258
+ accessToken,
259
+ userAgent: this.#userAgent,
260
+ requestOptions: requestOptions || this.#defaultRequestOptions,
261
+ showDeleted
262
+ })
263
+ }
264
+
265
+ /**
266
+ * Create or update content/card
267
+ * @see https://yoto.dev/api/createorupdatecontent/
268
+ * @param {object} params
269
+ * @param {YotoCreateOrUpdateContentRequest} params.content - Content data to create/update
270
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
271
+ * @returns {Promise<YotoCreateOrUpdateContentResponse>}
272
+ */
273
+ async createOrUpdateContent ({ content, requestOptions }) {
274
+ const accessToken = await this.#token.getAccessToken()
275
+ return await Content.createOrUpdateContent({
276
+ accessToken,
277
+ userAgent: this.#userAgent,
278
+ requestOptions: requestOptions || this.#defaultRequestOptions,
279
+ content
280
+ })
281
+ }
282
+
283
+ /**
284
+ * Delete content/card
285
+ * @see https://yoto.dev/api/deletecontent/
286
+ * @param {object} params
287
+ * @param {string} params.cardId - The card/content ID to delete
288
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
289
+ * @returns {Promise<YotoDeleteContentResponse>}
290
+ */
291
+ async deleteContent ({ cardId, requestOptions }) {
292
+ const accessToken = await this.#token.getAccessToken()
293
+ return await Content.deleteContent({
294
+ accessToken,
295
+ userAgent: this.#userAgent,
296
+ requestOptions: requestOptions || this.#defaultRequestOptions,
297
+ cardId
298
+ })
299
+ }
300
+
301
+ // ============================================================================
302
+ // Devices API
303
+ // ============================================================================
304
+
305
+ /**
306
+ * Get all devices for authenticated user
307
+ * @see https://yoto.dev/api/getdevices/
308
+ * @param {object} [params]
309
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
310
+ * @returns {Promise<YotoDevicesResponse>}
311
+ */
312
+ async getDevices ({ requestOptions } = {}) {
313
+ const accessToken = await this.#token.getAccessToken()
314
+ return await Devices.getDevices({ accessToken, userAgent: this.#userAgent, requestOptions: requestOptions || this.#defaultRequestOptions })
315
+ }
316
+
317
+ /**
318
+ * Get device status
319
+ * @see https://yoto.dev/api/getdevicestatus/
320
+ * @param {object} params
321
+ * @param {string} params.deviceId - Device ID
322
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
323
+ * @returns {Promise<YotoDeviceStatusResponse>}
324
+ */
325
+ async getDeviceStatus ({ deviceId, requestOptions }) {
326
+ const accessToken = await this.#token.getAccessToken()
327
+ return await Devices.getDeviceStatus({
328
+ accessToken,
329
+ userAgent: this.#userAgent,
330
+ requestOptions: requestOptions || this.#defaultRequestOptions,
331
+ deviceId
332
+ })
333
+ }
334
+
335
+ /**
336
+ * Get device configuration
337
+ * @see https://yoto.dev/api/getdeviceconfig/
338
+ * @param {object} params
339
+ * @param {string} params.deviceId - Device ID
340
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
341
+ * @returns {Promise<YotoDeviceConfigResponse>}
342
+ */
343
+ async getDeviceConfig ({ deviceId, requestOptions }) {
344
+ const accessToken = await this.#token.getAccessToken()
345
+ return await Devices.getDeviceConfig({
346
+ accessToken,
347
+ userAgent: this.#userAgent,
348
+ requestOptions: requestOptions || this.#defaultRequestOptions,
349
+ deviceId
350
+ })
351
+ }
352
+
353
+ /**
354
+ * Update device configuration
355
+ * @see https://yoto.dev/api/updatedeviceconfig/
356
+ * @param {object} params
357
+ * @param {string} params.deviceId - Device ID
358
+ * @param {YotoUpdateDeviceConfigRequest} params.configUpdate - Config updates
359
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
360
+ * @returns {Promise<YotoUpdateDeviceConfigResponse>}
361
+ */
362
+ async updateDeviceConfig ({ deviceId, configUpdate, requestOptions }) {
363
+ const accessToken = await this.#token.getAccessToken()
364
+ return await Devices.updateDeviceConfig({
365
+ accessToken,
366
+ userAgent: this.#userAgent,
367
+ requestOptions: requestOptions || this.#defaultRequestOptions,
368
+ deviceId,
369
+ configUpdate
370
+ })
371
+ }
372
+
373
+ /**
374
+ * Update device shortcuts
375
+ * @see https://yoto.dev/api/updateshortcutsbeta/
376
+ * @param {object} params
377
+ * @param {string} params.deviceId - Device ID
378
+ * @param {YotoUpdateShortcutsRequest} params.shortcutsUpdate - Shortcuts config
379
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
380
+ * @returns {Promise<YotoUpdateShortcutsResponse>}
381
+ */
382
+ async updateDeviceShortcuts ({ deviceId, shortcutsUpdate, requestOptions }) {
383
+ const accessToken = await this.#token.getAccessToken()
384
+ return await Devices.updateDeviceShortcuts({
385
+ accessToken,
386
+ userAgent: this.#userAgent,
387
+ requestOptions: requestOptions || this.#defaultRequestOptions,
388
+ deviceId,
389
+ shortcutsUpdate
390
+ })
391
+ }
392
+
393
+ /**
394
+ * Send command to device
395
+ * @see https://yoto.dev/api/senddevicecommand/
396
+ * @see https://yoto.dev/players-mqtt/mqtt-docs/
397
+ * @param {object} params
398
+ * @param {string} params.deviceId - Device ID
399
+ * @param {YotoDeviceCommand} params.command - Command to send
400
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
401
+ * @returns {Promise<YotoDeviceCommandResponse>}
402
+ */
403
+ async sendDeviceCommand ({ deviceId, command, requestOptions }) {
404
+ const accessToken = await this.#token.getAccessToken()
405
+ return await Devices.sendDeviceCommand({
406
+ accessToken,
407
+ userAgent: this.#userAgent,
408
+ requestOptions: requestOptions || this.#defaultRequestOptions,
409
+ deviceId,
410
+ command
411
+ })
412
+ }
413
+
414
+ // ============================================================================
415
+ // Family Library Groups API
416
+ // ============================================================================
417
+
418
+ /**
419
+ * Get all family library groups
420
+ * @see https://yoto.dev/api/getgroups/
421
+ * @param {object} [params]
422
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
423
+ * @returns {Promise<YotoGroup[]>}
424
+ */
425
+ async getGroups ({ requestOptions } = {}) {
426
+ const accessToken = await this.#token.getAccessToken()
427
+ return await FamilyLibraryGroups.getGroups({ accessToken, userAgent: this.#userAgent, requestOptions: requestOptions || this.#defaultRequestOptions })
428
+ }
429
+
430
+ /**
431
+ * Create a family library group
432
+ * @see https://yoto.dev/api/createagroup/
433
+ * @param {object} params
434
+ * @param {YotoCreateGroupRequest} params.group - Group data
435
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
436
+ * @returns {Promise<YotoGroup>}
437
+ */
438
+ async createGroup ({ group, requestOptions }) {
439
+ const accessToken = await this.#token.getAccessToken()
440
+ return await FamilyLibraryGroups.createGroup({
441
+ token: accessToken,
442
+ userAgent: this.#userAgent,
443
+ requestOptions: requestOptions || this.#defaultRequestOptions,
444
+ group
445
+ })
446
+ }
447
+
448
+ /**
449
+ * Get a specific family library group
450
+ * @see https://yoto.dev/api/getgroup/
451
+ * @param {object} params
452
+ * @param {string} params.groupId - Group ID
453
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
454
+ * @returns {Promise<YotoGroup>}
455
+ */
456
+ async getGroup ({ groupId, requestOptions }) {
457
+ const accessToken = await this.#token.getAccessToken()
458
+ return await FamilyLibraryGroups.getGroup({
459
+ accessToken,
460
+ userAgent: this.#userAgent,
461
+ requestOptions: requestOptions || this.#defaultRequestOptions,
462
+ groupId
463
+ })
464
+ }
465
+
466
+ /**
467
+ * Update a family library group
468
+ * @see https://yoto.dev/api/updateagroup/
469
+ * @param {object} params
470
+ * @param {string} params.groupId - Group ID
471
+ * @param {YotoUpdateGroupRequest} params.group - Updated group data
472
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
473
+ * @returns {Promise<YotoGroup>}
474
+ */
475
+ async updateGroup ({ groupId, group, requestOptions }) {
476
+ const accessToken = await this.#token.getAccessToken()
477
+ return await FamilyLibraryGroups.updateGroup({
478
+ accessToken,
479
+ userAgent: this.#userAgent,
480
+ requestOptions: requestOptions || this.#defaultRequestOptions,
481
+ groupId,
482
+ group
483
+ })
484
+ }
485
+
486
+ /**
487
+ * Delete a family library group
488
+ * @see https://yoto.dev/api/deleteagroup/
489
+ * @param {object} params
490
+ * @param {string} params.groupId - Group ID
491
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
492
+ * @returns {Promise<YotoDeleteGroupResponse>}
493
+ */
494
+ async deleteGroup ({ groupId, requestOptions }) {
495
+ const accessToken = await this.#token.getAccessToken()
496
+ return await FamilyLibraryGroups.deleteGroup({
497
+ accessToken,
498
+ userAgent: this.#userAgent,
499
+ requestOptions: requestOptions || this.#defaultRequestOptions,
500
+ groupId
501
+ })
502
+ }
503
+
504
+ // ============================================================================
505
+ // Family Images API
506
+ // ============================================================================
507
+
508
+ /**
509
+ * Get all family images
510
+ * @see https://yoto.dev/api/getfamilyimages/
511
+ * @param {object} [params]
512
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
513
+ * @returns {Promise<YotoFamilyImagesResponse>}
514
+ */
515
+ async getFamilyImages ({ requestOptions } = {}) {
516
+ const accessToken = await this.#token.getAccessToken()
517
+ return await Family.getFamilyImages({ accessToken, userAgent: this.#userAgent, requestOptions: requestOptions || this.#defaultRequestOptions })
518
+ }
519
+
520
+ /**
521
+ * Get a specific family image
522
+ * @see https://yoto.dev/api/getafamilyimage/
523
+ * @param {object} params
524
+ * @param {string} params.imageId - Image ID
525
+ * @param {'640x480' | '320x320'} params.size - Image size
526
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
527
+ * @returns {Promise<YotoFamilyImageResponse>}
528
+ */
529
+ async getAFamilyImage ({ imageId, size, requestOptions }) {
530
+ const accessToken = await this.#token.getAccessToken()
531
+ return await Family.getAFamilyImage({
532
+ accessToken,
533
+ userAgent: this.#userAgent,
534
+ requestOptions: requestOptions || this.#defaultRequestOptions,
535
+ imageId,
536
+ size
537
+ })
538
+ }
539
+
540
+ /**
541
+ * Upload a family image
542
+ * @see https://yoto.dev/api/uploadafamilyimage/
543
+ * @param {object} params
544
+ * @param {Buffer} params.imageData - Image binary data
545
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
546
+ * @returns {Promise<YotoUploadFamilyImageResponse>}
547
+ */
548
+ async uploadAFamilyImage ({ imageData, requestOptions }) {
549
+ const accessToken = await this.#token.getAccessToken()
550
+ return await Family.uploadAFamilyImage({
551
+ accessToken,
552
+ userAgent: this.#userAgent,
553
+ requestOptions: requestOptions || this.#defaultRequestOptions,
554
+ imageData
555
+ })
556
+ }
557
+
558
+ // ============================================================================
559
+ // Icons API
560
+ // ============================================================================
561
+
562
+ /**
563
+ * Get public Yoto icons
564
+ * @see https://yoto.dev/api/getpublicicons/
565
+ * @param {object} [params]
566
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
567
+ * @returns {Promise<YotoPublicIconsResponse>}
568
+ */
569
+ async getPublicIcons ({ requestOptions } = {}) {
570
+ const accessToken = await this.#token.getAccessToken()
571
+ return await Icons.getPublicIcons({ accessToken, userAgent: this.#userAgent, requestOptions: requestOptions || this.#defaultRequestOptions })
572
+ }
573
+
574
+ /**
575
+ * Get user's custom icons
576
+ * @see https://yoto.dev/api/getusericons/
577
+ * @param {object} [params]
578
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
579
+ * @returns {Promise<YotoUserIconsResponse>}
580
+ */
581
+ async getUserIcons ({ requestOptions } = {}) {
582
+ const accessToken = await this.#token.getAccessToken()
583
+ return await Icons.getUserIcons({ accessToken, userAgent: this.#userAgent, requestOptions: requestOptions || this.#defaultRequestOptions })
584
+ }
585
+
586
+ /**
587
+ * Upload a custom icon
588
+ * @see https://yoto.dev/api/uploadicon/
589
+ * @param {object} params
590
+ * @param {Buffer} params.imageData - Image binary data
591
+ * @param {boolean} [params.autoConvert=true] - Auto-convert to proper format
592
+ * @param {string} [params.filename] - Optional filename
593
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
594
+ * @returns {Promise<YotoUploadIconResponse>}
595
+ */
596
+ async uploadIcon ({ imageData, autoConvert = true, filename, requestOptions }) {
597
+ const accessToken = await this.#token.getAccessToken()
598
+ return await Icons.uploadIcon({
599
+ accessToken,
600
+ userAgent: this.#userAgent,
601
+ requestOptions: requestOptions || this.#defaultRequestOptions,
602
+ imageData,
603
+ autoConvert,
604
+ filename
605
+ })
606
+ }
607
+
608
+ // ============================================================================
609
+ // Media API
610
+ // ============================================================================
611
+
612
+ /**
613
+ * Get audio upload URL
614
+ * @see https://yoto.dev/api/getaudiouploadurl/
615
+ * @param {object} params
616
+ * @param {string} params.sha256 - SHA256 hash of audio file
617
+ * @param {string} [params.filename] - Optional filename
618
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
619
+ * @returns {Promise<YotoAudioUploadUrlResponse>}
620
+ */
621
+ async getAudioUploadUrl ({ sha256, filename, requestOptions }) {
622
+ const accessToken = await this.#token.getAccessToken()
623
+ return await Media.getAudioUploadUrl({
624
+ accessToken,
625
+ userAgent: this.#userAgent,
626
+ requestOptions: requestOptions || this.#defaultRequestOptions,
627
+ sha256,
628
+ filename
629
+ })
630
+ }
631
+
632
+ /**
633
+ * Upload a cover image
634
+ * @see https://yoto.dev/api/uploadcoverimage/
635
+ * @param {object} params
636
+ * @param {Buffer} [params.imageData] - Image binary data
637
+ * @param {string} [params.imageUrl] - URL to image
638
+ * @param {boolean} [params.autoConvert] - Auto-convert to proper format
639
+ * @param {YotoCoverType} [params.coverType] - Cover image type
640
+ * @param {string} [params.filename] - Optional filename
641
+ * @param {RequestOptions} [params.requestOptions] - Request options that override defaults
642
+ * @returns {Promise<YotoUploadCoverImageResponse>}
643
+ */
644
+ async uploadCoverImage ({ imageData, imageUrl, autoConvert, coverType, filename, requestOptions }) {
645
+ const accessToken = await this.#token.getAccessToken()
646
+ return await Media.uploadCoverImage({
647
+ accessToken,
648
+ userAgent: this.#userAgent,
649
+ requestOptions: requestOptions || this.#defaultRequestOptions,
650
+ imageData,
651
+ imageUrl,
652
+ autoConvert,
653
+ coverType,
654
+ filename
655
+ })
656
+ }
657
+
658
+ // ============================================================================
659
+ // MQTT Client
660
+ // ============================================================================
661
+
662
+ /**
663
+ * Create an MQTT client for a device
664
+ * @param {Object} params
665
+ * @param {string} params.deviceId - Device ID to connect to
666
+ * @param {MqttClientOptions} [params.mqttOptions] - MQTT.js client options (excluding deviceId and accessToken which are provided automatically)
667
+ * @returns {Promise<YotoMqttClient>}
668
+ */
669
+ async createMqttClient ({ deviceId, mqttOptions }) {
670
+ const accessToken = await this.#token.getAccessToken()
671
+
672
+ /** @type {YotoMqttOptions} */
673
+ const opts = {
674
+ deviceId,
675
+ accessToken,
676
+ ...mqttOptions
677
+ }
678
+
679
+ return createYotoMqttClient(opts)
680
+ }
681
+ }