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,56 @@
|
|
|
1
|
+
export function getAuthorizeUrl({ audience, scope, responseType, clientId, redirectUri, state, nonce, prompt, maxAge, codeChallenge, codeChallengeMethod }: {
|
|
2
|
+
audience?: string | undefined;
|
|
3
|
+
scope?: string | undefined;
|
|
4
|
+
responseType: "code" | "token" | "id_token" | "code token" | "code id_token" | "token id_token" | "code token id_token";
|
|
5
|
+
clientId: string;
|
|
6
|
+
redirectUri: string;
|
|
7
|
+
state: string;
|
|
8
|
+
nonce?: string | undefined;
|
|
9
|
+
prompt?: "none" | "login" | "consent" | "select_account" | undefined;
|
|
10
|
+
maxAge?: number | undefined;
|
|
11
|
+
codeChallenge?: string | undefined;
|
|
12
|
+
codeChallengeMethod?: "S256" | "plain" | undefined;
|
|
13
|
+
}): string;
|
|
14
|
+
export function exchangeToken({ grantType, code, redirectUri, refreshToken, clientId, clientSecret, scope, codeVerifier, deviceCode, audience, userAgent, requestOptions }: {
|
|
15
|
+
grantType: "authorization_code" | "refresh_token" | "client_credentials" | "urn:ietf:params:oauth:grant-type:device_code";
|
|
16
|
+
code?: string | undefined;
|
|
17
|
+
redirectUri?: string | undefined;
|
|
18
|
+
refreshToken?: string | undefined;
|
|
19
|
+
clientId?: string | undefined;
|
|
20
|
+
clientSecret?: string | undefined;
|
|
21
|
+
scope?: string | undefined;
|
|
22
|
+
codeVerifier?: string | undefined;
|
|
23
|
+
deviceCode?: string | undefined;
|
|
24
|
+
audience?: string | undefined;
|
|
25
|
+
userAgent?: string | undefined;
|
|
26
|
+
requestOptions?: ({
|
|
27
|
+
dispatcher?: import("undici").Dispatcher;
|
|
28
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
29
|
+
}): Promise<YotoTokenResponse>;
|
|
30
|
+
export function requestDeviceCode({ clientId, scope, audience, userAgent, requestOptions }: {
|
|
31
|
+
clientId: string;
|
|
32
|
+
scope?: string | undefined;
|
|
33
|
+
audience?: string | undefined;
|
|
34
|
+
userAgent?: string | undefined;
|
|
35
|
+
requestOptions?: ({
|
|
36
|
+
dispatcher?: import("undici").Dispatcher;
|
|
37
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
38
|
+
}): Promise<YotoDeviceCodeResponse>;
|
|
39
|
+
export type YotoTokenResponse = {
|
|
40
|
+
access_token: string;
|
|
41
|
+
token_type: string;
|
|
42
|
+
expires_in: number;
|
|
43
|
+
refresh_token?: string;
|
|
44
|
+
scope?: string;
|
|
45
|
+
id_token?: string;
|
|
46
|
+
expires_at?: number;
|
|
47
|
+
};
|
|
48
|
+
export type YotoDeviceCodeResponse = {
|
|
49
|
+
device_code: string;
|
|
50
|
+
user_code: string;
|
|
51
|
+
verification_uri: string;
|
|
52
|
+
verification_uri_complete?: string;
|
|
53
|
+
expires_in: number;
|
|
54
|
+
interval: number;
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["auth.js"],"names":[],"mappings":"AA4BA,4JAbG;IAA0B,QAAQ;IACR,KAAK;IAC6F,YAAY,EAA/H,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,gBAAgB,GAAG,qBAAqB;IACzF,QAAQ,EAAxB,MAAM;IACU,WAAW,EAA3B,MAAM;IACU,KAAK,EAArB,MAAM;IACW,KAAK;IACoC,MAAM;IAC/C,MAAM;IACN,aAAa;IACH,mBAAmB;CACvD,GAAS,MAAM,CA+BjB;AAgCD,4KAdG;IAAiI,SAAS,EAAjI,oBAAoB,GAAG,eAAe,GAAG,oBAAoB,GAAG,8CAA8C;IAC7F,IAAI;IACJ,WAAW;IACX,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,KAAK;IACL,YAAY;IACZ,UAAU;IACV,QAAQ;IACR,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,iBAAiB,CAAC,CAoErC;AAwBD,4FAPG;IAAyB,QAAQ,EAAxB,MAAM;IACW,KAAK;IACL,QAAQ;IACR,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,sBAAsB,CAAC,CA+B1C;;kBAlJa,MAAM;gBACN,MAAM;gBACN,MAAM;oBACN,MAAM;YACN,MAAM;eACN,MAAM;iBACN,MAAM;;;iBA4FN,MAAM;eACN,MAAM;sBACN,MAAM;gCACN,MAAM;gBACN,MAAM;cACN,MAAM"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { RequestOptions } from './helpers.js'
|
|
3
|
+
*/
|
|
4
|
+
import { request } from 'undici'
|
|
5
|
+
import { defaultHeaders, handleBadResponse, mergeRequestOptions, YotoAPIError } from './helpers.js'
|
|
6
|
+
import { DEFAULT_SCOPE, DEFAULT_AUDIENCE, YOTO_LOGIN_URL } from './constants.js'
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Authentication: Authentication endpoints for browser-based and device flows
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Redirects the user to Yoto's login page to begin the OAuth2 Authorization Code flow.
|
|
14
|
+
* @see https://yoto.dev/api/get-authorize/
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] Audience for the token
|
|
17
|
+
* @param {string} [options.scope='openid profile offline_access'] Requested scopes
|
|
18
|
+
* @param {'code' | 'token' | 'id_token' | 'code token' | 'code id_token' | 'token id_token' | 'code token id_token'} options.responseType Required response type
|
|
19
|
+
* @param {string} options.clientId Required client ID
|
|
20
|
+
* @param {string} options.redirectUri Required redirect URI
|
|
21
|
+
* @param {string} options.state Required opaque value for preventing CSRF attacks
|
|
22
|
+
* @param {string} [options.nonce] String value to prevent replay attacks
|
|
23
|
+
* @param {'none' | 'login' | 'consent' | 'select_account'} [options.prompt] Authorization server prompt behavior
|
|
24
|
+
* @param {number} [options.maxAge] Maximum authentication age in seconds
|
|
25
|
+
* @param {string} [options.codeChallenge] PKCE code challenge
|
|
26
|
+
* @param {'S256' | 'plain'} [options.codeChallengeMethod] PKCE code challenge method
|
|
27
|
+
* @return {string} The authorization URL to redirect the user to
|
|
28
|
+
*/
|
|
29
|
+
export function getAuthorizeUrl ({
|
|
30
|
+
audience = DEFAULT_AUDIENCE,
|
|
31
|
+
scope = DEFAULT_SCOPE,
|
|
32
|
+
responseType,
|
|
33
|
+
clientId,
|
|
34
|
+
redirectUri,
|
|
35
|
+
state,
|
|
36
|
+
nonce,
|
|
37
|
+
prompt,
|
|
38
|
+
maxAge,
|
|
39
|
+
codeChallenge,
|
|
40
|
+
codeChallengeMethod
|
|
41
|
+
}) {
|
|
42
|
+
const requestUrl = new URL('/authorize', YOTO_LOGIN_URL)
|
|
43
|
+
|
|
44
|
+
requestUrl.searchParams.set('audience', audience)
|
|
45
|
+
requestUrl.searchParams.set('scope', scope)
|
|
46
|
+
requestUrl.searchParams.set('response_type', responseType)
|
|
47
|
+
requestUrl.searchParams.set('client_id', clientId)
|
|
48
|
+
requestUrl.searchParams.set('redirect_uri', redirectUri)
|
|
49
|
+
requestUrl.searchParams.set('state', state)
|
|
50
|
+
|
|
51
|
+
if (nonce) requestUrl.searchParams.set('nonce', nonce)
|
|
52
|
+
if (prompt) requestUrl.searchParams.set('prompt', prompt)
|
|
53
|
+
if (maxAge !== undefined) requestUrl.searchParams.set('max_age', maxAge.toString())
|
|
54
|
+
if (codeChallenge) requestUrl.searchParams.set('code_challenge', codeChallenge)
|
|
55
|
+
if (codeChallengeMethod) requestUrl.searchParams.set('code_challenge_method', codeChallengeMethod)
|
|
56
|
+
|
|
57
|
+
return requestUrl.toString()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
62
|
+
* @typedef {Object} YotoTokenResponse
|
|
63
|
+
* @property {string} access_token
|
|
64
|
+
* @property {string} token_type
|
|
65
|
+
* @property {number} expires_in
|
|
66
|
+
* @property {string} [refresh_token]
|
|
67
|
+
* @property {string} [scope]
|
|
68
|
+
* @property {string} [id_token]
|
|
69
|
+
* @property {number} [expires_at]
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Exchange authorization code or refresh token for access tokens.
|
|
74
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
75
|
+
* @param {object} options
|
|
76
|
+
* @param {'authorization_code' | 'refresh_token' | 'client_credentials' | 'urn:ietf:params:oauth:grant-type:device_code'} options.grantType Required grant type
|
|
77
|
+
* @param {string} [options.code] Authorization code (required for authorization_code grant)
|
|
78
|
+
* @param {string} [options.redirectUri] Redirect URI (required for authorization_code grant if used in authorize request)
|
|
79
|
+
* @param {string} [options.refreshToken] Refresh token (required for refresh_token grant)
|
|
80
|
+
* @param {string} [options.clientId] Client ID
|
|
81
|
+
* @param {string} [options.clientSecret] Client secret
|
|
82
|
+
* @param {string} [options.scope] Requested scope
|
|
83
|
+
* @param {string} [options.codeVerifier] PKCE code verifier (if code_challenge was used)
|
|
84
|
+
* @param {string} [options.deviceCode] Device code (required for device_code grant)
|
|
85
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] Audience for the token
|
|
86
|
+
* @param {string} [options.userAgent] Optional user agent string
|
|
87
|
+
* @param {RequestOptions} [options.requestOptions] Additional undici request options
|
|
88
|
+
* @return {Promise<YotoTokenResponse>} Token response
|
|
89
|
+
*/
|
|
90
|
+
export async function exchangeToken ({
|
|
91
|
+
grantType,
|
|
92
|
+
code,
|
|
93
|
+
redirectUri,
|
|
94
|
+
refreshToken,
|
|
95
|
+
clientId,
|
|
96
|
+
clientSecret,
|
|
97
|
+
scope,
|
|
98
|
+
codeVerifier,
|
|
99
|
+
deviceCode,
|
|
100
|
+
audience = DEFAULT_AUDIENCE,
|
|
101
|
+
userAgent,
|
|
102
|
+
requestOptions
|
|
103
|
+
}) {
|
|
104
|
+
const requestUrl = new URL('/oauth/token', YOTO_LOGIN_URL)
|
|
105
|
+
|
|
106
|
+
const formData = new URLSearchParams()
|
|
107
|
+
formData.set('grant_type', grantType)
|
|
108
|
+
|
|
109
|
+
if (grantType === 'authorization_code') {
|
|
110
|
+
if (!code) throw new Error('code is required for authorization_code grant')
|
|
111
|
+
formData.set('code', code)
|
|
112
|
+
if (redirectUri) formData.set('redirect_uri', redirectUri)
|
|
113
|
+
if (codeVerifier) formData.set('code_verifier', codeVerifier)
|
|
114
|
+
} else if (grantType === 'refresh_token') {
|
|
115
|
+
if (!refreshToken) throw new Error('refreshToken is required for refresh_token grant')
|
|
116
|
+
formData.set('refresh_token', refreshToken)
|
|
117
|
+
} else if (grantType === 'urn:ietf:params:oauth:grant-type:device_code') {
|
|
118
|
+
if (!deviceCode) throw new Error('deviceCode is required for device_code grant')
|
|
119
|
+
formData.set('device_code', deviceCode)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (clientId) formData.set('client_id', clientId)
|
|
123
|
+
if (clientSecret) formData.set('client_secret', clientSecret)
|
|
124
|
+
if (scope) formData.set('scope', scope)
|
|
125
|
+
if (audience) formData.set('audience', audience)
|
|
126
|
+
|
|
127
|
+
const headers = {
|
|
128
|
+
...defaultHeaders({ userAgent }),
|
|
129
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const response = await request(requestUrl, mergeRequestOptions({
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers,
|
|
135
|
+
body: formData.toString()
|
|
136
|
+
}, requestOptions))
|
|
137
|
+
|
|
138
|
+
// For device_code grant, always parse JSON first so error details are available
|
|
139
|
+
// 403 errors with authorization_pending/slow_down are expected during polling
|
|
140
|
+
if (grantType === 'urn:ietf:params:oauth:grant-type:device_code') {
|
|
141
|
+
/** @type {any} */
|
|
142
|
+
const rawResponse = await response.body.json()
|
|
143
|
+
|
|
144
|
+
if (response.statusCode > 299) {
|
|
145
|
+
throw new YotoAPIError(response, rawResponse, { grantType })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const responseBody = /** @type {YotoTokenResponse} */ (rawResponse)
|
|
149
|
+
return responseBody
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await handleBadResponse(response, { grantType })
|
|
153
|
+
|
|
154
|
+
const responseBody = /** @type {YotoTokenResponse} */ (await response.body.json())
|
|
155
|
+
return responseBody
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @see https://yoto.dev/api/post-oauth-device-code/
|
|
160
|
+
* @typedef {Object} YotoDeviceCodeResponse
|
|
161
|
+
* @property {string} device_code - The device verification code
|
|
162
|
+
* @property {string} user_code - The code displayed to the user
|
|
163
|
+
* @property {string} verification_uri - The URL where the user should enter the user_code
|
|
164
|
+
* @property {string} [verification_uri_complete] - The verification URL with the code included
|
|
165
|
+
* @property {number} expires_in - The lifetime of the device code in seconds
|
|
166
|
+
* @property {number} interval - Minimum polling interval in seconds
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Start the OAuth2 Device Authorization flow for CLI/server-side applications.
|
|
171
|
+
* @see https://yoto.dev/api/post-oauth-device-code/
|
|
172
|
+
* @param {object} options
|
|
173
|
+
* @param {string} options.clientId Required client ID
|
|
174
|
+
* @param {string} [options.scope='openid profile offline_access'] Requested scopes
|
|
175
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] Audience for the token
|
|
176
|
+
* @param {string} [options.userAgent] Optional user agent string
|
|
177
|
+
* @param {RequestOptions} [options.requestOptions] Additional undici request options
|
|
178
|
+
* @return {Promise<YotoDeviceCodeResponse>} Device code response with user_code and verification_uri
|
|
179
|
+
*/
|
|
180
|
+
export async function requestDeviceCode ({
|
|
181
|
+
clientId,
|
|
182
|
+
scope = DEFAULT_SCOPE,
|
|
183
|
+
audience = DEFAULT_AUDIENCE,
|
|
184
|
+
userAgent,
|
|
185
|
+
requestOptions
|
|
186
|
+
}) {
|
|
187
|
+
const requestUrl = new URL('/oauth/device/code', YOTO_LOGIN_URL)
|
|
188
|
+
|
|
189
|
+
const formData = new URLSearchParams()
|
|
190
|
+
formData.set('client_id', clientId)
|
|
191
|
+
formData.set('scope', scope)
|
|
192
|
+
formData.set('audience', audience)
|
|
193
|
+
|
|
194
|
+
const headers = {
|
|
195
|
+
...defaultHeaders({ userAgent }),
|
|
196
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const response = await request(requestUrl, mergeRequestOptions({
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers,
|
|
202
|
+
body: formData.toString()
|
|
203
|
+
}, requestOptions))
|
|
204
|
+
|
|
205
|
+
await handleBadResponse(response, { clientId })
|
|
206
|
+
|
|
207
|
+
const responseBody = /** @type {YotoDeviceCodeResponse} */ (await response.body.json())
|
|
208
|
+
return responseBody
|
|
209
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { exchangeToken } from './auth.js'
|
|
4
|
+
import { YotoAPIError } from './helpers.js'
|
|
5
|
+
import { loadTestTokens } from './test-helpers.js'
|
|
6
|
+
|
|
7
|
+
const { clientId } = loadTestTokens()
|
|
8
|
+
|
|
9
|
+
test('exchangeToken - refresh flow', async (t) => {
|
|
10
|
+
await t.test('should fail with invalid refresh token', async () => {
|
|
11
|
+
await assert.rejects(
|
|
12
|
+
async () => {
|
|
13
|
+
await exchangeToken({
|
|
14
|
+
grantType: 'refresh_token',
|
|
15
|
+
refreshToken: 'invalid-refresh-token',
|
|
16
|
+
clientId
|
|
17
|
+
})
|
|
18
|
+
},
|
|
19
|
+
(err) => {
|
|
20
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
21
|
+
assert.ok(err.statusCode, 'Error should have statusCode')
|
|
22
|
+
assert.ok(err.body, 'Error should have body')
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const DEFAULT_CLIENT_ID: "ix91Qy0B4uA8187JhI0tQbQQ5I5nUKYh";
|
|
2
|
+
export const DEFAULT_AUDIENCE: "https://api.yotoplay.com";
|
|
3
|
+
export const DEFAULT_SCOPE: "openid profile offline_access";
|
|
4
|
+
export const YOTO_API_URL: "https://api.yotoplay.com";
|
|
5
|
+
export const YOTO_LOGIN_URL: "https://login.yotoplay.com";
|
|
6
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["constants.js"],"names":[],"mappings":"AAKA,gCAAiC,kCAAkC,CAAA;AAMnE,+BAAgC,0BAA0B,CAAA;AAO1D,4BAA6B,+BAA+B,CAAA;AAM5D,2BAA4B,0BAA0B,CAAA;AAMtD,6BAA8B,4BAA4B,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default OAuth client ID for testing and development
|
|
3
|
+
* This is a public client ID provided by Yoto for development purposes
|
|
4
|
+
* @constant {string}
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_CLIENT_ID = 'ix91Qy0B4uA8187JhI0tQbQQ5I5nUKYh'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default OAuth audience for Yoto API
|
|
10
|
+
* @constant {string}
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_AUDIENCE = 'https://api.yotoplay.com'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default OAuth scope for Yoto API
|
|
16
|
+
* Includes OpenID Connect, profile information, and offline access (refresh tokens)
|
|
17
|
+
* @constant {string}
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_SCOPE = 'openid profile offline_access'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Yoto API base URL
|
|
23
|
+
* @constant {string}
|
|
24
|
+
*/
|
|
25
|
+
export const YOTO_API_URL = 'https://api.yotoplay.com'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Yoto login/authentication base URL
|
|
29
|
+
* @constant {string}
|
|
30
|
+
*/
|
|
31
|
+
export const YOTO_LOGIN_URL = 'https://login.yotoplay.com'
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
export function getContent({ accessToken, userAgent, requestOptions, cardId, timezone, signingType, playable }: {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
cardId: string;
|
|
4
|
+
timezone?: string | undefined;
|
|
5
|
+
signingType?: string | undefined;
|
|
6
|
+
playable?: boolean | undefined;
|
|
7
|
+
userAgent?: string | undefined;
|
|
8
|
+
requestOptions?: ({
|
|
9
|
+
dispatcher?: import("undici").Dispatcher;
|
|
10
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
11
|
+
}): Promise<YotoContentResponse>;
|
|
12
|
+
export function getUserMyoContent({ accessToken, userAgent, requestOptions, showDeleted }: {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
showDeleted?: boolean | undefined;
|
|
15
|
+
userAgent?: string | undefined;
|
|
16
|
+
requestOptions?: ({
|
|
17
|
+
dispatcher?: import("undici").Dispatcher;
|
|
18
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
19
|
+
}): Promise<YotoMyoContentResponse>;
|
|
20
|
+
export function createOrUpdateContent({ accessToken, userAgent, requestOptions, content }: {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
content: YotoCreateOrUpdateContentRequest;
|
|
23
|
+
userAgent?: string | undefined;
|
|
24
|
+
requestOptions?: ({
|
|
25
|
+
dispatcher?: import("undici").Dispatcher;
|
|
26
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
27
|
+
}): Promise<YotoCreateOrUpdateContentResponse>;
|
|
28
|
+
export function deleteContent({ accessToken, userAgent, requestOptions, cardId }: {
|
|
29
|
+
accessToken: string;
|
|
30
|
+
cardId: string;
|
|
31
|
+
userAgent?: string | undefined;
|
|
32
|
+
requestOptions?: ({
|
|
33
|
+
dispatcher?: import("undici").Dispatcher;
|
|
34
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
35
|
+
}): Promise<YotoDeleteContentResponse>;
|
|
36
|
+
export type YotoCategory = "none" | "stories" | "music" | "radio" | "podcast" | "sfx" | "activities" | "alarms";
|
|
37
|
+
export type YotoAudioFormat = "mp3" | "aac" | "opus" | "ogg";
|
|
38
|
+
export type YotoLanguage = "en" | "en-gb" | "en-us" | "fr" | "fr-fr" | "es" | "es-es" | "es-419" | "de" | "it";
|
|
39
|
+
export type YotoStatusName = "new" | "inprogress" | "complete" | "live" | "archived";
|
|
40
|
+
export type YotoPlaybackDirection = "DESC" | "ASC";
|
|
41
|
+
export type YotoPlaybackType = "linear" | "interactive";
|
|
42
|
+
export type YotoTrackDisplay = {
|
|
43
|
+
icon16x16: string;
|
|
44
|
+
};
|
|
45
|
+
export type YotoTrackType = "audio" | "stream";
|
|
46
|
+
export type YotoChannels = "stereo" | "mono";
|
|
47
|
+
export type YotoTrack = {
|
|
48
|
+
key: string;
|
|
49
|
+
title: string;
|
|
50
|
+
trackUrl: string;
|
|
51
|
+
format: YotoAudioFormat;
|
|
52
|
+
type: YotoTrackType;
|
|
53
|
+
overlayLabel: string;
|
|
54
|
+
duration: number;
|
|
55
|
+
fileSize: number;
|
|
56
|
+
channels: YotoChannels;
|
|
57
|
+
ambient: any | null;
|
|
58
|
+
display: YotoTrackDisplay;
|
|
59
|
+
uid?: string | null;
|
|
60
|
+
overlayLabelOverride?: string | null;
|
|
61
|
+
};
|
|
62
|
+
export type YotoChapterDisplay = {
|
|
63
|
+
icon16x16: string;
|
|
64
|
+
};
|
|
65
|
+
export type YotoChapter = {
|
|
66
|
+
key: string;
|
|
67
|
+
title: string;
|
|
68
|
+
tracks: YotoTrack[];
|
|
69
|
+
display: YotoChapterDisplay;
|
|
70
|
+
overlayLabel: string;
|
|
71
|
+
duration: number;
|
|
72
|
+
fileSize: number;
|
|
73
|
+
availableFrom: any | null;
|
|
74
|
+
ambient: any | null;
|
|
75
|
+
defaultTrackDisplay: string | null;
|
|
76
|
+
defaultTrackAmbient: string | null;
|
|
77
|
+
overlayLabelOverride?: string | null;
|
|
78
|
+
startTime?: number;
|
|
79
|
+
};
|
|
80
|
+
export type YotoEditSettings = {
|
|
81
|
+
autoOverlayLabels: string;
|
|
82
|
+
editKeys: boolean;
|
|
83
|
+
transcodeAudioUploads: boolean;
|
|
84
|
+
};
|
|
85
|
+
export type YotoMetadata = {
|
|
86
|
+
category: YotoCategory;
|
|
87
|
+
cover: {
|
|
88
|
+
imageL: string | null;
|
|
89
|
+
};
|
|
90
|
+
media: YotoMedia;
|
|
91
|
+
accent?: string;
|
|
92
|
+
addToFamilyLibrary?: boolean;
|
|
93
|
+
author?: string;
|
|
94
|
+
copyright?: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
genre?: string[];
|
|
97
|
+
languages?: YotoLanguage[];
|
|
98
|
+
maxAge?: number;
|
|
99
|
+
minAge?: number;
|
|
100
|
+
musicType?: string[];
|
|
101
|
+
note?: string;
|
|
102
|
+
order?: string;
|
|
103
|
+
audioPreviewUrl?: string;
|
|
104
|
+
readBy?: string;
|
|
105
|
+
share?: boolean;
|
|
106
|
+
status?: YotoStatus;
|
|
107
|
+
tags?: string[];
|
|
108
|
+
feedUrl?: string;
|
|
109
|
+
numEpisodes?: number;
|
|
110
|
+
playbackDirection?: YotoPlaybackDirection;
|
|
111
|
+
};
|
|
112
|
+
export type YotoMedia = {
|
|
113
|
+
duration: number;
|
|
114
|
+
fileSize: number;
|
|
115
|
+
hasStreams: boolean;
|
|
116
|
+
};
|
|
117
|
+
export type YotoContentConfig = {
|
|
118
|
+
autoadvance?: string;
|
|
119
|
+
onlineOnly?: boolean;
|
|
120
|
+
shuffle?: YotoShuffle[];
|
|
121
|
+
trackNumberOverlayTimeout?: number;
|
|
122
|
+
resumeTimeout?: number;
|
|
123
|
+
systemActivity?: boolean;
|
|
124
|
+
};
|
|
125
|
+
export type YotoShuffle = {
|
|
126
|
+
end: number;
|
|
127
|
+
limit: number;
|
|
128
|
+
start: number;
|
|
129
|
+
};
|
|
130
|
+
export type YotoContentCover = {
|
|
131
|
+
imageL: string | null;
|
|
132
|
+
};
|
|
133
|
+
export type YotoMyoEditSettings = {
|
|
134
|
+
autoOverlayLabels: string;
|
|
135
|
+
editKeys: boolean;
|
|
136
|
+
podcastTrackDisplay?: YotoPodcastTrackDisplay;
|
|
137
|
+
podcastType?: string;
|
|
138
|
+
};
|
|
139
|
+
export type YotoPodcastTrackDisplay = {
|
|
140
|
+
icon16x16: string;
|
|
141
|
+
};
|
|
142
|
+
export type YotoStatus = {
|
|
143
|
+
name: YotoStatusName;
|
|
144
|
+
updatedAt: string;
|
|
145
|
+
};
|
|
146
|
+
export type YotoSharing = {
|
|
147
|
+
linkCreatedAt: string;
|
|
148
|
+
linkUrl: string;
|
|
149
|
+
shareCount: number;
|
|
150
|
+
shareLimit: number;
|
|
151
|
+
};
|
|
152
|
+
export type YotoClubAvailability = {
|
|
153
|
+
store: string;
|
|
154
|
+
};
|
|
155
|
+
export type YotoContentResponse = {
|
|
156
|
+
card: YotoCard;
|
|
157
|
+
};
|
|
158
|
+
export type YotoCard = {
|
|
159
|
+
cardId: string;
|
|
160
|
+
content: YotoContent;
|
|
161
|
+
createdAt: string;
|
|
162
|
+
creatorEmail: string;
|
|
163
|
+
deleted: boolean;
|
|
164
|
+
metadata: YotoMetadata;
|
|
165
|
+
shareLimit: number;
|
|
166
|
+
shareLinkCreatedAt: string;
|
|
167
|
+
slug: string;
|
|
168
|
+
title: string;
|
|
169
|
+
updatedAt: string;
|
|
170
|
+
userId: string;
|
|
171
|
+
tags?: string[];
|
|
172
|
+
};
|
|
173
|
+
export type YotoContent = {
|
|
174
|
+
activity: string;
|
|
175
|
+
chapters: YotoChapter[];
|
|
176
|
+
config: YotoContentConfig;
|
|
177
|
+
editSettings: YotoEditSettings;
|
|
178
|
+
version: string;
|
|
179
|
+
playbackType?: YotoPlaybackType;
|
|
180
|
+
};
|
|
181
|
+
export type YotoMyoContentResponse = {
|
|
182
|
+
cards: YotoMyoCard[];
|
|
183
|
+
};
|
|
184
|
+
export type YotoMyoCard = {
|
|
185
|
+
availability: string;
|
|
186
|
+
cardId: string;
|
|
187
|
+
clubAvailability?: YotoClubAvailability[];
|
|
188
|
+
content: YotoMyoContent;
|
|
189
|
+
createdAt: string;
|
|
190
|
+
deleted: boolean;
|
|
191
|
+
metadata: YotoMyoMetadata;
|
|
192
|
+
shareLinkCreatedAt?: string;
|
|
193
|
+
shareLinkUrl?: string;
|
|
194
|
+
sharing?: YotoSharing;
|
|
195
|
+
slug: string;
|
|
196
|
+
sortkey?: string;
|
|
197
|
+
title: string;
|
|
198
|
+
updatedAt: string;
|
|
199
|
+
userId: string;
|
|
200
|
+
};
|
|
201
|
+
export type YotoMyoContent = {
|
|
202
|
+
activity: string;
|
|
203
|
+
config: YotoContentConfig;
|
|
204
|
+
cover?: YotoContentCover;
|
|
205
|
+
editSettings: YotoMyoEditSettings;
|
|
206
|
+
hidden?: boolean;
|
|
207
|
+
restricted?: boolean;
|
|
208
|
+
version: string;
|
|
209
|
+
playbackType?: YotoPlaybackType;
|
|
210
|
+
};
|
|
211
|
+
export type YotoMyoMetadata = {
|
|
212
|
+
category: YotoCategory;
|
|
213
|
+
cover?: YotoContentCover;
|
|
214
|
+
media: YotoMedia;
|
|
215
|
+
accent?: string;
|
|
216
|
+
addToFamilyLibrary?: boolean;
|
|
217
|
+
author?: string;
|
|
218
|
+
copyright?: string;
|
|
219
|
+
description?: string;
|
|
220
|
+
genre?: string[];
|
|
221
|
+
languages?: YotoLanguage[];
|
|
222
|
+
maxAge?: number;
|
|
223
|
+
minAge?: number;
|
|
224
|
+
musicType?: string[];
|
|
225
|
+
note?: string;
|
|
226
|
+
order?: any;
|
|
227
|
+
previewAudio?: string;
|
|
228
|
+
audioPreviewUrl?: string;
|
|
229
|
+
readBy?: string;
|
|
230
|
+
share?: boolean;
|
|
231
|
+
status?: YotoStatus;
|
|
232
|
+
tags?: string[];
|
|
233
|
+
feedUrl?: string | null;
|
|
234
|
+
numEpisodes?: number;
|
|
235
|
+
playbackDirection?: YotoPlaybackDirection;
|
|
236
|
+
hidden?: boolean;
|
|
237
|
+
list?: any[];
|
|
238
|
+
};
|
|
239
|
+
export type YotoCreateOrUpdateContentRequest = {
|
|
240
|
+
cardId?: string;
|
|
241
|
+
title: string;
|
|
242
|
+
content: YotoContentInput;
|
|
243
|
+
metadata?: YotoMetadataInput;
|
|
244
|
+
};
|
|
245
|
+
export type YotoContentInput = {
|
|
246
|
+
chapters?: YotoChapter[];
|
|
247
|
+
config?: YotoContentConfig;
|
|
248
|
+
playbackType?: string;
|
|
249
|
+
};
|
|
250
|
+
export type YotoMetadataInput = {
|
|
251
|
+
description?: string;
|
|
252
|
+
title?: string;
|
|
253
|
+
};
|
|
254
|
+
export type YotoCreateOrUpdateContentResponse = {
|
|
255
|
+
card: YotoCreatedCard;
|
|
256
|
+
};
|
|
257
|
+
export type YotoCreatedCard = {
|
|
258
|
+
_id: string;
|
|
259
|
+
cardId: string;
|
|
260
|
+
content: YotoCreatedContent;
|
|
261
|
+
createdAt: string;
|
|
262
|
+
metadata: Object;
|
|
263
|
+
title: string;
|
|
264
|
+
updatedAt: string;
|
|
265
|
+
userId: string;
|
|
266
|
+
};
|
|
267
|
+
export type YotoCreatedContent = {
|
|
268
|
+
chapters: YotoChapter[];
|
|
269
|
+
config: YotoContentConfig;
|
|
270
|
+
playbackType: string;
|
|
271
|
+
};
|
|
272
|
+
export type YotoDeleteContentResponse = {
|
|
273
|
+
status: string;
|
|
274
|
+
};
|
|
275
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["content.js"],"names":[],"mappings":"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"}
|