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
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @import { RequestOptions } from './helpers.js'
3
+ */
4
+ import { request } from 'undici'
5
+ import { defaultAuthHeaders, handleBadResponse, mergeRequestOptions } from './helpers.js'
6
+ import { YOTO_API_URL } from './constants.js'
7
+
8
+ // ============================================================================
9
+ // Icons: Display icon endpoints for public and custom user icons
10
+ // ============================================================================
11
+
12
+ /**
13
+ * @see https://yoto.dev/api/getpublicicons/
14
+ * @typedef {Object} YotoPublicIconsResponse
15
+ * @property {YotoPublicIcon[]} displayIcons - Array of public display icons
16
+ */
17
+
18
+ /**
19
+ * @see https://yoto.dev/api/getpublicicons/
20
+ * @typedef {Object} YotoPublicIcon
21
+ * @property {string} displayIconId - Unique identifier for the icon
22
+ * @property {string} mediaId - Unique identifier for the underlying icon file
23
+ * @property {string} userId - ID of the user who uploaded this icon (always "yoto" for public icons)
24
+ * @property {string} createdAt - ISO 8601 timestamp when icon record was created
25
+ * @property {string} title - Title of the display icon
26
+ * @property {string} url - URL of the display icon
27
+ * @property {boolean} public - Indicates if the icon is public (always true for public icons)
28
+ * @property {boolean} [new] - Indicates if this is a new icon (may not always be present)
29
+ * @property {string[]} publicTags - Public tags associated with the display icon
30
+ */
31
+
32
+ /**
33
+ * Retrieves the list of public icons that are available to every user.
34
+ * @see https://yoto.dev/api/getpublicicons/
35
+ * @param {object} options
36
+ * @param {string} options.accessToken The API token to request with
37
+ * @param {string} [options.userAgent] Optional user agent string
38
+ * @param {RequestOptions} [options.requestOptions] Additional undici request options
39
+ * @return {Promise<YotoPublicIconsResponse>} Public display icons
40
+ * @example
41
+ * import { getPublicIcons } from 'yoto-nodejs-client'
42
+ *
43
+ * const icons = await getPublicIcons({
44
+ * accessToken
45
+ * })
46
+ *
47
+ * console.log(`Found ${icons.displayIcons.length} public icons`)
48
+ * icons.displayIcons.forEach(icon => {
49
+ * console.log(`${icon.title} - Tags: ${icon.publicTags.join(', ')}`)
50
+ * })
51
+ */
52
+ export async function getPublicIcons ({
53
+ accessToken,
54
+ userAgent,
55
+ requestOptions
56
+ }) {
57
+ const requestUrl = new URL('/media/displayIcons/user/yoto', YOTO_API_URL)
58
+
59
+ const response = await request(requestUrl, mergeRequestOptions({
60
+ method: 'GET',
61
+ headers: defaultAuthHeaders({ accessToken, userAgent })
62
+ }, requestOptions))
63
+
64
+ await handleBadResponse(response)
65
+
66
+ const responseBody = /** @type {YotoPublicIconsResponse} */ (await response.body.json())
67
+ return responseBody
68
+ }
69
+
70
+ /**
71
+ * @see https://yoto.dev/api/getusericons/
72
+ * @typedef {Object} YotoUserIconsResponse
73
+ * @property {YotoUserIcon[]} displayIcons - Array of user's custom display icons
74
+ */
75
+
76
+ /**
77
+ * @see https://yoto.dev/api/getusericons/
78
+ * @typedef {Object} YotoUserIcon
79
+ * @property {string} displayIconId - Unique identifier for the icon
80
+ * @property {string} mediaId - Unique identifier for the underlying icon file
81
+ * @property {string} userId - ID of the user who uploaded this icon
82
+ * @property {string} createdAt - ISO 8601 timestamp when icon record was created
83
+ * @property {string} url - URL of the display icon
84
+ * @property {boolean} public - Indicates if the icon is public (always false for user icons)
85
+ */
86
+
87
+ /**
88
+ * Retrieves the authenticated user's custom uploaded icons.
89
+ * @see https://yoto.dev/api/getusericons/
90
+ * @param {object} options
91
+ * @param {string} options.accessToken The API token to request with
92
+ * @param {string} [options.userAgent] Optional user agent string
93
+ * @param {RequestOptions} [options.requestOptions] Additional undici request options
94
+ * @return {Promise<YotoUserIconsResponse>} User's custom display icons
95
+ */
96
+ export async function getUserIcons ({
97
+ accessToken,
98
+ userAgent,
99
+ requestOptions
100
+ }) {
101
+ const requestUrl = new URL('/media/displayIcons/user/me', YOTO_API_URL)
102
+
103
+ const response = await request(requestUrl, mergeRequestOptions({
104
+ method: 'GET',
105
+ headers: defaultAuthHeaders({ accessToken, userAgent })
106
+ }, requestOptions))
107
+
108
+ await handleBadResponse(response)
109
+
110
+ const responseBody = /** @type {YotoUserIconsResponse} */ (await response.body.json())
111
+ return responseBody
112
+ }
113
+
114
+ /**
115
+ * @see https://yoto.dev/api/uploadcustomicon/
116
+ * @typedef {Object} YotoUploadIconResponse
117
+ * @property {YotoDisplayIcon} displayIcon - The uploaded or existing display icon
118
+ */
119
+
120
+ /**
121
+ * @see https://yoto.dev/api/uploadcustomicon/
122
+ * @typedef {Object} YotoDisplayIcon
123
+ * @property {string} displayIconId - Unique identifier for the icon
124
+ * @property {string} mediaId - Unique identifier for the underlying icon file
125
+ * @property {string} userId - ID of the user who uploaded this icon
126
+ * @property {string | object} url - URL of the display icon, or empty object {} for duplicates
127
+ * @property {boolean} [new] - True if this is a new upload, undefined for duplicates
128
+ * @property {string} [_id] - MongoDB ID (present for duplicate uploads)
129
+ * @property {string} [createdAt] - ISO 8601 timestamp (present for duplicate uploads)
130
+ */
131
+
132
+ /**
133
+ * Uploads a custom 16×16px icon for the authenticated user.
134
+ * Icons are deduplicated by content - re-uploading the same image returns the existing icon.
135
+ *
136
+ * Image processing with autoConvert=true (recommended):
137
+ * - Auto-resizes to 16×16px (crop/pad as needed)
138
+ * - Adjusts brightness if > ⅔
139
+ * - Converts to PNG
140
+ * - Accepts any image format
141
+ *
142
+ * Image requirements with autoConvert=false (strict):
143
+ * - Must be exactly 16×16px
144
+ * - Only PNG or GIF allowed
145
+ * - PNG must be 24-bit RGBA (sRGB, 4 channels, hasAlpha, no palette)
146
+ * - GIF accepted as-is
147
+ *
148
+ * @see https://yoto.dev/api/uploadcustomicon/
149
+ * @param {object} options
150
+ * @param {string} options.accessToken The API token to request with
151
+ * @param {Buffer} options.imageData The binary image data (16×16px icon)
152
+ * @param {boolean} [options.autoConvert=true] Auto-resize and process the image to 16×16px
153
+ * @param {string} [options.filename] Override the stored base filename
154
+ * @param {string} [options.userAgent] Optional user agent string
155
+ * @param {RequestOptions} [options.requestOptions] Additional undici request options
156
+ * @return {Promise<YotoUploadIconResponse>} The uploaded or existing display icon
157
+ * @example
158
+ * import { readFile } from 'fs/promises'
159
+ * import { uploadIcon } from 'yoto-nodejs-client'
160
+ *
161
+ * const imageData = await readFile('./my-icon.png')
162
+ * const result = await uploadIcon({
163
+ * accessToken,
164
+ * imageData,
165
+ * autoConvert: true,
166
+ * filename: 'my-custom-icon'
167
+ * })
168
+ *
169
+ * if (result.displayIcon.new) {
170
+ * console.log('New icon uploaded:', result.displayIcon.displayIconId)
171
+ * } else {
172
+ * console.log('Icon already exists:', result.displayIcon.displayIconId)
173
+ * }
174
+ */
175
+ export async function uploadIcon ({
176
+ accessToken,
177
+ userAgent,
178
+ imageData,
179
+ autoConvert = true,
180
+ filename,
181
+ requestOptions
182
+ }) {
183
+ const requestUrl = new URL('/media/displayIcons/user/me/upload', YOTO_API_URL)
184
+
185
+ if (autoConvert !== undefined) requestUrl.searchParams.set('autoConvert', autoConvert.toString())
186
+ if (filename) requestUrl.searchParams.set('filename', filename)
187
+
188
+ const response = await request(requestUrl, mergeRequestOptions({
189
+ method: 'POST',
190
+ headers: {
191
+ ...defaultAuthHeaders({ accessToken, userAgent }),
192
+ 'Content-Type': 'application/octet-stream'
193
+ },
194
+ body: imageData
195
+ }, requestOptions))
196
+
197
+ await handleBadResponse(response)
198
+
199
+ const responseBody = /** @type {YotoUploadIconResponse} */ (await response.body.json())
200
+ return responseBody
201
+ }
@@ -0,0 +1,118 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { getPublicIcons, getUserIcons } from './icons.js'
4
+ import { YotoAPIError } from './helpers.js'
5
+ import { loadTestTokens, logResponse } from './test-helpers.js'
6
+
7
+ const { accessToken } = loadTestTokens()
8
+
9
+ test('getPublicIcons', async (t) => {
10
+ await t.test('should fetch public display icons', async () => {
11
+ const response = await getPublicIcons({
12
+ accessToken
13
+ })
14
+
15
+ // Log response for type verification and documentation
16
+ logResponse('GET /media/displayIcons/user/yoto', response)
17
+
18
+ // Validate response structure matches YotoPublicIconsResponse
19
+ assert.ok(response, 'Response should exist')
20
+ assert.ok(response.displayIcons, 'Response should have displayIcons property')
21
+ assert.ok(Array.isArray(response.displayIcons), 'displayIcons should be an array')
22
+ assert.ok(response.displayIcons.length > 0, 'Should have at least one public icon')
23
+
24
+ // Validate icon structure
25
+ const icon = response.displayIcons[0]
26
+ assert.ok(icon, 'Icon should exist')
27
+ assert.ok(typeof icon.displayIconId === 'string', 'Icon should have displayIconId string')
28
+ assert.ok(typeof icon.mediaId === 'string', 'Icon should have mediaId string')
29
+ assert.ok(typeof icon.userId === 'string', 'Icon should have userId string')
30
+ assert.strictEqual(icon.userId, 'yoto', 'Public icon userId should be "yoto"')
31
+ assert.ok(typeof icon.createdAt === 'string', 'Icon should have createdAt string')
32
+ assert.ok(typeof icon.title === 'string', 'Icon should have title string')
33
+ assert.ok(typeof icon.url === 'string', 'Icon should have url string')
34
+ assert.ok(icon.url.startsWith('http'), 'Icon URL should be a valid URL')
35
+ assert.strictEqual(icon.public, true, 'Public icon should have public=true')
36
+ // Note: new field may not always be present in public icons
37
+ if (icon.new !== undefined) {
38
+ assert.strictEqual(typeof icon.new, 'boolean', 'Icon new should be boolean when present')
39
+ }
40
+ assert.ok(Array.isArray(icon.publicTags), 'Icon should have publicTags array')
41
+ })
42
+
43
+ await t.test('should fail with invalid token', async () => {
44
+ await assert.rejects(
45
+ async () => {
46
+ await getPublicIcons({
47
+ accessToken: 'invalid-token'
48
+ })
49
+ },
50
+ (err) => {
51
+ assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
52
+ assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
53
+ assert.ok(err.body, 'Error should have body')
54
+ return true
55
+ }
56
+ )
57
+ })
58
+ })
59
+
60
+ test('getUserIcons', async (t) => {
61
+ await t.test('should fetch user custom icons', async () => {
62
+ const response = await getUserIcons({
63
+ accessToken
64
+ })
65
+
66
+ // Log response for type verification and documentation
67
+ logResponse('GET /media/displayIcons/user/me', response)
68
+
69
+ // Validate response structure matches YotoUserIconsResponse
70
+ assert.ok(response, 'Response should exist')
71
+ assert.ok(response.displayIcons, 'Response should have displayIcons property')
72
+ assert.ok(Array.isArray(response.displayIcons), 'displayIcons should be an array')
73
+
74
+ // Note: User may not have any custom icons, so we only validate structure if icons exist
75
+ if (response.displayIcons.length > 0) {
76
+ const icon = response.displayIcons[0]
77
+ assert.ok(icon, 'Icon should exist')
78
+ assert.ok(typeof icon.displayIconId === 'string', 'Icon should have displayIconId string')
79
+ assert.ok(typeof icon.mediaId === 'string', 'Icon should have mediaId string')
80
+ assert.ok(typeof icon.userId === 'string', 'Icon should have userId string')
81
+ assert.ok(typeof icon.createdAt === 'string', 'Icon should have createdAt string')
82
+ assert.ok(typeof icon.url === 'string', 'Icon should have url string')
83
+ assert.strictEqual(icon.public, false, 'User icon should have public=false')
84
+ assert.strictEqual('title' in icon, false, 'User icon should not have title')
85
+ assert.strictEqual('publicTags' in icon, false, 'User icon should not have publicTags')
86
+ }
87
+ })
88
+
89
+ await t.test('should fail with invalid token', async () => {
90
+ await assert.rejects(
91
+ async () => {
92
+ await getUserIcons({
93
+ accessToken: 'invalid-token'
94
+ })
95
+ },
96
+ (err) => {
97
+ assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
98
+ assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
99
+ assert.ok(err.body, 'Error should have body')
100
+ return true
101
+ }
102
+ )
103
+ })
104
+ })
105
+
106
+ // TODO: Add tests for uploadIcon
107
+ // - should upload a new icon with autoConvert=true
108
+ // - should return new=true for first upload
109
+ // - should validate response structure matches YotoUploadIconResponse
110
+ // - should upload icon with custom filename
111
+ // - should handle re-upload of same icon (deduplication)
112
+ // - should return existing icon with url={} for duplicate
113
+ // - should list user icons and find uploaded icon
114
+ // - should upload icon with autoConvert=false (strict mode)
115
+ // - should fail with invalid dimensions when autoConvert=false
116
+ // - should fail with invalid format when autoConvert=false
117
+ // - should fail with file that's not an image
118
+ // - should fail with invalid token
@@ -0,0 +1,37 @@
1
+ export function getAudioUploadUrl({ accessToken, userAgent, sha256, filename, requestOptions }: {
2
+ accessToken: string;
3
+ sha256: string;
4
+ filename?: string | undefined;
5
+ userAgent?: string | undefined;
6
+ requestOptions?: ({
7
+ dispatcher?: import("undici").Dispatcher;
8
+ } & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
9
+ }): Promise<YotoAudioUploadUrlResponse>;
10
+ export function uploadCoverImage({ accessToken, userAgent, imageData, imageUrl, autoConvert, coverType, filename, requestOptions }: {
11
+ accessToken: string;
12
+ imageData?: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike> | undefined;
13
+ imageUrl?: string | undefined;
14
+ autoConvert?: boolean | undefined;
15
+ coverType?: YotoCoverType | undefined;
16
+ filename?: string | undefined;
17
+ userAgent?: string | undefined;
18
+ requestOptions?: ({
19
+ dispatcher?: import("undici").Dispatcher;
20
+ } & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
21
+ }): Promise<YotoUploadCoverImageResponse>;
22
+ export type YotoAudioUpload = {
23
+ uploadId: string;
24
+ uploadUrl: string | null;
25
+ };
26
+ export type YotoAudioUploadUrlResponse = {
27
+ upload: YotoAudioUpload;
28
+ };
29
+ export type YotoCoverImage = {
30
+ mediaId: string;
31
+ mediaUrl: string;
32
+ };
33
+ export type YotoUploadCoverImageResponse = {
34
+ coverImage: YotoCoverImage;
35
+ };
36
+ export type YotoCoverType = "default" | "activities" | "music" | "myo" | "podcast" | "radio" | "sfx" | "stories";
37
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["media.js"],"names":[],"mappings":"AAmEA,gGAPG;IAAwB,WAAW,EAA3B,MAAM;IACU,MAAM,EAAtB,MAAM;IACW,QAAQ;IACR,SAAS;IACD,cAAc;;;CAC/C,GAAU,OAAO,CAAC,0BAA0B,CAAC,CAuB/C;AA6BD,oIAVG;IAAwB,WAAW,EAA3B,MAAM;IACiC,SAAS;IAC/B,QAAQ;IACP,WAAW;IACL,SAAS;IAChB,QAAQ;IACR,SAAS;IACD,cAAc;;;CAC/C,GAAU,OAAO,CAAC,4BAA4B,CAAC,CAuCjD;;cArIa,MAAM;eACN,MAAM,GAAG,IAAI;;;YAMb,eAAe;;;aAMf,MAAM;cACN,MAAM;;;gBAMN,cAAc;;4BAKf,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @import { RequestOptions } from './helpers.js'
3
+ */
4
+ import { request } from 'undici'
5
+ import { defaultAuthHeaders, handleBadResponse, mergeRequestOptions } from './helpers.js'
6
+ import { YOTO_API_URL } from './constants.js'
7
+
8
+ // ============================================================================
9
+ // Media: Media endpoints for audio uploads and cover images
10
+ //
11
+ // Audio Transcoding:
12
+ // - All uploaded audio files are transcoded using FFmpeg
13
+ // - Converted to MP4 format with AAC codec (libfdk_aac)
14
+ // - Bit Rate: 128Kbps, Sample Rate: 11.025/22.05/44.1/48 kHz
15
+ // - Files are transcoded to balance quality vs compression for players
16
+ // - Maximum file size: 1GB (arbitrary limit to prevent timeout/corruption)
17
+ // ============================================================================
18
+
19
+ /**
20
+ * @see https://yoto.dev/api/getanuploadurl/
21
+ * @typedef {Object} YotoAudioUpload
22
+ * @property {string} uploadId - Upload identifier
23
+ * @property {string | null} uploadUrl - Signed upload URL, or null if file already exists
24
+ */
25
+
26
+ /**
27
+ * @see https://yoto.dev/api/getanuploadurl/
28
+ * @typedef {Object} YotoAudioUploadUrlResponse
29
+ * @property {YotoAudioUpload} upload
30
+ */
31
+
32
+ /**
33
+ * @see https://yoto.dev/api/uploadcoverimage/
34
+ * @typedef {Object} YotoCoverImage
35
+ * @property {string} mediaId - Media identifier
36
+ * @property {string} mediaUrl - URL to access the uploaded cover image
37
+ */
38
+
39
+ /**
40
+ * @see https://yoto.dev/api/uploadcoverimage/
41
+ * @typedef {Object} YotoUploadCoverImageResponse
42
+ * @property {YotoCoverImage} coverImage
43
+ */
44
+
45
+ /**
46
+ * @see https://yoto.dev/api/uploadcoverimage/
47
+ * @typedef {'default' | 'activities' | 'music' | 'myo' | 'podcast' | 'radio' | 'sfx' | 'stories'} YotoCoverType
48
+ */
49
+
50
+ /**
51
+ * Get a signed URL for uploading an audio file. The SHA256 hash is used to check
52
+ * if a file with that checksum already exists (deduplication).
53
+ *
54
+ * Response behavior:
55
+ * - If file already exists: uploadUrl will be null (file is already in store)
56
+ * - If file doesn't exist: uploadUrl contains a signed URL for uploading
57
+ * - uploadId is always returned and can be used to reference the upload
58
+ *
59
+ * @see https://yoto.dev/api/getanuploadurl/
60
+ * @param {object} options
61
+ * @param {string} options.accessToken - Authentication token
62
+ * @param {string} options.sha256 - SHA256 hash of the file to upload
63
+ * @param {string} [options.filename] - Optional filename for the uploaded file
64
+ * @param {string} [options.userAgent] - Optional user agent string
65
+ * @param {RequestOptions} [options.requestOptions] - Additional undici request options
66
+ * @returns {Promise<YotoAudioUploadUrlResponse>}
67
+ */
68
+ export async function getAudioUploadUrl ({
69
+ accessToken,
70
+ userAgent,
71
+ sha256,
72
+ filename,
73
+ requestOptions
74
+ }) {
75
+ const requestUrl = new URL('/media/transcode/audio/uploadUrl', YOTO_API_URL)
76
+
77
+ requestUrl.searchParams.set('sha256', sha256)
78
+ if (filename) requestUrl.searchParams.set('filename', filename)
79
+
80
+ const response = await request(requestUrl, mergeRequestOptions({
81
+ method: 'GET',
82
+ headers: defaultAuthHeaders({ accessToken, userAgent })
83
+ }, requestOptions))
84
+
85
+ await handleBadResponse(response, { sha256 })
86
+
87
+ const responseBody = /** @type {YotoAudioUploadUrlResponse} */ (await response.body.json())
88
+ return responseBody
89
+ }
90
+
91
+ /**
92
+ * Upload a cover image to the user's media account. Supports both direct binary
93
+ * uploads and fetching from a URL. Images are automatically resized based on coverType.
94
+ *
95
+ * Image processing:
96
+ * - Images are resized according to coverType (default: 638x1011px)
97
+ * - Aspect ratio is preserved
98
+ * - Images are cropped to fit dimensions, positioned to center
99
+ * - Supports automatic image conversion with autoConvert flag
100
+ *
101
+ * Cover type dimensions:
102
+ * - default/activities/music/sfx/stories: 638×1011px
103
+ * - myo: 520×400px
104
+ * - podcast/radio: 600×600px
105
+ *
106
+ * @see https://yoto.dev/api/uploadcoverimage/
107
+ * @param {object} options
108
+ * @param {string} options.accessToken - Authentication token
109
+ * @param {Buffer | Uint8Array | string} [options.imageData] - Binary image data to upload (required if imageUrl not provided)
110
+ * @param {string} [options.imageUrl] - URL of image to fetch and upload (required if imageData not provided)
111
+ * @param {boolean} [options.autoConvert] - Whether to automatically convert the image
112
+ * @param {YotoCoverType} [options.coverType] - Type of cover image, determines dimensions
113
+ * @param {string} [options.filename] - Custom filename for the uploaded image
114
+ * @param {string} [options.userAgent] - Optional user agent string
115
+ * @param {RequestOptions} [options.requestOptions] - Additional undici request options
116
+ * @returns {Promise<YotoUploadCoverImageResponse>}
117
+ */
118
+ export async function uploadCoverImage ({
119
+ accessToken,
120
+ userAgent,
121
+ imageData,
122
+ imageUrl,
123
+ autoConvert,
124
+ coverType,
125
+ filename,
126
+ requestOptions
127
+ }) {
128
+ const requestUrl = new URL('/media/coverImage/user/me/upload', YOTO_API_URL)
129
+
130
+ if (imageUrl) requestUrl.searchParams.set('imageUrl', imageUrl)
131
+ if (autoConvert !== undefined) requestUrl.searchParams.set('autoconvert', autoConvert.toString())
132
+ if (coverType) requestUrl.searchParams.set('coverType', coverType)
133
+ if (filename) requestUrl.searchParams.set('filename', filename)
134
+
135
+ // Build headers - add Content-Type for binary uploads
136
+ const headers = imageData
137
+ ? {
138
+ ...defaultAuthHeaders({ accessToken, userAgent }),
139
+ 'Content-Type': 'application/octet-stream'
140
+ }
141
+ : defaultAuthHeaders({ accessToken, userAgent })
142
+
143
+ const baseRequestOptions = {
144
+ method: 'POST',
145
+ headers,
146
+ ...(imageData && { body: imageData })
147
+ }
148
+
149
+ const response = await request(requestUrl, mergeRequestOptions(baseRequestOptions, requestOptions))
150
+
151
+ await handleBadResponse(response, { imageUrl, filename })
152
+
153
+ const responseBody = /** @type {YotoUploadCoverImageResponse} */ (await response.body.json())
154
+ return responseBody
155
+ }
@@ -0,0 +1,7 @@
1
+ export function loadTestTokens(): {
2
+ accessToken: string;
3
+ refreshToken: string;
4
+ clientId: string;
5
+ };
6
+ export function logResponse(label: string, response: any): void;
7
+ //# sourceMappingURL=test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["test-helpers.js"],"names":[],"mappings":"AAUA,kCAFa;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAiCzE;AAmBD,mCAPW,MAAM,YACN,GAAG,QASb"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared test helpers for loading tokens and test setup
3
+ */
4
+
5
+ import { join } from 'node:path'
6
+
7
+ /**
8
+ * Load tokens from .env file for testing
9
+ * @returns {{accessToken: string, refreshToken: string, clientId: string}}
10
+ */
11
+ export function loadTestTokens () {
12
+ const testDir = import.meta.dirname
13
+ const envPath = join(testDir, '../..', '.env')
14
+
15
+ // Load .env file from project root
16
+ try {
17
+ process.loadEnvFile(envPath)
18
+ } catch (err) {
19
+ console.error(`Failed to load .env file from ${envPath}`)
20
+ console.error('Tests require YOTO_ACCESS_TOKEN and YOTO_REFRESH_TOKEN in .env')
21
+ console.error('Run `yoto-auth` to authenticate first')
22
+ process.exit(1)
23
+ }
24
+
25
+ const accessToken = process.env['YOTO_ACCESS_TOKEN']
26
+ const refreshToken = process.env['YOTO_REFRESH_TOKEN']
27
+ const clientId = process.env['YOTO_CLIENT_ID']
28
+
29
+ if (!accessToken || !refreshToken) {
30
+ console.error('Missing required environment variables:')
31
+ console.error(' YOTO_ACCESS_TOKEN:', accessToken ? '✓' : '✗')
32
+ console.error(' YOTO_REFRESH_TOKEN:', refreshToken ? '✓' : '✗')
33
+ console.error('\nRun `yoto-auth` to authenticate first')
34
+ process.exit(1)
35
+ }
36
+
37
+ return {
38
+ accessToken,
39
+ refreshToken,
40
+ clientId: clientId || ''
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Log API response for type verification and documentation.
46
+ *
47
+ * This is a first-class testing feature, not temporary debug code.
48
+ * It helps with:
49
+ * - Writing new tests by showing actual API response structure
50
+ * - Verifying type definitions match reality
51
+ * - Debugging test failures
52
+ * - Documenting API behavior
53
+ *
54
+ * @param {string} label - Descriptive label for the response (e.g., 'GET DEVICES')
55
+ * @param {any} response - The API response object to log
56
+ *
57
+ * @example
58
+ * const devices = await getDevices({ token })
59
+ * logResponse('GET DEVICES', devices)
60
+ */
61
+ export function logResponse (label, response) {
62
+ console.log(`\n${label}:`)
63
+ console.dir(response, { depth: null, colors: true })
64
+ }