yoto-nodejs-client 0.0.1 → 0.0.3
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/README.md +523 -30
- package/bin/auth.js +36 -46
- package/bin/content.js +0 -0
- package/bin/device-model.d.ts +3 -0
- package/bin/device-model.d.ts.map +1 -0
- package/bin/device-model.js +360 -0
- package/bin/device-tui.TODO.md +125 -0
- package/bin/device-tui.d.ts +31 -0
- package/bin/device-tui.d.ts.map +1 -0
- package/bin/device-tui.js +1123 -0
- package/bin/devices.js +166 -28
- package/bin/groups.js +0 -0
- package/bin/icons.js +0 -0
- package/bin/lib/cli-helpers.d.ts +33 -1
- package/bin/lib/cli-helpers.d.ts.map +1 -1
- package/bin/lib/cli-helpers.js +5 -5
- package/bin/lib/token-helpers.d.ts +32 -0
- package/bin/lib/token-helpers.d.ts.map +1 -1
- package/bin/refresh-token.js +6 -6
- package/bin/token-info.js +3 -3
- package/index.d.ts +4 -217
- package/index.d.ts.map +1 -1
- package/index.js +11 -689
- package/lib/api-client.d.ts +576 -0
- package/lib/api-client.d.ts.map +1 -0
- package/lib/api-client.js +681 -0
- package/lib/api-endpoints/auth.d.ts +280 -4
- package/lib/api-endpoints/auth.d.ts.map +1 -1
- package/lib/api-endpoints/auth.js +224 -7
- package/lib/api-endpoints/auth.test.js +54 -2
- package/lib/api-endpoints/constants.d.ts +30 -2
- package/lib/api-endpoints/constants.d.ts.map +1 -1
- package/lib/api-endpoints/constants.js +17 -10
- package/lib/api-endpoints/content.d.ts +760 -0
- package/lib/api-endpoints/content.d.ts.map +1 -1
- package/lib/api-endpoints/content.test.js +1 -1
- package/lib/api-endpoints/devices.d.ts +917 -48
- package/lib/api-endpoints/devices.d.ts.map +1 -1
- package/lib/api-endpoints/devices.js +114 -52
- package/lib/api-endpoints/devices.test.js +1 -1
- package/lib/api-endpoints/endpoint-test-helpers.d.ts +28 -0
- package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.d.ts +187 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -1
- package/lib/api-endpoints/family-library-groups.test.js +1 -1
- package/lib/api-endpoints/family.d.ts +88 -0
- package/lib/api-endpoints/family.d.ts.map +1 -1
- package/lib/api-endpoints/family.test.js +1 -1
- package/lib/api-endpoints/helpers.d.ts +37 -3
- package/lib/api-endpoints/helpers.d.ts.map +1 -1
- package/lib/api-endpoints/icons.d.ts +196 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -1
- package/lib/api-endpoints/icons.test.js +1 -1
- package/lib/api-endpoints/media.d.ts +83 -0
- package/lib/api-endpoints/media.d.ts.map +1 -1
- package/lib/helpers/power-state.d.ts +53 -0
- package/lib/helpers/power-state.d.ts.map +1 -0
- package/lib/helpers/power-state.js +73 -0
- package/lib/helpers/power-state.test.js +100 -0
- package/lib/helpers/temperature.d.ts +24 -0
- package/lib/helpers/temperature.d.ts.map +1 -0
- package/lib/helpers/temperature.js +61 -0
- package/lib/helpers/temperature.test.js +58 -0
- package/lib/helpers/typed-keys.d.ts +7 -0
- package/lib/helpers/typed-keys.d.ts.map +1 -0
- package/lib/helpers/typed-keys.js +8 -0
- package/lib/mqtt/client.d.ts +610 -7
- package/lib/mqtt/client.d.ts.map +1 -1
- package/lib/mqtt/client.js +213 -31
- package/lib/mqtt/commands.d.ts +195 -0
- package/lib/mqtt/commands.d.ts.map +1 -1
- package/lib/mqtt/factory.d.ts +62 -1
- package/lib/mqtt/factory.d.ts.map +1 -1
- package/lib/mqtt/factory.js +27 -5
- package/lib/mqtt/mqtt.test.js +85 -28
- package/lib/mqtt/topics.d.ts +186 -1
- package/lib/mqtt/topics.d.ts.map +1 -1
- package/lib/mqtt/topics.js +54 -20
- package/lib/pkg.d.cts +9 -0
- package/lib/token.d.ts +106 -3
- package/lib/token.d.ts.map +1 -1
- package/lib/token.js +30 -23
- package/lib/yoto-account.d.ts +163 -0
- package/lib/yoto-account.d.ts.map +1 -0
- package/lib/yoto-account.js +340 -0
- package/lib/yoto-device.d.ts +656 -0
- package/lib/yoto-device.d.ts.map +1 -0
- package/lib/yoto-device.js +2850 -0
- package/package.json +22 -15
- package/lib/api-endpoints/test-helpers.d.ts +0 -7
- package/lib/api-endpoints/test-helpers.d.ts.map +0 -1
- /package/lib/api-endpoints/{test-helpers.js → endpoint-test-helpers.js} +0 -0
|
@@ -1,18 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth2 response type for authorization requests
|
|
3
|
+
* @typedef {'code' | 'token' | 'id_token' | 'code token' | 'code id_token' | 'token id_token' | 'code token id_token'} YotoOAuthResponseType
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* OAuth2 prompt parameter for authorization requests
|
|
7
|
+
* @typedef {'none' | 'login' | 'consent' | 'select_account'} YotoOAuthPromptType
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* PKCE code challenge method for authorization requests
|
|
11
|
+
* @typedef {'S256' | 'plain'} YotoOAuthCodeChallengeMethod
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* OAuth2 grant type for token exchange requests
|
|
15
|
+
* @typedef {'authorization_code' | 'refresh_token' | 'client_credentials' | typeof DEVICE_CODE_GRANT_TYPE} YotoOAuthGrantType
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Redirects the user to Yoto's login page to begin the OAuth2 Authorization Code flow.
|
|
19
|
+
* @see https://yoto.dev/api/get-authorize/
|
|
20
|
+
* @param {object} options
|
|
21
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] Audience for the token
|
|
22
|
+
* @param {string} [options.scope='openid profile offline_access'] Requested scopes
|
|
23
|
+
* @param {YotoOAuthResponseType} options.responseType Required response type
|
|
24
|
+
* @param {string} options.clientId Required client ID
|
|
25
|
+
* @param {string} options.redirectUri Required redirect URI
|
|
26
|
+
* @param {string} options.state Required opaque value for preventing CSRF attacks
|
|
27
|
+
* @param {string} [options.nonce] String value to prevent replay attacks
|
|
28
|
+
* @param {YotoOAuthPromptType} [options.prompt] Authorization server prompt behavior
|
|
29
|
+
* @param {number} [options.maxAge] Maximum authentication age in seconds
|
|
30
|
+
* @param {string} [options.codeChallenge] PKCE code challenge
|
|
31
|
+
* @param {YotoOAuthCodeChallengeMethod} [options.codeChallengeMethod] PKCE code challenge method
|
|
32
|
+
* @return {string} The authorization URL to redirect the user to
|
|
33
|
+
*/
|
|
1
34
|
export function getAuthorizeUrl({ audience, scope, responseType, clientId, redirectUri, state, nonce, prompt, maxAge, codeChallenge, codeChallengeMethod }: {
|
|
2
35
|
audience?: string | undefined;
|
|
3
36
|
scope?: string | undefined;
|
|
4
|
-
responseType:
|
|
37
|
+
responseType: YotoOAuthResponseType;
|
|
5
38
|
clientId: string;
|
|
6
39
|
redirectUri: string;
|
|
7
40
|
state: string;
|
|
8
41
|
nonce?: string | undefined;
|
|
9
|
-
prompt?:
|
|
42
|
+
prompt?: YotoOAuthPromptType | undefined;
|
|
10
43
|
maxAge?: number | undefined;
|
|
11
44
|
codeChallenge?: string | undefined;
|
|
12
|
-
codeChallengeMethod?:
|
|
45
|
+
codeChallengeMethod?: YotoOAuthCodeChallengeMethod | undefined;
|
|
13
46
|
}): string;
|
|
47
|
+
/**
|
|
48
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
49
|
+
* @typedef {Object} YotoTokenResponse
|
|
50
|
+
* @property {string} access_token
|
|
51
|
+
* @property {string} token_type
|
|
52
|
+
* @property {number} expires_in
|
|
53
|
+
* @property {string} [refresh_token]
|
|
54
|
+
* @property {string} [scope]
|
|
55
|
+
* @property {string} [id_token]
|
|
56
|
+
* @property {number} [expires_at]
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Exchange authorization code or refresh token for access tokens.
|
|
60
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
61
|
+
* @param {object} options
|
|
62
|
+
* @param {YotoOAuthGrantType} options.grantType Required grant type
|
|
63
|
+
* @param {string} [options.code] Authorization code (required for authorization_code grant)
|
|
64
|
+
* @param {string} [options.redirectUri] Redirect URI (required for authorization_code grant if used in authorize request)
|
|
65
|
+
* @param {string} [options.refreshToken] Refresh token (required for refresh_token grant)
|
|
66
|
+
* @param {string} [options.clientId] Client ID
|
|
67
|
+
* @param {string} [options.clientSecret] Client secret
|
|
68
|
+
* @param {string} [options.scope] Requested scope
|
|
69
|
+
* @param {string} [options.codeVerifier] PKCE code verifier (if code_challenge was used)
|
|
70
|
+
* @param {string} [options.deviceCode] Device code (required for device_code grant)
|
|
71
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] Audience for the token
|
|
72
|
+
* @param {string} [options.userAgent] Optional user agent string
|
|
73
|
+
* @param {RequestOptions} [options.requestOptions] Additional undici request options
|
|
74
|
+
* @return {Promise<YotoTokenResponse>} Token response
|
|
75
|
+
*/
|
|
14
76
|
export function exchangeToken({ grantType, code, redirectUri, refreshToken, clientId, clientSecret, scope, codeVerifier, deviceCode, audience, userAgent, requestOptions }: {
|
|
15
|
-
grantType:
|
|
77
|
+
grantType: YotoOAuthGrantType;
|
|
16
78
|
code?: string | undefined;
|
|
17
79
|
redirectUri?: string | undefined;
|
|
18
80
|
refreshToken?: string | undefined;
|
|
@@ -27,6 +89,27 @@ export function exchangeToken({ grantType, code, redirectUri, refreshToken, clie
|
|
|
27
89
|
dispatcher?: import("undici").Dispatcher;
|
|
28
90
|
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
29
91
|
}): Promise<YotoTokenResponse>;
|
|
92
|
+
/**
|
|
93
|
+
* @see https://yoto.dev/api/post-oauth-device-code/
|
|
94
|
+
* @typedef {Object} YotoDeviceCodeResponse
|
|
95
|
+
* @property {string} device_code - The device verification code
|
|
96
|
+
* @property {string} user_code - The code displayed to the user
|
|
97
|
+
* @property {string} verification_uri - The URL where the user should enter the user_code
|
|
98
|
+
* @property {string} [verification_uri_complete] - The verification URL with the code included
|
|
99
|
+
* @property {number} expires_in - The lifetime of the device code in seconds
|
|
100
|
+
* @property {number} interval - Minimum polling interval in seconds
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* Start the OAuth2 Device Authorization flow for CLI/server-side applications.
|
|
104
|
+
* @see https://yoto.dev/api/post-oauth-device-code/
|
|
105
|
+
* @param {object} options
|
|
106
|
+
* @param {string} options.clientId Required client ID
|
|
107
|
+
* @param {string} [options.scope='openid profile offline_access'] Requested scopes
|
|
108
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] Audience for the token
|
|
109
|
+
* @param {string} [options.userAgent] Optional user agent string
|
|
110
|
+
* @param {RequestOptions} [options.requestOptions] Additional undici request options
|
|
111
|
+
* @return {Promise<YotoDeviceCodeResponse>} Device code response with user_code and verification_uri
|
|
112
|
+
*/
|
|
30
113
|
export function requestDeviceCode({ clientId, scope, audience, userAgent, requestOptions }: {
|
|
31
114
|
clientId: string;
|
|
32
115
|
scope?: string | undefined;
|
|
@@ -36,6 +119,137 @@ export function requestDeviceCode({ clientId, scope, audience, userAgent, reques
|
|
|
36
119
|
dispatcher?: import("undici").Dispatcher;
|
|
37
120
|
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
38
121
|
}): Promise<YotoDeviceCodeResponse>;
|
|
122
|
+
/**
|
|
123
|
+
* Poll result when authorization is still pending
|
|
124
|
+
* @typedef {Object} YotoDevicePollPending
|
|
125
|
+
* @property {'pending'} status - Indicates polling should continue
|
|
126
|
+
* @property {number} interval - Current polling interval in milliseconds
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* Poll result when polling needs to slow down
|
|
130
|
+
* @typedef {Object} YotoDevicePollSlowDown
|
|
131
|
+
* @property {'slow_down'} status - Indicates polling interval should be increased
|
|
132
|
+
* @property {number} interval - New polling interval in milliseconds
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Poll result when authorization is successful
|
|
136
|
+
* @typedef {Object} YotoDevicePollSuccess
|
|
137
|
+
* @property {'success'} status - Indicates successful authorization
|
|
138
|
+
* @property {YotoTokenResponse} tokens - OAuth tokens
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Poll result for device authorization flow
|
|
142
|
+
* @typedef {YotoDevicePollPending | YotoDevicePollSlowDown | YotoDevicePollSuccess} YotoDevicePollResult
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* Poll for device authorization completion with automatic error handling (single poll attempt).
|
|
146
|
+
* This function handles common polling errors (authorization_pending, slow_down)
|
|
147
|
+
* and only throws for unrecoverable errors (expired_token, access_denied, etc).
|
|
148
|
+
*
|
|
149
|
+
* Non-blocking - returns immediately with poll result. Suitable for:
|
|
150
|
+
* - Manual polling loops in CLI applications
|
|
151
|
+
* - Server-side endpoints that poll on behalf of clients (e.g., Homebridge UI server)
|
|
152
|
+
* - Custom UI implementations with specific polling behavior
|
|
153
|
+
*
|
|
154
|
+
* For the simplest approach (automatic polling loop), use waitForDeviceAuthorization() instead.
|
|
155
|
+
*
|
|
156
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
157
|
+
* @param {object} options
|
|
158
|
+
* @param {string} options.deviceCode - Device code from requestDeviceCode()
|
|
159
|
+
* @param {string} options.clientId - OAuth client ID
|
|
160
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] - Audience for the token
|
|
161
|
+
* @param {number} [options.currentInterval=5000] - Current polling interval in milliseconds
|
|
162
|
+
* @param {string} [options.userAgent] - Optional user agent string
|
|
163
|
+
* @param {RequestOptions} [options.requestOptions] - Additional undici request options
|
|
164
|
+
* @returns {Promise<YotoDevicePollResult>} Poll result with status and data
|
|
165
|
+
* @throws {YotoAPIError} For unrecoverable errors (expired_token, access_denied, invalid_grant, etc)
|
|
166
|
+
*
|
|
167
|
+
*/
|
|
168
|
+
export function pollForDeviceToken({ deviceCode, clientId, audience, currentInterval, userAgent, requestOptions }: {
|
|
169
|
+
deviceCode: string;
|
|
170
|
+
clientId: string;
|
|
171
|
+
audience?: string | undefined;
|
|
172
|
+
currentInterval?: number | undefined;
|
|
173
|
+
userAgent?: string | undefined;
|
|
174
|
+
requestOptions?: ({
|
|
175
|
+
dispatcher?: import("undici").Dispatcher;
|
|
176
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
177
|
+
}): Promise<YotoDevicePollResult>;
|
|
178
|
+
/**
|
|
179
|
+
* Wait for device authorization to complete with automatic polling.
|
|
180
|
+
* This function wraps the entire polling loop - just call it and await the result.
|
|
181
|
+
* It handles all polling logic internally including interval adjustments.
|
|
182
|
+
*
|
|
183
|
+
* Designed for CLI usage where you want to block until authorization completes.
|
|
184
|
+
* For UI implementations with progress feedback, use pollForDeviceToken() directly.
|
|
185
|
+
*
|
|
186
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
187
|
+
* @param {object} options
|
|
188
|
+
* @param {string} options.deviceCode - Device code from requestDeviceCode()
|
|
189
|
+
* @param {string} options.clientId - OAuth client ID
|
|
190
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] - Audience for the token
|
|
191
|
+
* @param {number} [options.initialInterval=5000] - Initial polling interval in milliseconds
|
|
192
|
+
* @param {number} [options.expiresIn] - Seconds until device code expires (for timeout calculation)
|
|
193
|
+
* @param {string} [options.userAgent] - Optional user agent string
|
|
194
|
+
* @param {RequestOptions} [options.requestOptions] - Additional undici request options
|
|
195
|
+
* @param {(result: YotoDevicePollResult) => void} [options.onPoll] - Optional callback invoked after each poll attempt
|
|
196
|
+
* @returns {Promise<YotoTokenResponse>} Token response on successful authorization
|
|
197
|
+
* @throws {YotoAPIError} For unrecoverable errors (expired_token, access_denied, invalid_grant, etc)
|
|
198
|
+
* @throws {Error} If device code expires (timeout)
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* // Simple usage - just wait for tokens
|
|
202
|
+
* const deviceAuth = await requestDeviceCode({ clientId })
|
|
203
|
+
* console.log(`Visit: ${deviceAuth.verification_uri_complete}`)
|
|
204
|
+
*
|
|
205
|
+
* const tokens = await waitForDeviceAuthorization({
|
|
206
|
+
* deviceCode: deviceAuth.device_code,
|
|
207
|
+
* clientId,
|
|
208
|
+
* initialInterval: deviceAuth.interval * 1000,
|
|
209
|
+
* expiresIn: deviceAuth.expires_in
|
|
210
|
+
* })
|
|
211
|
+
*
|
|
212
|
+
* console.log('Got tokens:', tokens)
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* // With progress callback
|
|
216
|
+
* const tokens = await waitForDeviceAuthorization({
|
|
217
|
+
* deviceCode: deviceAuth.device_code,
|
|
218
|
+
* clientId,
|
|
219
|
+
* onPoll: (result) => {
|
|
220
|
+
* if (result.status === 'pending') process.stdout.write('.')
|
|
221
|
+
* if (result.status === 'slow_down') console.log('\nSlowing down...')
|
|
222
|
+
* }
|
|
223
|
+
* })
|
|
224
|
+
*/
|
|
225
|
+
export function waitForDeviceAuthorization({ deviceCode, clientId, audience, initialInterval, expiresIn, userAgent, requestOptions, onPoll }: {
|
|
226
|
+
deviceCode: string;
|
|
227
|
+
clientId: string;
|
|
228
|
+
audience?: string | undefined;
|
|
229
|
+
initialInterval?: number | undefined;
|
|
230
|
+
expiresIn?: number | undefined;
|
|
231
|
+
userAgent?: string | undefined;
|
|
232
|
+
requestOptions?: ({
|
|
233
|
+
dispatcher?: import("undici").Dispatcher;
|
|
234
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
235
|
+
onPoll?: ((result: YotoDevicePollResult) => void) | undefined;
|
|
236
|
+
}): Promise<YotoTokenResponse>;
|
|
237
|
+
/**
|
|
238
|
+
* OAuth2 response type for authorization requests
|
|
239
|
+
*/
|
|
240
|
+
export type YotoOAuthResponseType = "code" | "token" | "id_token" | "code token" | "code id_token" | "token id_token" | "code token id_token";
|
|
241
|
+
/**
|
|
242
|
+
* OAuth2 prompt parameter for authorization requests
|
|
243
|
+
*/
|
|
244
|
+
export type YotoOAuthPromptType = "none" | "login" | "consent" | "select_account";
|
|
245
|
+
/**
|
|
246
|
+
* PKCE code challenge method for authorization requests
|
|
247
|
+
*/
|
|
248
|
+
export type YotoOAuthCodeChallengeMethod = "S256" | "plain";
|
|
249
|
+
/**
|
|
250
|
+
* OAuth2 grant type for token exchange requests
|
|
251
|
+
*/
|
|
252
|
+
export type YotoOAuthGrantType = "authorization_code" | "refresh_token" | "client_credentials" | typeof DEVICE_CODE_GRANT_TYPE;
|
|
39
253
|
export type YotoTokenResponse = {
|
|
40
254
|
access_token: string;
|
|
41
255
|
token_type: string;
|
|
@@ -46,11 +260,73 @@ export type YotoTokenResponse = {
|
|
|
46
260
|
expires_at?: number;
|
|
47
261
|
};
|
|
48
262
|
export type YotoDeviceCodeResponse = {
|
|
263
|
+
/**
|
|
264
|
+
* - The device verification code
|
|
265
|
+
*/
|
|
49
266
|
device_code: string;
|
|
267
|
+
/**
|
|
268
|
+
* - The code displayed to the user
|
|
269
|
+
*/
|
|
50
270
|
user_code: string;
|
|
271
|
+
/**
|
|
272
|
+
* - The URL where the user should enter the user_code
|
|
273
|
+
*/
|
|
51
274
|
verification_uri: string;
|
|
275
|
+
/**
|
|
276
|
+
* - The verification URL with the code included
|
|
277
|
+
*/
|
|
52
278
|
verification_uri_complete?: string;
|
|
279
|
+
/**
|
|
280
|
+
* - The lifetime of the device code in seconds
|
|
281
|
+
*/
|
|
53
282
|
expires_in: number;
|
|
283
|
+
/**
|
|
284
|
+
* - Minimum polling interval in seconds
|
|
285
|
+
*/
|
|
54
286
|
interval: number;
|
|
55
287
|
};
|
|
288
|
+
/**
|
|
289
|
+
* Poll result when authorization is still pending
|
|
290
|
+
*/
|
|
291
|
+
export type YotoDevicePollPending = {
|
|
292
|
+
/**
|
|
293
|
+
* - Indicates polling should continue
|
|
294
|
+
*/
|
|
295
|
+
status: "pending";
|
|
296
|
+
/**
|
|
297
|
+
* - Current polling interval in milliseconds
|
|
298
|
+
*/
|
|
299
|
+
interval: number;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Poll result when polling needs to slow down
|
|
303
|
+
*/
|
|
304
|
+
export type YotoDevicePollSlowDown = {
|
|
305
|
+
/**
|
|
306
|
+
* - Indicates polling interval should be increased
|
|
307
|
+
*/
|
|
308
|
+
status: "slow_down";
|
|
309
|
+
/**
|
|
310
|
+
* - New polling interval in milliseconds
|
|
311
|
+
*/
|
|
312
|
+
interval: number;
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Poll result when authorization is successful
|
|
316
|
+
*/
|
|
317
|
+
export type YotoDevicePollSuccess = {
|
|
318
|
+
/**
|
|
319
|
+
* - Indicates successful authorization
|
|
320
|
+
*/
|
|
321
|
+
status: "success";
|
|
322
|
+
/**
|
|
323
|
+
* - OAuth tokens
|
|
324
|
+
*/
|
|
325
|
+
tokens: YotoTokenResponse;
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* Poll result for device authorization flow
|
|
329
|
+
*/
|
|
330
|
+
export type YotoDevicePollResult = YotoDevicePollPending | YotoDevicePollSlowDown | YotoDevicePollSuccess;
|
|
331
|
+
import { DEVICE_CODE_GRANT_TYPE } from './constants.js';
|
|
56
332
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["auth.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["auth.js"],"names":[],"mappings":"AAWA;;;GAGG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,4JAbG;IAA0B,QAAQ;IACR,KAAK;IACS,YAAY,EAA3C,qBAAqB;IACL,QAAQ,EAAxB,MAAM;IACU,WAAW,EAA3B,MAAM;IACU,KAAK,EAArB,MAAM;IACW,KAAK;IACQ,MAAM;IACnB,MAAM;IACN,aAAa;IACS,mBAAmB;CACnE,GAAS,MAAM,CA+BjB;AAED;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,4KAdG;IAAqC,SAAS,EAArC,kBAAkB;IACD,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;AAED;;;;;;;;;GASG;AAEH;;;;;;;;;;GAUG;AACH,4FAPG;IAAyB,QAAQ,EAAxB,MAAM;IACW,KAAK;IACL,QAAQ;IACR,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,sBAAsB,CAAC,CA+B1C;AAED;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,mHAVG;IAAwB,UAAU,EAA1B,MAAM;IACU,QAAQ,EAAxB,MAAM;IACW,QAAQ;IACR,eAAe;IACf,SAAS;IACD,cAAc;;;CAC/C,GAAU,OAAO,CAAC,oBAAoB,CAAC,CAuDzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,8IArCG;IAAwB,UAAU,EAA1B,MAAM;IACU,QAAQ,EAAxB,MAAM;IACW,QAAQ;IACR,eAAe;IACf,SAAS;IACT,SAAS;IACD,cAAc;;;IACU,MAAM,aAA9C,oBAAoB,KAAK,IAAI;CAC9C,GAAU,OAAO,CAAC,iBAAiB,CAAC,CA0EtC;;;;oCA5ZY,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,gBAAgB,GAAG,qBAAqB;;;;kCAKzG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB;;;;2CAK/C,MAAM,GAAG,OAAO;;;;iCAKhB,oBAAoB,GAAG,eAAe,GAAG,oBAAoB,GAAG,OAAO,sBAAsB;;kBAsD5F,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;;;;;;;;;YAgDN,SAAS;;;;cACT,MAAM;;;;;;;;;YAMN,WAAW;;;;cACX,MAAM;;;;;;;;;YAMN,SAAS;;;;YACT,iBAAiB;;;;;mCAKlB,qBAAqB,GAAG,sBAAsB,GAAG,qBAAqB;uCAxPK,gBAAgB"}
|
|
@@ -3,27 +3,47 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { request } from 'undici'
|
|
5
5
|
import { defaultHeaders, handleBadResponse, mergeRequestOptions, YotoAPIError } from './helpers.js'
|
|
6
|
-
import { DEFAULT_SCOPE, DEFAULT_AUDIENCE, YOTO_LOGIN_URL } from './constants.js'
|
|
6
|
+
import { DEFAULT_SCOPE, DEFAULT_AUDIENCE, YOTO_LOGIN_URL, DEVICE_CODE_GRANT_TYPE } from './constants.js'
|
|
7
7
|
|
|
8
8
|
// ============================================================================
|
|
9
9
|
// Authentication: Authentication endpoints for browser-based and device flows
|
|
10
10
|
// ============================================================================
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* OAuth2 response type for authorization requests
|
|
14
|
+
* @typedef {'code' | 'token' | 'id_token' | 'code token' | 'code id_token' | 'token id_token' | 'code token id_token'} YotoOAuthResponseType
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OAuth2 prompt parameter for authorization requests
|
|
19
|
+
* @typedef {'none' | 'login' | 'consent' | 'select_account'} YotoOAuthPromptType
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* PKCE code challenge method for authorization requests
|
|
24
|
+
* @typedef {'S256' | 'plain'} YotoOAuthCodeChallengeMethod
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* OAuth2 grant type for token exchange requests
|
|
29
|
+
* @typedef {'authorization_code' | 'refresh_token' | 'client_credentials' | typeof DEVICE_CODE_GRANT_TYPE} YotoOAuthGrantType
|
|
30
|
+
*/
|
|
31
|
+
|
|
12
32
|
/**
|
|
13
33
|
* Redirects the user to Yoto's login page to begin the OAuth2 Authorization Code flow.
|
|
14
34
|
* @see https://yoto.dev/api/get-authorize/
|
|
15
35
|
* @param {object} options
|
|
16
36
|
* @param {string} [options.audience='https://api.yotoplay.com'] Audience for the token
|
|
17
37
|
* @param {string} [options.scope='openid profile offline_access'] Requested scopes
|
|
18
|
-
* @param {
|
|
38
|
+
* @param {YotoOAuthResponseType} options.responseType Required response type
|
|
19
39
|
* @param {string} options.clientId Required client ID
|
|
20
40
|
* @param {string} options.redirectUri Required redirect URI
|
|
21
41
|
* @param {string} options.state Required opaque value for preventing CSRF attacks
|
|
22
42
|
* @param {string} [options.nonce] String value to prevent replay attacks
|
|
23
|
-
* @param {
|
|
43
|
+
* @param {YotoOAuthPromptType} [options.prompt] Authorization server prompt behavior
|
|
24
44
|
* @param {number} [options.maxAge] Maximum authentication age in seconds
|
|
25
45
|
* @param {string} [options.codeChallenge] PKCE code challenge
|
|
26
|
-
* @param {
|
|
46
|
+
* @param {YotoOAuthCodeChallengeMethod} [options.codeChallengeMethod] PKCE code challenge method
|
|
27
47
|
* @return {string} The authorization URL to redirect the user to
|
|
28
48
|
*/
|
|
29
49
|
export function getAuthorizeUrl ({
|
|
@@ -73,7 +93,7 @@ export function getAuthorizeUrl ({
|
|
|
73
93
|
* Exchange authorization code or refresh token for access tokens.
|
|
74
94
|
* @see https://yoto.dev/api/post-oauth-token/
|
|
75
95
|
* @param {object} options
|
|
76
|
-
* @param {
|
|
96
|
+
* @param {YotoOAuthGrantType} options.grantType Required grant type
|
|
77
97
|
* @param {string} [options.code] Authorization code (required for authorization_code grant)
|
|
78
98
|
* @param {string} [options.redirectUri] Redirect URI (required for authorization_code grant if used in authorize request)
|
|
79
99
|
* @param {string} [options.refreshToken] Refresh token (required for refresh_token grant)
|
|
@@ -114,7 +134,7 @@ export async function exchangeToken ({
|
|
|
114
134
|
} else if (grantType === 'refresh_token') {
|
|
115
135
|
if (!refreshToken) throw new Error('refreshToken is required for refresh_token grant')
|
|
116
136
|
formData.set('refresh_token', refreshToken)
|
|
117
|
-
} else if (grantType ===
|
|
137
|
+
} else if (grantType === DEVICE_CODE_GRANT_TYPE) {
|
|
118
138
|
if (!deviceCode) throw new Error('deviceCode is required for device_code grant')
|
|
119
139
|
formData.set('device_code', deviceCode)
|
|
120
140
|
}
|
|
@@ -137,7 +157,7 @@ export async function exchangeToken ({
|
|
|
137
157
|
|
|
138
158
|
// For device_code grant, always parse JSON first so error details are available
|
|
139
159
|
// 403 errors with authorization_pending/slow_down are expected during polling
|
|
140
|
-
if (grantType ===
|
|
160
|
+
if (grantType === DEVICE_CODE_GRANT_TYPE) {
|
|
141
161
|
/** @type {any} */
|
|
142
162
|
const rawResponse = await response.body.json()
|
|
143
163
|
|
|
@@ -207,3 +227,200 @@ export async function requestDeviceCode ({
|
|
|
207
227
|
const responseBody = /** @type {YotoDeviceCodeResponse} */ (await response.body.json())
|
|
208
228
|
return responseBody
|
|
209
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Poll result when authorization is still pending
|
|
233
|
+
* @typedef {Object} YotoDevicePollPending
|
|
234
|
+
* @property {'pending'} status - Indicates polling should continue
|
|
235
|
+
* @property {number} interval - Current polling interval in milliseconds
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Poll result when polling needs to slow down
|
|
240
|
+
* @typedef {Object} YotoDevicePollSlowDown
|
|
241
|
+
* @property {'slow_down'} status - Indicates polling interval should be increased
|
|
242
|
+
* @property {number} interval - New polling interval in milliseconds
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Poll result when authorization is successful
|
|
247
|
+
* @typedef {Object} YotoDevicePollSuccess
|
|
248
|
+
* @property {'success'} status - Indicates successful authorization
|
|
249
|
+
* @property {YotoTokenResponse} tokens - OAuth tokens
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Poll result for device authorization flow
|
|
254
|
+
* @typedef {YotoDevicePollPending | YotoDevicePollSlowDown | YotoDevicePollSuccess} YotoDevicePollResult
|
|
255
|
+
*/
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Poll for device authorization completion with automatic error handling (single poll attempt).
|
|
259
|
+
* This function handles common polling errors (authorization_pending, slow_down)
|
|
260
|
+
* and only throws for unrecoverable errors (expired_token, access_denied, etc).
|
|
261
|
+
*
|
|
262
|
+
* Non-blocking - returns immediately with poll result. Suitable for:
|
|
263
|
+
* - Manual polling loops in CLI applications
|
|
264
|
+
* - Server-side endpoints that poll on behalf of clients (e.g., Homebridge UI server)
|
|
265
|
+
* - Custom UI implementations with specific polling behavior
|
|
266
|
+
*
|
|
267
|
+
* For the simplest approach (automatic polling loop), use waitForDeviceAuthorization() instead.
|
|
268
|
+
*
|
|
269
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
270
|
+
* @param {object} options
|
|
271
|
+
* @param {string} options.deviceCode - Device code from requestDeviceCode()
|
|
272
|
+
* @param {string} options.clientId - OAuth client ID
|
|
273
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] - Audience for the token
|
|
274
|
+
* @param {number} [options.currentInterval=5000] - Current polling interval in milliseconds
|
|
275
|
+
* @param {string} [options.userAgent] - Optional user agent string
|
|
276
|
+
* @param {RequestOptions} [options.requestOptions] - Additional undici request options
|
|
277
|
+
* @returns {Promise<YotoDevicePollResult>} Poll result with status and data
|
|
278
|
+
* @throws {YotoAPIError} For unrecoverable errors (expired_token, access_denied, invalid_grant, etc)
|
|
279
|
+
*
|
|
280
|
+
*/
|
|
281
|
+
export async function pollForDeviceToken ({
|
|
282
|
+
deviceCode,
|
|
283
|
+
clientId,
|
|
284
|
+
audience = DEFAULT_AUDIENCE,
|
|
285
|
+
currentInterval = 5000,
|
|
286
|
+
userAgent,
|
|
287
|
+
requestOptions
|
|
288
|
+
}) {
|
|
289
|
+
try {
|
|
290
|
+
const tokens = await exchangeToken({
|
|
291
|
+
grantType: DEVICE_CODE_GRANT_TYPE,
|
|
292
|
+
deviceCode,
|
|
293
|
+
clientId,
|
|
294
|
+
audience,
|
|
295
|
+
userAgent,
|
|
296
|
+
requestOptions
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// Success - return tokens
|
|
300
|
+
return {
|
|
301
|
+
status: 'success',
|
|
302
|
+
tokens
|
|
303
|
+
}
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const error = /** @type {any} */ (err)
|
|
306
|
+
const errorCode = error.body?.error
|
|
307
|
+
|
|
308
|
+
// Handle recoverable polling states
|
|
309
|
+
if (errorCode === 'authorization_pending') {
|
|
310
|
+
// User hasn't authorized yet - continue polling
|
|
311
|
+
return {
|
|
312
|
+
status: 'pending',
|
|
313
|
+
interval: currentInterval
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (errorCode === 'slow_down') {
|
|
318
|
+
// Polling too fast - increase interval by 5 seconds
|
|
319
|
+
return {
|
|
320
|
+
status: 'slow_down',
|
|
321
|
+
interval: currentInterval + 5000
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// All other errors are unrecoverable - throw them
|
|
326
|
+
// Common unrecoverable errors:
|
|
327
|
+
// - expired_token: Device code expired
|
|
328
|
+
// - access_denied: User denied authorization
|
|
329
|
+
// - invalid_grant: Invalid device code or other grant issue
|
|
330
|
+
throw error
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Wait for device authorization to complete with automatic polling.
|
|
336
|
+
* This function wraps the entire polling loop - just call it and await the result.
|
|
337
|
+
* It handles all polling logic internally including interval adjustments.
|
|
338
|
+
*
|
|
339
|
+
* Designed for CLI usage where you want to block until authorization completes.
|
|
340
|
+
* For UI implementations with progress feedback, use pollForDeviceToken() directly.
|
|
341
|
+
*
|
|
342
|
+
* @see https://yoto.dev/api/post-oauth-token/
|
|
343
|
+
* @param {object} options
|
|
344
|
+
* @param {string} options.deviceCode - Device code from requestDeviceCode()
|
|
345
|
+
* @param {string} options.clientId - OAuth client ID
|
|
346
|
+
* @param {string} [options.audience='https://api.yotoplay.com'] - Audience for the token
|
|
347
|
+
* @param {number} [options.initialInterval=5000] - Initial polling interval in milliseconds
|
|
348
|
+
* @param {number} [options.expiresIn] - Seconds until device code expires (for timeout calculation)
|
|
349
|
+
* @param {string} [options.userAgent] - Optional user agent string
|
|
350
|
+
* @param {RequestOptions} [options.requestOptions] - Additional undici request options
|
|
351
|
+
* @param {(result: YotoDevicePollResult) => void} [options.onPoll] - Optional callback invoked after each poll attempt
|
|
352
|
+
* @returns {Promise<YotoTokenResponse>} Token response on successful authorization
|
|
353
|
+
* @throws {YotoAPIError} For unrecoverable errors (expired_token, access_denied, invalid_grant, etc)
|
|
354
|
+
* @throws {Error} If device code expires (timeout)
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* // Simple usage - just wait for tokens
|
|
358
|
+
* const deviceAuth = await requestDeviceCode({ clientId })
|
|
359
|
+
* console.log(`Visit: ${deviceAuth.verification_uri_complete}`)
|
|
360
|
+
*
|
|
361
|
+
* const tokens = await waitForDeviceAuthorization({
|
|
362
|
+
* deviceCode: deviceAuth.device_code,
|
|
363
|
+
* clientId,
|
|
364
|
+
* initialInterval: deviceAuth.interval * 1000,
|
|
365
|
+
* expiresIn: deviceAuth.expires_in
|
|
366
|
+
* })
|
|
367
|
+
*
|
|
368
|
+
* console.log('Got tokens:', tokens)
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* // With progress callback
|
|
372
|
+
* const tokens = await waitForDeviceAuthorization({
|
|
373
|
+
* deviceCode: deviceAuth.device_code,
|
|
374
|
+
* clientId,
|
|
375
|
+
* onPoll: (result) => {
|
|
376
|
+
* if (result.status === 'pending') process.stdout.write('.')
|
|
377
|
+
* if (result.status === 'slow_down') console.log('\nSlowing down...')
|
|
378
|
+
* }
|
|
379
|
+
* })
|
|
380
|
+
*/
|
|
381
|
+
export async function waitForDeviceAuthorization ({
|
|
382
|
+
deviceCode,
|
|
383
|
+
clientId,
|
|
384
|
+
audience = DEFAULT_AUDIENCE,
|
|
385
|
+
initialInterval = 5000,
|
|
386
|
+
expiresIn,
|
|
387
|
+
userAgent,
|
|
388
|
+
requestOptions,
|
|
389
|
+
onPoll
|
|
390
|
+
}) {
|
|
391
|
+
let interval = initialInterval
|
|
392
|
+
const startTime = Date.now()
|
|
393
|
+
const expiresAt = expiresIn ? startTime + (expiresIn * 1000) : null
|
|
394
|
+
|
|
395
|
+
while (true) {
|
|
396
|
+
// Check if we've exceeded the expiration time
|
|
397
|
+
if (expiresAt && Date.now() >= expiresAt) {
|
|
398
|
+
throw new Error('Device code has expired')
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const result = await pollForDeviceToken({
|
|
402
|
+
deviceCode,
|
|
403
|
+
clientId,
|
|
404
|
+
audience,
|
|
405
|
+
currentInterval: interval,
|
|
406
|
+
userAgent,
|
|
407
|
+
requestOptions
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
// Invoke callback if provided
|
|
411
|
+
if (onPoll) {
|
|
412
|
+
onPoll(result)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (result.status === 'success') {
|
|
416
|
+
return result.tokens
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (result.status === 'slow_down') {
|
|
420
|
+
interval = result.interval
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Wait before next poll
|
|
424
|
+
await new Promise(resolve => setTimeout(resolve, interval))
|
|
425
|
+
}
|
|
426
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
|
-
import { exchangeToken } from './auth.js'
|
|
3
|
+
import { exchangeToken, pollForDeviceToken } from './auth.js'
|
|
4
4
|
import { YotoAPIError } from './helpers.js'
|
|
5
|
-
import { loadTestTokens } from './test-helpers.js'
|
|
5
|
+
import { loadTestTokens } from './endpoint-test-helpers.js'
|
|
6
6
|
|
|
7
7
|
const { clientId } = loadTestTokens()
|
|
8
8
|
|
|
@@ -25,3 +25,55 @@ test('exchangeToken - refresh flow', async (t) => {
|
|
|
25
25
|
)
|
|
26
26
|
})
|
|
27
27
|
})
|
|
28
|
+
|
|
29
|
+
test('pollForDeviceToken', async (t) => {
|
|
30
|
+
await t.test('should throw invalid_grant for invalid device code', async () => {
|
|
31
|
+
// Invalid device codes throw invalid_grant error (unrecoverable)
|
|
32
|
+
await assert.rejects(
|
|
33
|
+
async () => {
|
|
34
|
+
await pollForDeviceToken({
|
|
35
|
+
deviceCode: 'invalid-device-code',
|
|
36
|
+
clientId,
|
|
37
|
+
currentInterval: 5000
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
(err) => {
|
|
41
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
42
|
+
// @ts-expect-error
|
|
43
|
+
assert.strictEqual(err.body?.error, 'invalid_grant', 'Should be invalid_grant error')
|
|
44
|
+
assert.ok(err.statusCode, 'Error should have statusCode')
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await t.test('should throw for expired_token error', async () => {
|
|
51
|
+
// The function should throw for expired_token (unrecoverable)
|
|
52
|
+
// We can't easily test this without a real flow, but we verify the error handling works
|
|
53
|
+
await assert.rejects(
|
|
54
|
+
async () => {
|
|
55
|
+
await pollForDeviceToken({
|
|
56
|
+
deviceCode: 'expired-device-code-xyz',
|
|
57
|
+
clientId,
|
|
58
|
+
currentInterval: 5000
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
(err) => {
|
|
62
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
63
|
+
// Will get invalid_grant for malformed codes
|
|
64
|
+
// @ts-expect-error
|
|
65
|
+
assert.ok(err.body?.error, 'Should have error code')
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Note: Testing authorization_pending and slow_down states requires a real device flow
|
|
72
|
+
// These tests would need to:
|
|
73
|
+
// 1. Call requestDeviceCode() to get a valid device_code
|
|
74
|
+
// 2. Poll immediately (should get authorization_pending)
|
|
75
|
+
// 3. Poll rapidly (might get slow_down)
|
|
76
|
+
// 4. Complete auth in browser (would get success)
|
|
77
|
+
// This is too complex and flaky for unit tests, so we skip testing those paths
|
|
78
|
+
// The implementation is verified by the CLI tool usage in bin/auth.js
|
|
79
|
+
})
|