yoto-nodejs-client 0.0.10 → 0.0.11
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/bin/auth.js +1 -1
- package/bin/device-model.js +11 -8
- package/bin/lib/cli-helpers.d.ts.map +1 -1
- package/bin/lib/cli-helpers.js +9 -6
- package/lib/api-endpoints/auth.d.ts.map +1 -1
- package/lib/api-endpoints/auth.js +15 -5
- package/lib/api-endpoints/auth.test.js +3 -3
- package/lib/api-endpoints/content.test.js +1 -1
- package/lib/api-endpoints/devices.test.js +5 -5
- package/lib/api-endpoints/family-library-groups.test.js +1 -1
- package/lib/api-endpoints/family.test.js +2 -2
- package/lib/api-endpoints/helpers.d.ts +5 -3
- package/lib/api-endpoints/helpers.d.ts.map +1 -1
- package/lib/api-endpoints/helpers.js +17 -10
- package/lib/api-endpoints/icons.test.js +2 -2
- package/lib/token.d.ts.map +1 -1
- package/lib/token.js +4 -2
- package/lib/yoto-device.d.ts.map +1 -1
- package/lib/yoto-device.js +144 -3
- package/package.json +1 -1
package/bin/auth.js
CHANGED
|
@@ -108,7 +108,7 @@ async function main () {
|
|
|
108
108
|
if (error.message === 'Device code has expired') {
|
|
109
109
|
console.error('\n❌ Device code has expired. Please run the command again.')
|
|
110
110
|
process.exit(1)
|
|
111
|
-
} else if (error.
|
|
111
|
+
} else if (error.jsonBody?.error === 'expired_token') {
|
|
112
112
|
console.error('\n❌ Device code has expired. Please run the command again.')
|
|
113
113
|
process.exit(1)
|
|
114
114
|
} else {
|
package/bin/device-model.js
CHANGED
|
@@ -256,19 +256,22 @@ async function main () {
|
|
|
256
256
|
// Show only changed fields
|
|
257
257
|
console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (${changedFields.size} change${changedFields.size === 1 ? '' : 's'})`)
|
|
258
258
|
for (const field of changedFields) {
|
|
259
|
+
if (field === 'trackLength' && changedFields.has('position')) {
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
259
263
|
let value = playback[field]
|
|
260
264
|
if (field === 'position' || field === 'trackLength') {
|
|
261
|
-
// Show position/trackLength together if either changed
|
|
265
|
+
// Show position/trackLength together if either changed.
|
|
262
266
|
value = `${playback.position}/${playback.trackLength}s`
|
|
263
267
|
console.log(` position/trackLength: ${value}`)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
console.log(` ${field}: ${value}`)
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (field === 'cardDurationSeconds' && typeof value === 'number') {
|
|
272
|
+
value = `${value}s`
|
|
271
273
|
}
|
|
274
|
+
console.log(` ${field}: ${value}`)
|
|
272
275
|
}
|
|
273
276
|
} else {
|
|
274
277
|
console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (no changes)`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-helpers.d.ts","sourceRoot":"","sources":["cli-helpers.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH,oCAFa,gCAAgC,CAoB5C;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;GAKG;AACH,wCAJW;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;CAAE,GACtD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAoB5F;AAED;;;;;;;;GAQG;AACH,sFANG;IAAwB,QAAQ,EAAxB,MAAM;IACU,YAAY,EAA5B,MAAM;IACU,WAAW,EAA3B,MAAM;IACW,UAAU;CACnC,GAAU,UAAU,CAwBtB;AAED;;;GAGG;AACH,sCAFW,GAAG,
|
|
1
|
+
{"version":3,"file":"cli-helpers.d.ts","sourceRoot":"","sources":["cli-helpers.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH,oCAFa,gCAAgC,CAoB5C;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;GAKG;AACH,wCAJW;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;CAAE,GACtD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAoB5F;AAED;;;;;;;;GAQG;AACH,sFANG;IAAwB,QAAQ,EAAxB,MAAM;IACU,YAAY,EAA5B,MAAM;IACU,WAAW,EAA3B,MAAM;IACW,UAAU;CACnC,GAAU,UAAU,CAwBtB;AAED;;;GAGG;AACH,sCAFW,GAAG,QA4Bb;AAED;;;GAGG;AACH,mCAFW,MAAM,QAKhB;sDA/IoD,YAAY;2BAGtC,yBAAyB"}
|
package/bin/lib/cli-helpers.js
CHANGED
|
@@ -116,17 +116,20 @@ export function handleCliError (error) {
|
|
|
116
116
|
console.error(`Status Code: ${error.statusCode}`)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
if (error.
|
|
120
|
-
console.error(`Error: ${error.
|
|
119
|
+
if (error.jsonBody?.error) {
|
|
120
|
+
console.error(`Error: ${error.jsonBody.error}`)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
if (error.
|
|
124
|
-
console.error(`Description: ${error.
|
|
123
|
+
if (error.jsonBody?.error_description) {
|
|
124
|
+
console.error(`Description: ${error.jsonBody.error_description}`)
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
if (error.
|
|
127
|
+
if (error.jsonBody && typeof error.jsonBody === 'object') {
|
|
128
128
|
console.error('\nFull error response:')
|
|
129
|
-
console.error(JSON.stringify(error.
|
|
129
|
+
console.error(JSON.stringify(error.jsonBody, null, 2))
|
|
130
|
+
} else if (error.textBody) {
|
|
131
|
+
console.error('\nRaw error response:')
|
|
132
|
+
console.error(error.textBody)
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
process.exit(1)
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,CA8ErC;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;;;;oCAtaY,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;;;;;;iBAsGN,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;uCAlQK,gBAAgB"}
|
|
@@ -158,14 +158,24 @@ export async function exchangeToken ({
|
|
|
158
158
|
// For device_code grant, always parse JSON first so error details are available
|
|
159
159
|
// 403 errors with authorization_pending/slow_down are expected during polling
|
|
160
160
|
if (grantType === DEVICE_CODE_GRANT_TYPE) {
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
const textBody = await response.body.text()
|
|
162
|
+
let jsonBody = null
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
jsonBody = JSON.parse(textBody)
|
|
166
|
+
} catch {
|
|
167
|
+
jsonBody = null
|
|
168
|
+
}
|
|
163
169
|
|
|
164
170
|
if (response.statusCode > 299) {
|
|
165
|
-
throw new YotoAPIError(response,
|
|
171
|
+
throw new YotoAPIError(response, textBody, jsonBody, { grantType })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!jsonBody || typeof jsonBody !== 'object') {
|
|
175
|
+
throw new Error('OAuth token response did not contain valid JSON')
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
const responseBody = /** @type {YotoTokenResponse} */ (
|
|
178
|
+
const responseBody = /** @type {YotoTokenResponse} */ (jsonBody)
|
|
169
179
|
return responseBody
|
|
170
180
|
}
|
|
171
181
|
|
|
@@ -303,7 +313,7 @@ export async function pollForDeviceToken ({
|
|
|
303
313
|
}
|
|
304
314
|
} catch (err) {
|
|
305
315
|
const error = /** @type {any} */ (err)
|
|
306
|
-
const errorCode = error.
|
|
316
|
+
const errorCode = error.jsonBody?.error
|
|
307
317
|
|
|
308
318
|
// Handle recoverable polling states
|
|
309
319
|
if (errorCode === 'authorization_pending') {
|
|
@@ -19,7 +19,7 @@ test('exchangeToken - refresh flow', async (t) => {
|
|
|
19
19
|
(err) => {
|
|
20
20
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
21
21
|
assert.ok(err.statusCode, 'Error should have statusCode')
|
|
22
|
-
assert.ok(err.
|
|
22
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
23
23
|
return true
|
|
24
24
|
}
|
|
25
25
|
)
|
|
@@ -40,7 +40,7 @@ test('pollForDeviceToken', async (t) => {
|
|
|
40
40
|
(err) => {
|
|
41
41
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
42
42
|
// @ts-expect-error
|
|
43
|
-
assert.strictEqual(err.
|
|
43
|
+
assert.strictEqual(err.jsonBody?.error, 'invalid_grant', 'Should be invalid_grant error')
|
|
44
44
|
assert.ok(err.statusCode, 'Error should have statusCode')
|
|
45
45
|
return true
|
|
46
46
|
}
|
|
@@ -62,7 +62,7 @@ test('pollForDeviceToken', async (t) => {
|
|
|
62
62
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
63
63
|
// Will get invalid_grant for malformed codes
|
|
64
64
|
// @ts-expect-error
|
|
65
|
-
assert.ok(err.
|
|
65
|
+
assert.ok(err.jsonBody?.error, 'Should have error code')
|
|
66
66
|
return true
|
|
67
67
|
}
|
|
68
68
|
)
|
|
@@ -79,7 +79,7 @@ test('getUserMyoContent', async (t) => {
|
|
|
79
79
|
(err) => {
|
|
80
80
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
81
81
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
82
|
-
assert.ok(err.
|
|
82
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
83
83
|
return true
|
|
84
84
|
}
|
|
85
85
|
)
|
|
@@ -43,7 +43,7 @@ test('getDevices', async (t) => {
|
|
|
43
43
|
(err) => {
|
|
44
44
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
45
45
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
46
|
-
assert.ok(err.
|
|
46
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
47
47
|
return true
|
|
48
48
|
}
|
|
49
49
|
)
|
|
@@ -185,7 +185,7 @@ test('getDeviceStatus', async (t) => {
|
|
|
185
185
|
(err) => {
|
|
186
186
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
187
187
|
assert.equal(err.statusCode, 404, 'Should return 404 for invalid device ID')
|
|
188
|
-
assert.ok(err.
|
|
188
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
189
189
|
return true
|
|
190
190
|
}
|
|
191
191
|
)
|
|
@@ -205,7 +205,7 @@ test('getDeviceStatus', async (t) => {
|
|
|
205
205
|
(err) => {
|
|
206
206
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
207
207
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
208
|
-
assert.ok(err.
|
|
208
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
209
209
|
return true
|
|
210
210
|
}
|
|
211
211
|
)
|
|
@@ -455,7 +455,7 @@ test('getDeviceConfig', async (t) => {
|
|
|
455
455
|
(err) => {
|
|
456
456
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
457
457
|
assert.ok(err.statusCode >= 400, 'Should return error status code for invalid device ID')
|
|
458
|
-
assert.ok(err.
|
|
458
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
459
459
|
return true
|
|
460
460
|
}
|
|
461
461
|
)
|
|
@@ -475,7 +475,7 @@ test('getDeviceConfig', async (t) => {
|
|
|
475
475
|
(err) => {
|
|
476
476
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
477
477
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
478
|
-
assert.ok(err.
|
|
478
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
479
479
|
return true
|
|
480
480
|
}
|
|
481
481
|
)
|
|
@@ -53,7 +53,7 @@ test('getGroups', async (t) => {
|
|
|
53
53
|
(err) => {
|
|
54
54
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
55
55
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
56
|
-
assert.ok(err.
|
|
56
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
57
57
|
return true
|
|
58
58
|
}
|
|
59
59
|
)
|
|
@@ -37,7 +37,7 @@ test('getFamilyImages', async (t) => {
|
|
|
37
37
|
(err) => {
|
|
38
38
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
39
39
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
40
|
-
assert.ok(err.
|
|
40
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
41
41
|
return true
|
|
42
42
|
}
|
|
43
43
|
)
|
|
@@ -164,7 +164,7 @@ test('getAFamilyImage', async (t) => {
|
|
|
164
164
|
(err) => {
|
|
165
165
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
166
166
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
167
|
-
assert.ok(err.
|
|
167
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
168
168
|
return true
|
|
169
169
|
}
|
|
170
170
|
)
|
|
@@ -46,12 +46,14 @@ export function handleBadResponse(response: Dispatcher.ResponseData, extra?: any
|
|
|
46
46
|
export class YotoAPIError extends Error {
|
|
47
47
|
/**
|
|
48
48
|
* @param {Dispatcher.ResponseData} response A undici Response
|
|
49
|
-
* @param {string
|
|
49
|
+
* @param {string} textBody response body as text
|
|
50
|
+
* @param {unknown | null} jsonBody parsed response body (or null if invalid JSON)
|
|
50
51
|
* @param {any} [extra] any extra info to attach to the error
|
|
51
52
|
*/
|
|
52
|
-
constructor(response: Dispatcher.ResponseData,
|
|
53
|
+
constructor(response: Dispatcher.ResponseData, textBody: string, jsonBody: unknown | null, extra?: any);
|
|
53
54
|
/** @type { number } */ statusCode: number;
|
|
54
|
-
/** @type {string
|
|
55
|
+
/** @type {string} */ textBody: string;
|
|
56
|
+
/** @type {unknown | null} */ jsonBody: unknown | null;
|
|
55
57
|
/** @type {any} */ extra: any;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["helpers.js"],"names":[],"mappings":"AAOA;;;GAGG;AAEH;;;;GAIG;AACH,yCAHG;IAAyB,SAAS;IACD,cAAc;;;CACjD;;;EAWA;AAED;;;;GAIG;AACH,sEAHG;IAAwB,WAAW,EAA1B,MAAM;IACS,SAAS;CACnC;;;;EAMA;AAED;;;;;;;GAOG;AACH,iDAJW,cAAc,mBACd,cAAc,GACZ,MAAM,CAoBlB;AAED;;;GAGG;AACH,4CAHY,uBAAuB,UACvB,GAAG,
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["helpers.js"],"names":[],"mappings":"AAOA;;;GAGG;AAEH;;;;GAIG;AACH,yCAHG;IAAyB,SAAS;IACD,cAAc;;;CACjD;;;EAWA;AAED;;;;GAIG;AACH,sEAHG;IAAwB,WAAW,EAA1B,MAAM;IACS,SAAS;CACnC;;;;EAMA;AAED;;;;;;;GAOG;AACH,iDAJW,cAAc,mBACd,cAAc,GACZ,MAAM,CAoBlB;AAED;;;GAGG;AACH,4CAHY,uBAAuB,UACvB,GAAG,iBAed;AAED;IAME;;;;;OAKG;IACH,sBALY,uBAAuB,YACvB,MAAM,YACN,OAAO,GAAG,IAAI,UACd,GAAG,EAWd;IApBD,uBAAuB,CAAC,YAAZ,MAAM,CAAgB;IAClC,qBAAqB,CAAC,UAAX,MAAM,CAAa;IAC9B,6BAA6B,CAAC,UAAnB,OAAO,GAAG,IAAI,CAAa;IACtC,kBAAkB,CAAC,OAAR,GAAG,CAAU;CAkBzB;;;;6BArGY,WAAW,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;gCARxB,QAAQ;6BACX,QAAQ"}
|
|
@@ -73,32 +73,39 @@ export function mergeRequestOptions (baseOptions, requestOptions) {
|
|
|
73
73
|
*/
|
|
74
74
|
export async function handleBadResponse (response, extra) {
|
|
75
75
|
if (response.statusCode > 299) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
const textBody = await response.body.text()
|
|
77
|
+
let jsonBody = null
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
jsonBody = JSON.parse(textBody)
|
|
81
|
+
} catch {
|
|
82
|
+
jsonBody = null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new YotoAPIError(response, textBody, jsonBody, extra)
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
export class YotoAPIError extends Error {
|
|
86
90
|
/** @type { number } */ statusCode
|
|
87
|
-
/** @type {string
|
|
91
|
+
/** @type {string} */ textBody
|
|
92
|
+
/** @type {unknown | null} */ jsonBody
|
|
88
93
|
/** @type {any} */ extra
|
|
89
94
|
|
|
90
95
|
/**
|
|
91
96
|
* @param {Dispatcher.ResponseData} response A undici Response
|
|
92
|
-
* @param {string
|
|
97
|
+
* @param {string} textBody response body as text
|
|
98
|
+
* @param {unknown | null} jsonBody parsed response body (or null if invalid JSON)
|
|
93
99
|
* @param {any} [extra] any extra info to attach to the error
|
|
94
100
|
*/
|
|
95
|
-
constructor (response,
|
|
101
|
+
constructor (response, textBody, jsonBody, extra) {
|
|
96
102
|
super('Unexpected response status code')
|
|
97
103
|
this.name = this.constructor.name
|
|
98
104
|
Error.captureStackTrace(this, this.constructor)
|
|
99
105
|
|
|
100
106
|
this.statusCode = response.statusCode
|
|
101
|
-
this.
|
|
107
|
+
this.textBody = textBody
|
|
108
|
+
this.jsonBody = jsonBody
|
|
102
109
|
this.extra = extra
|
|
103
110
|
}
|
|
104
111
|
}
|
|
@@ -50,7 +50,7 @@ test('getPublicIcons', async (t) => {
|
|
|
50
50
|
(err) => {
|
|
51
51
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
52
52
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
53
|
-
assert.ok(err.
|
|
53
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
54
54
|
return true
|
|
55
55
|
}
|
|
56
56
|
)
|
|
@@ -96,7 +96,7 @@ test('getUserIcons', async (t) => {
|
|
|
96
96
|
(err) => {
|
|
97
97
|
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
98
98
|
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
99
|
-
assert.ok(err.
|
|
99
|
+
assert.ok(err.jsonBody, 'Error should have jsonBody')
|
|
100
100
|
return true
|
|
101
101
|
}
|
|
102
102
|
)
|
package/lib/token.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["token.js"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AAEH;;;;;;GAMG;AAEH;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AAEH;IAwBE;;OAEG;IACH,oFAFW,oBAAoB,EAuB9B;IAED;;;;OAIG;IACH,kBAHa,OAAO,CAAC,MAAM,CAAC,CAkB3B;
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["token.js"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AAEH;;;;;;GAMG;AAEH;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AAEH;IAwBE;;OAEG;IACH,oFAFW,oBAAoB,EAuB9B;IAED;;;;OAIG;IACH,kBAHa,OAAO,CAAC,MAAM,CAAC,CAkB3B;IA2GD;;OAEG;IACH,wBAGC;IAED;;OAEG;IACH,yBAQC;IAED;;;OAGG;IACH,wBAFa,OAAO,CAInB;IAED;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,mBAFa,MAAM,CAIlB;IAiED;;;OAGG;IACH,WAFa,OAAO,CASnB;IAED;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,oBAFa,MAAM,CAKlB;IAED;;;;;OAKG;IACH,WAHa,OAAO,CAAC,mBAAmB,CAAC,CAUxC;;CACF;;;;;cA3Xa,MAAM;;;;kBACN,MAAM;;;;iBACN,MAAM;;;;oBACN,qBAAqB;;;;oBACrB,MAAM;;;;;;;oCAQP,CAAC,mBAAmB,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;;cAKjE,MAAM;;;;wBACN,MAAM;;;;yBACN,MAAM;;;;sBACN,MAAM;;;;qBACN,MAAM;;;;sBACN,MAAM;;;;mBACN,MAAM;;;;;uCAKP;IACZ,eAAmB,EAAE,EAAE,CAAC;IACxB,iBAAqB,EAAE,CAAC,mBAAmB,CAAC,CAAC;IAC7C,eAAmB,EAAE,CAAC,KAAK,CAAC,CAAC;IAC7B,SAAa,EAAE,CAAC,KAAK,CAAC,CAAA;CACnB;6BA1CyB,aAAa"}
|
package/lib/token.js
CHANGED
|
@@ -214,12 +214,14 @@ export class RefreshableToken extends EventEmitter {
|
|
|
214
214
|
'refresh_token_expired'
|
|
215
215
|
]
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
const errorCode = error.jsonBody?.error
|
|
218
|
+
if (errorCode && invalidRefreshErrors.includes(errorCode)) {
|
|
218
219
|
// Mark this token as permanently invalid
|
|
219
220
|
this.#invalid = true
|
|
220
221
|
this.#clearAutoRefreshTimeout()
|
|
221
222
|
const statusCode = error.statusCode ? ` (${error.statusCode})` : ''
|
|
222
|
-
const
|
|
223
|
+
const errorDescription = error.jsonBody?.error_description
|
|
224
|
+
const invalidError = new Error(`Refresh token is invalid or expired${statusCode}: ${errorCode}${errorDescription ? ` - ${errorDescription}` : ''}`)
|
|
223
225
|
this.emit('invalid', invalidError)
|
|
224
226
|
throw invalidError
|
|
225
227
|
}
|
package/lib/yoto-device.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"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"}
|
package/lib/yoto-device.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { EventEmitter } from 'events'
|
|
21
21
|
import fastq from 'fastq'
|
|
22
22
|
import QuickLRU from 'quick-lru'
|
|
23
|
+
import { YotoAPIError } from './api-endpoints/helpers.js'
|
|
23
24
|
import { parseTemperature } from './helpers/temperature.js'
|
|
24
25
|
import { detectPowerState } from './helpers/power-state.js'
|
|
25
26
|
import { typedKeys } from './helpers/typed-keys.js'
|
|
@@ -1467,12 +1468,16 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
1467
1468
|
#updateStatusFromFullStatus (fullStatus) {
|
|
1468
1469
|
let statusChanged = false
|
|
1469
1470
|
let configChanged = false
|
|
1471
|
+
let playbackChanged = false
|
|
1470
1472
|
const wasOnline = this.#deviceOnline
|
|
1471
|
-
const { status, config } = this.#state
|
|
1473
|
+
const { status, config, playback } = this.#state
|
|
1474
|
+
const previousPlaybackCardId = playback.cardId
|
|
1472
1475
|
/** @type {Set<keyof YotoDeviceStatus>} */
|
|
1473
1476
|
const changedFields = new Set()
|
|
1474
1477
|
/** @type {Set<keyof YotoDeviceModelConfig>} */
|
|
1475
1478
|
const configChangedFields = new Set()
|
|
1479
|
+
/** @type {Set<keyof YotoPlaybackState>} */
|
|
1480
|
+
const playbackChangedFields = new Set()
|
|
1476
1481
|
|
|
1477
1482
|
/**
|
|
1478
1483
|
* Handler function for each status field
|
|
@@ -1837,6 +1842,20 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
1837
1842
|
handleField(key, fullStatus)
|
|
1838
1843
|
}
|
|
1839
1844
|
|
|
1845
|
+
if (changedFields.has('activeCardId') && previousPlaybackCardId !== status.activeCardId) {
|
|
1846
|
+
playback.cardId = status.activeCardId
|
|
1847
|
+
playbackChangedFields.add('cardId')
|
|
1848
|
+
playbackChanged = true
|
|
1849
|
+
|
|
1850
|
+
if (clearCardCachePlaybackFields(playback, playbackChangedFields)) {
|
|
1851
|
+
playbackChanged = true
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (this.#applyCardCacheToPlayback(playback.cardId, playback, playbackChangedFields)) {
|
|
1855
|
+
playbackChanged = true
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1840
1859
|
// Only emit if something actually changed
|
|
1841
1860
|
if (statusChanged) {
|
|
1842
1861
|
// Update metadata
|
|
@@ -1856,6 +1875,12 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
1856
1875
|
this.emit('configUpdate', this.config, configChangedFields)
|
|
1857
1876
|
}
|
|
1858
1877
|
|
|
1878
|
+
if (playbackChanged) {
|
|
1879
|
+
playback.updatedAt = new Date().toISOString()
|
|
1880
|
+
this.#state.lastUpdate.playback = Date.now()
|
|
1881
|
+
this.emit('playbackUpdate', this.playback, playbackChangedFields)
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1859
1884
|
// Check if device went offline (transition from online to offline)
|
|
1860
1885
|
if (wasOnline && !this.#deviceOnline) {
|
|
1861
1886
|
this.emit('offline', { reason: 'http-status', source: 'http' })
|
|
@@ -1870,10 +1895,14 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
1870
1895
|
*/
|
|
1871
1896
|
#updateStatusFromStatusResponse (statusResponse) {
|
|
1872
1897
|
let statusChanged = false
|
|
1898
|
+
let playbackChanged = false
|
|
1873
1899
|
const wasOnline = this.#deviceOnline
|
|
1874
|
-
const { status } = this.#state
|
|
1900
|
+
const { status, playback } = this.#state
|
|
1901
|
+
const previousPlaybackCardId = playback.cardId
|
|
1875
1902
|
/** @type {Set<keyof YotoDeviceStatus>} */
|
|
1876
1903
|
const changedFields = new Set()
|
|
1904
|
+
/** @type {Set<keyof YotoPlaybackState>} */
|
|
1905
|
+
const playbackChangedFields = new Set()
|
|
1877
1906
|
|
|
1878
1907
|
/**
|
|
1879
1908
|
* Handler function for each status field
|
|
@@ -2114,6 +2143,20 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
2114
2143
|
handleField(key, statusResponse)
|
|
2115
2144
|
}
|
|
2116
2145
|
|
|
2146
|
+
if (changedFields.has('activeCardId') && previousPlaybackCardId !== status.activeCardId) {
|
|
2147
|
+
playback.cardId = status.activeCardId
|
|
2148
|
+
playbackChangedFields.add('cardId')
|
|
2149
|
+
playbackChanged = true
|
|
2150
|
+
|
|
2151
|
+
if (clearCardCachePlaybackFields(playback, playbackChangedFields)) {
|
|
2152
|
+
playbackChanged = true
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
if (this.#applyCardCacheToPlayback(playback.cardId, playback, playbackChangedFields)) {
|
|
2156
|
+
playbackChanged = true
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2117
2160
|
// Only emit if something actually changed
|
|
2118
2161
|
if (statusChanged) {
|
|
2119
2162
|
// Update metadata
|
|
@@ -2126,6 +2169,12 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
2126
2169
|
this.emit('statusUpdate', this.status, 'http', changedFields)
|
|
2127
2170
|
}
|
|
2128
2171
|
|
|
2172
|
+
if (playbackChanged) {
|
|
2173
|
+
playback.updatedAt = new Date().toISOString()
|
|
2174
|
+
this.#state.lastUpdate.playback = Date.now()
|
|
2175
|
+
this.emit('playbackUpdate', this.playback, playbackChangedFields)
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2129
2178
|
// Check if device went offline (transition from online to offline)
|
|
2130
2179
|
if (wasOnline && !this.#deviceOnline) {
|
|
2131
2180
|
this.emit('offline', { reason: 'http-status', source: 'http' })
|
|
@@ -3198,7 +3247,25 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
3198
3247
|
}
|
|
3199
3248
|
|
|
3200
3249
|
return entry
|
|
3201
|
-
} catch {
|
|
3250
|
+
} catch (err) {
|
|
3251
|
+
if (err instanceof YotoAPIError) {
|
|
3252
|
+
const errorBody = /** @type {{ error?: { code?: string, message?: string } } | null} */ (err.jsonBody)
|
|
3253
|
+
const statusCode = err.statusCode
|
|
3254
|
+
const extraCardId = err.extra?.cardId
|
|
3255
|
+
const bodyCode = errorBody?.error?.code
|
|
3256
|
+
const bodyMessage = errorBody?.error?.message
|
|
3257
|
+
|
|
3258
|
+
const ignoreForbiddenCard = statusCode === 403 &&
|
|
3259
|
+
(!extraCardId || extraCardId === task.cardId) &&
|
|
3260
|
+
bodyCode === 'forbidden' &&
|
|
3261
|
+
bodyMessage?.includes('Access to card') &&
|
|
3262
|
+
bodyMessage?.includes('forbidden')
|
|
3263
|
+
|
|
3264
|
+
if (ignoreForbiddenCard) return null
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
3268
|
+
this.emit('error', error)
|
|
3202
3269
|
return null
|
|
3203
3270
|
} finally {
|
|
3204
3271
|
this.#cardCacheQueueTasks.delete(task.cardId)
|
|
@@ -3408,6 +3475,9 @@ export class YotoDeviceModel extends EventEmitter {
|
|
|
3408
3475
|
}
|
|
3409
3476
|
|
|
3410
3477
|
if (previousCardId !== playback.cardId) {
|
|
3478
|
+
if (clearCardCachePlaybackFields(playback, playbackChangedFields)) {
|
|
3479
|
+
playbackChanged = true
|
|
3480
|
+
}
|
|
3411
3481
|
if (this.#applyCardCacheToPlayback(playback.cardId, playback, playbackChangedFields)) {
|
|
3412
3482
|
playbackChanged = true
|
|
3413
3483
|
}
|
|
@@ -3517,6 +3587,77 @@ function buildCardCacheEntry (card) {
|
|
|
3517
3587
|
}
|
|
3518
3588
|
}
|
|
3519
3589
|
|
|
3590
|
+
/**
|
|
3591
|
+
* @param {YotoPlaybackState} playback
|
|
3592
|
+
* @param {Set<keyof YotoPlaybackState>} changedFields
|
|
3593
|
+
* @returns {boolean}
|
|
3594
|
+
*/
|
|
3595
|
+
function clearCardCachePlaybackFields (playback, changedFields) {
|
|
3596
|
+
let changed = false
|
|
3597
|
+
|
|
3598
|
+
if (!changedFields.has('cardTitle') && playback.cardTitle !== null) {
|
|
3599
|
+
playback.cardTitle = null
|
|
3600
|
+
changedFields.add('cardTitle')
|
|
3601
|
+
changed = true
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
if (!changedFields.has('cardSlug') && playback.cardSlug !== null) {
|
|
3605
|
+
playback.cardSlug = null
|
|
3606
|
+
changedFields.add('cardSlug')
|
|
3607
|
+
changed = true
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
if (!changedFields.has('cardCoverImageUrl') && playback.cardCoverImageUrl !== null) {
|
|
3611
|
+
playback.cardCoverImageUrl = null
|
|
3612
|
+
changedFields.add('cardCoverImageUrl')
|
|
3613
|
+
changed = true
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
if (!changedFields.has('cardAuthor') && playback.cardAuthor !== null) {
|
|
3617
|
+
playback.cardAuthor = null
|
|
3618
|
+
changedFields.add('cardAuthor')
|
|
3619
|
+
changed = true
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
if (!changedFields.has('cardReadBy') && playback.cardReadBy !== null) {
|
|
3623
|
+
playback.cardReadBy = null
|
|
3624
|
+
changedFields.add('cardReadBy')
|
|
3625
|
+
changed = true
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
if (!changedFields.has('cardDurationSeconds') && playback.cardDurationSeconds !== null) {
|
|
3629
|
+
playback.cardDurationSeconds = null
|
|
3630
|
+
changedFields.add('cardDurationSeconds')
|
|
3631
|
+
changed = true
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
if (!changedFields.has('trackTitle') && playback.trackTitle !== null) {
|
|
3635
|
+
playback.trackTitle = null
|
|
3636
|
+
changedFields.add('trackTitle')
|
|
3637
|
+
changed = true
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
if (!changedFields.has('trackLength') && playback.trackLength !== null) {
|
|
3641
|
+
playback.trackLength = null
|
|
3642
|
+
changedFields.add('trackLength')
|
|
3643
|
+
changed = true
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
if (!changedFields.has('chapterKey') && playback.chapterKey !== null) {
|
|
3647
|
+
playback.chapterKey = null
|
|
3648
|
+
changedFields.add('chapterKey')
|
|
3649
|
+
changed = true
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
if (!changedFields.has('chapterTitle') && playback.chapterTitle !== null) {
|
|
3653
|
+
playback.chapterTitle = null
|
|
3654
|
+
changedFields.add('chapterTitle')
|
|
3655
|
+
changed = true
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
return changed
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3520
3661
|
/**
|
|
3521
3662
|
* @param {YotoCardCacheEntry} entry
|
|
3522
3663
|
* @param {YotoPlaybackState} playback
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yoto-nodejs-client",
|
|
3
3
|
"description": "(Unofficial) Node.js client for the Yoto API with automatic token refresh, MQTT device communication, and TypeScript support",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.11",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/yoto-nodejs-client/issues"
|