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.
- package/LICENSE +21 -0
- package/README.md +736 -0
- package/bin/auth.d.ts +3 -0
- package/bin/auth.d.ts.map +1 -0
- package/bin/auth.js +130 -0
- package/bin/content.d.ts +3 -0
- package/bin/content.d.ts.map +1 -0
- package/bin/content.js +117 -0
- package/bin/devices.d.ts +3 -0
- package/bin/devices.d.ts.map +1 -0
- package/bin/devices.js +239 -0
- package/bin/groups.d.ts +3 -0
- package/bin/groups.d.ts.map +1 -0
- package/bin/groups.js +80 -0
- package/bin/icons.d.ts +3 -0
- package/bin/icons.d.ts.map +1 -0
- package/bin/icons.js +100 -0
- package/bin/lib/cli-helpers.d.ts +21 -0
- package/bin/lib/cli-helpers.d.ts.map +1 -0
- package/bin/lib/cli-helpers.js +140 -0
- package/bin/lib/token-helpers.d.ts +14 -0
- package/bin/lib/token-helpers.d.ts.map +1 -0
- package/bin/lib/token-helpers.js +151 -0
- package/bin/refresh-token.d.ts +3 -0
- package/bin/refresh-token.d.ts.map +1 -0
- package/bin/refresh-token.js +168 -0
- package/bin/token-info.d.ts +3 -0
- package/bin/token-info.d.ts.map +1 -0
- package/bin/token-info.js +351 -0
- package/index.d.ts +218 -0
- package/index.d.ts.map +1 -0
- package/index.js +689 -0
- package/lib/api-endpoints/auth.d.ts +56 -0
- package/lib/api-endpoints/auth.d.ts.map +1 -0
- package/lib/api-endpoints/auth.js +209 -0
- package/lib/api-endpoints/auth.test.js +27 -0
- package/lib/api-endpoints/constants.d.ts +6 -0
- package/lib/api-endpoints/constants.d.ts.map +1 -0
- package/lib/api-endpoints/constants.js +31 -0
- package/lib/api-endpoints/content.d.ts +275 -0
- package/lib/api-endpoints/content.d.ts.map +1 -0
- package/lib/api-endpoints/content.js +518 -0
- package/lib/api-endpoints/content.test.js +250 -0
- package/lib/api-endpoints/devices.d.ts +202 -0
- package/lib/api-endpoints/devices.d.ts.map +1 -0
- package/lib/api-endpoints/devices.js +404 -0
- package/lib/api-endpoints/devices.test.js +483 -0
- package/lib/api-endpoints/family-library-groups.d.ts +75 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.js +247 -0
- package/lib/api-endpoints/family-library-groups.test.js +272 -0
- package/lib/api-endpoints/family.d.ts +39 -0
- package/lib/api-endpoints/family.d.ts.map +1 -0
- package/lib/api-endpoints/family.js +166 -0
- package/lib/api-endpoints/family.test.js +184 -0
- package/lib/api-endpoints/helpers.d.ts +29 -0
- package/lib/api-endpoints/helpers.d.ts.map +1 -0
- package/lib/api-endpoints/helpers.js +104 -0
- package/lib/api-endpoints/icons.d.ts +62 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -0
- package/lib/api-endpoints/icons.js +201 -0
- package/lib/api-endpoints/icons.test.js +118 -0
- package/lib/api-endpoints/media.d.ts +37 -0
- package/lib/api-endpoints/media.d.ts.map +1 -0
- package/lib/api-endpoints/media.js +155 -0
- package/lib/api-endpoints/test-helpers.d.ts +7 -0
- package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/test-helpers.js +64 -0
- package/lib/mqtt/client.d.ts +124 -0
- package/lib/mqtt/client.d.ts.map +1 -0
- package/lib/mqtt/client.js +558 -0
- package/lib/mqtt/commands.d.ts +69 -0
- package/lib/mqtt/commands.d.ts.map +1 -0
- package/lib/mqtt/commands.js +238 -0
- package/lib/mqtt/factory.d.ts +12 -0
- package/lib/mqtt/factory.d.ts.map +1 -0
- package/lib/mqtt/factory.js +107 -0
- package/lib/mqtt/index.d.ts +5 -0
- package/lib/mqtt/index.d.ts.map +1 -0
- package/lib/mqtt/index.js +81 -0
- package/lib/mqtt/mqtt.test.js +168 -0
- package/lib/mqtt/topics.d.ts +34 -0
- package/lib/mqtt/topics.d.ts.map +1 -0
- package/lib/mqtt/topics.js +295 -0
- package/lib/pkg.cjs +3 -0
- package/lib/pkg.d.cts +70 -0
- package/lib/pkg.d.cts.map +1 -0
- package/lib/token.d.ts +29 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +240 -0
- package/package.json +91 -0
- 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 @@
|
|
|
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
|
+
}
|