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
package/lib/mqtt/topics.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* MQTT topic type for subscriptions
|
|
14
|
-
* @typedef {'events' | 'status' | 'response'} YotoMqttTopicType
|
|
14
|
+
* @typedef {'events' | 'status' | 'status-legacy' | 'response'} YotoMqttTopicType
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -45,28 +45,47 @@ export const MQTT_KEEPALIVE = 300
|
|
|
45
45
|
export const MQTT_ALPN_PROTOCOLS = ['x-amzn-mqtt-ca']
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Get the events topics for a device
|
|
49
|
-
*
|
|
48
|
+
* Get the events topics for a device
|
|
49
|
+
*
|
|
50
|
+
* NOTE: Old undocumented format device/{id}/events exists but returns different low-level
|
|
51
|
+
* hardware data. Do not use. Only the documented /data/ path is supported.
|
|
52
|
+
*
|
|
50
53
|
* @param {string} deviceId - Device ID
|
|
51
|
-
* @returns {string[]} Events topics
|
|
54
|
+
* @returns {string[]} Events topics
|
|
52
55
|
*/
|
|
53
56
|
export function getEventsTopic (deviceId) {
|
|
54
57
|
return [
|
|
55
|
-
`device/${deviceId}/events`, //
|
|
56
|
-
`device/${deviceId}/data/events` //
|
|
58
|
+
// `device/${deviceId}/events`, // OLD UNDOCUMENTED FORMAT - DO NOT USE (low-level hardware data, different schema)
|
|
59
|
+
`device/${deviceId}/data/events` // Documented format - use this
|
|
57
60
|
]
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
|
-
* Get the status topics for a device
|
|
62
|
-
*
|
|
64
|
+
* Get the status topics for a device
|
|
65
|
+
*
|
|
66
|
+
* Subscribes to BOTH documented and legacy status topics:
|
|
67
|
+
* - device/{id}/data/status: Documented format - responds to requestStatus(), auto-publishes every 5 min
|
|
68
|
+
* - device/{id}/status: Legacy format - does NOT respond to requests, emits on lifecycle events + every 5 min
|
|
69
|
+
*
|
|
70
|
+
* The legacy topic is the ONLY source for:
|
|
71
|
+
* - shutDown field: 'nA' = device running, any other value = device shutting down/shut down
|
|
72
|
+
* - Startup detection: shutDown='nA' + low upTime values + utcTime: 0 after power on
|
|
73
|
+
* - Full hardware diagnostics: battery voltage, memory stats, temperatures
|
|
74
|
+
*
|
|
75
|
+
* Key behavior differences:
|
|
76
|
+
* - Documented topic: Responds immediately to requestStatus() command
|
|
77
|
+
* - Legacy topic: Emits real-time on shutdown/startup events, plus periodic updates every 5 minutes
|
|
78
|
+
* - Legacy topic does NOT respond to requestStatus() - it's passive/event-driven only
|
|
79
|
+
*
|
|
80
|
+
* Both topics are necessary - the documented /data/ path does not include lifecycle events.
|
|
81
|
+
*
|
|
63
82
|
* @param {string} deviceId - Device ID
|
|
64
|
-
* @returns {string[]} Status topics
|
|
83
|
+
* @returns {string[]} Status topics (both documented and legacy)
|
|
65
84
|
*/
|
|
66
85
|
export function getStatusTopic (deviceId) {
|
|
67
86
|
return [
|
|
68
|
-
`device/${deviceId}/status`,
|
|
69
|
-
`device/${deviceId}/
|
|
87
|
+
`device/${deviceId}/data/status`, // Documented format - regular status updates
|
|
88
|
+
`device/${deviceId}/status` // Legacy format - REQUIRED for lifecycle events (shutDown, startup)
|
|
70
89
|
]
|
|
71
90
|
}
|
|
72
91
|
|
|
@@ -82,20 +101,24 @@ export function getResponseTopic (deviceId) {
|
|
|
82
101
|
/**
|
|
83
102
|
* Get all subscription topics for a device
|
|
84
103
|
*
|
|
85
|
-
* Subscribes to
|
|
86
|
-
* -
|
|
87
|
-
* -
|
|
104
|
+
* Subscribes to documented formats PLUS legacy status topic:
|
|
105
|
+
* - device/{id}/data/events - Event messages (documented)
|
|
106
|
+
* - device/{id}/data/status - Status messages (documented)
|
|
107
|
+
* - device/{id}/status - Legacy status (REQUIRED for shutdown/startup lifecycle events)
|
|
108
|
+
* - device/{id}/response - Command responses (documented)
|
|
88
109
|
*
|
|
89
|
-
*
|
|
110
|
+
* The legacy status topic is necessary because the documented /data/ topics do not
|
|
111
|
+
* include device lifecycle events (shutdown, startup) which are critical for proper
|
|
112
|
+
* device state management.
|
|
90
113
|
*
|
|
91
114
|
* @param {string} deviceId - Device ID
|
|
92
|
-
* @returns {string[]} Array of topics to subscribe to
|
|
115
|
+
* @returns {string[]} Array of topics to subscribe to
|
|
93
116
|
*/
|
|
94
117
|
export function getSubscriptionTopics (deviceId) {
|
|
95
118
|
return [
|
|
96
|
-
...getEventsTopic(deviceId), //
|
|
97
|
-
...getStatusTopic(deviceId), //
|
|
98
|
-
getResponseTopic(deviceId) // Response topic
|
|
119
|
+
...getEventsTopic(deviceId), // Events topic (documented format)
|
|
120
|
+
...getStatusTopic(deviceId), // Status topics (documented + legacy for lifecycle)
|
|
121
|
+
getResponseTopic(deviceId) // Response topic
|
|
99
122
|
]
|
|
100
123
|
}
|
|
101
124
|
|
|
@@ -113,6 +136,11 @@ export function getCommandTopic (deviceId, resource, action) {
|
|
|
113
136
|
|
|
114
137
|
/**
|
|
115
138
|
* Parse a topic string to extract device ID and message type
|
|
139
|
+
*
|
|
140
|
+
* Distinguishes between documented and legacy status topics:
|
|
141
|
+
* - device/{id}/data/status -> messageType: 'status' (documented)
|
|
142
|
+
* - device/{id}/status -> messageType: 'status-legacy' (for lifecycle events)
|
|
143
|
+
*
|
|
116
144
|
* @param {string} topic - Full MQTT topic string
|
|
117
145
|
* @returns {{ deviceId: string, messageType: YotoMqttTopicType | 'unknown' }} Parsed topic info
|
|
118
146
|
*/
|
|
@@ -125,10 +153,16 @@ export function parseTopic (topic) {
|
|
|
125
153
|
|
|
126
154
|
const deviceId = parts[1] || ''
|
|
127
155
|
|
|
128
|
-
// Handle both
|
|
156
|
+
// Handle both documented format (device/{id}/data/status) and legacy format (device/{id}/status)
|
|
129
157
|
let messageType = parts[2]
|
|
158
|
+
|
|
130
159
|
if (messageType === 'data' && parts.length >= 4) {
|
|
160
|
+
// Documented format: device/{id}/data/{type}
|
|
131
161
|
messageType = parts[3] // Get the actual message type after /data/
|
|
162
|
+
} else if (messageType === 'status') {
|
|
163
|
+
// Legacy format: device/{id}/status
|
|
164
|
+
// Return 'status-legacy' to distinguish from documented status
|
|
165
|
+
return { deviceId, messageType: 'status-legacy' }
|
|
132
166
|
}
|
|
133
167
|
|
|
134
168
|
// Validate message type
|
package/lib/pkg.d.cts
CHANGED
|
@@ -7,6 +7,7 @@ export const pkg: {
|
|
|
7
7
|
url: string;
|
|
8
8
|
};
|
|
9
9
|
dependencies: {
|
|
10
|
+
"@unblessed/node": string;
|
|
10
11
|
"jwt-decode": string;
|
|
11
12
|
mqtt: string;
|
|
12
13
|
undici: string;
|
|
@@ -17,6 +18,7 @@ export const pkg: {
|
|
|
17
18
|
argsclopts: string;
|
|
18
19
|
"auto-changelog": string;
|
|
19
20
|
c8: string;
|
|
21
|
+
eslint: string;
|
|
20
22
|
"gh-release": string;
|
|
21
23
|
neostandard: string;
|
|
22
24
|
"npm-run-all2": string;
|
|
@@ -37,6 +39,13 @@ export const pkg: {
|
|
|
37
39
|
bin: {
|
|
38
40
|
"yoto-auth": string;
|
|
39
41
|
"yoto-token-info": string;
|
|
42
|
+
"yoto-refresh-token": string;
|
|
43
|
+
"yoto-devices": string;
|
|
44
|
+
"yoto-device-model": string;
|
|
45
|
+
"yoto-device-tui": string;
|
|
46
|
+
"yoto-content": string;
|
|
47
|
+
"yoto-groups": string;
|
|
48
|
+
"yoto-icons": string;
|
|
40
49
|
};
|
|
41
50
|
repository: {
|
|
42
51
|
type: string;
|
package/lib/token.d.ts
CHANGED
|
@@ -1,24 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} RefreshableTokenOpts
|
|
3
|
+
* @property {string} clientId - OAuth client ID
|
|
4
|
+
* @property {string} refreshToken - OAuth refresh token
|
|
5
|
+
* @property {string} accessToken - Initial OAuth access token (JWT)
|
|
6
|
+
* @property {number} [bufferSeconds=30] - Seconds before expiration to consider token expired
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} RefreshSuccessEvent
|
|
10
|
+
* @property {string} clientId - The OAuth client ID
|
|
11
|
+
* @property {string} updatedAccessToken - The new access token
|
|
12
|
+
* @property {string} updatedRefreshToken - The refresh token (may be updated)
|
|
13
|
+
* @property {number} updatedExpiresAt - Unix timestamp in seconds when token expires
|
|
14
|
+
* @property {string} prevAccessToken - The accessToken used in to initiate the refresh flow to be discarded
|
|
15
|
+
* @property {string} prevRefreshToken - The refreshToken used in to initiate the refresh flow to be discarded
|
|
16
|
+
* @property {number} prevExpiresAt - The expiresAt for the previous token pair
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Event map for RefreshableToken
|
|
20
|
+
* @typedef {{
|
|
21
|
+
* 'refresh:start': [],
|
|
22
|
+
* 'refresh:success': [RefreshSuccessEvent],
|
|
23
|
+
* 'refresh:error': [Error],
|
|
24
|
+
* 'invalid': [Error]
|
|
25
|
+
* }} RefreshableTokenEventMap
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* A refreshable OAuth token that automatically refreshes when expired.
|
|
29
|
+
* Handles in-flight refresh deduplication to prevent multiple concurrent refresh requests.
|
|
30
|
+
*
|
|
31
|
+
* Events:
|
|
32
|
+
* - 'refresh:start' - Emitted when token refresh begins
|
|
33
|
+
* - 'refresh:success' - Emitted when token refresh succeeds, passes { clientId, accessToken, refreshToken, expiresAt }
|
|
34
|
+
* - 'refresh:error' - Emitted when token refresh fails (transient errors), passes error
|
|
35
|
+
* - 'invalid' - Emitted when refresh token is permanently invalid, passes error
|
|
36
|
+
*
|
|
37
|
+
* @extends {EventEmitter<RefreshableTokenEventMap>}
|
|
38
|
+
*/
|
|
1
39
|
export class RefreshableToken extends EventEmitter<RefreshableTokenEventMap> {
|
|
40
|
+
/**
|
|
41
|
+
* @param {RefreshableTokenOpts} opts
|
|
42
|
+
*/
|
|
2
43
|
constructor({ clientId, refreshToken, accessToken, bufferSeconds }: RefreshableTokenOpts);
|
|
44
|
+
/**
|
|
45
|
+
* Get a valid access token, refreshing if necessary.
|
|
46
|
+
* @returns {Promise<string>} Valid access token
|
|
47
|
+
* @throws {Error} If token is invalid or refresh fails
|
|
48
|
+
*/
|
|
3
49
|
getAccessToken(): Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Check if the token is currently valid (not expired and not marked invalid).
|
|
52
|
+
* @returns {boolean} True if token is valid
|
|
53
|
+
*/
|
|
4
54
|
isValid(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Get the expiration timestamp of the current access token.
|
|
57
|
+
* @returns {number} Unix timestamp (seconds since epoch)
|
|
58
|
+
*/
|
|
5
59
|
getExpiresAt(): number;
|
|
60
|
+
/**
|
|
61
|
+
* Get the time remaining until token expiration.
|
|
62
|
+
* @returns {number} Seconds until expiration (may be negative if expired)
|
|
63
|
+
*/
|
|
6
64
|
getTimeRemaining(): number;
|
|
65
|
+
/**
|
|
66
|
+
* Manually trigger a token refresh, regardless of expiration status.
|
|
67
|
+
* Useful for proactive refresh or testing.
|
|
68
|
+
* @returns {Promise<RefreshSuccessEvent>} Token information including clientId, accessToken, refreshToken, and expiresAt
|
|
69
|
+
* @throws {Error} If token is invalid or refresh fails
|
|
70
|
+
*/
|
|
7
71
|
refresh(): Promise<RefreshSuccessEvent>;
|
|
8
72
|
#private;
|
|
9
73
|
}
|
|
10
74
|
export type RefreshableTokenOpts = {
|
|
75
|
+
/**
|
|
76
|
+
* - OAuth client ID
|
|
77
|
+
*/
|
|
11
78
|
clientId: string;
|
|
79
|
+
/**
|
|
80
|
+
* - OAuth refresh token
|
|
81
|
+
*/
|
|
12
82
|
refreshToken: string;
|
|
83
|
+
/**
|
|
84
|
+
* - Initial OAuth access token (JWT)
|
|
85
|
+
*/
|
|
13
86
|
accessToken: string;
|
|
87
|
+
/**
|
|
88
|
+
* - Seconds before expiration to consider token expired
|
|
89
|
+
*/
|
|
14
90
|
bufferSeconds?: number;
|
|
15
91
|
};
|
|
16
92
|
export type RefreshSuccessEvent = {
|
|
93
|
+
/**
|
|
94
|
+
* - The OAuth client ID
|
|
95
|
+
*/
|
|
17
96
|
clientId: string;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
97
|
+
/**
|
|
98
|
+
* - The new access token
|
|
99
|
+
*/
|
|
100
|
+
updatedAccessToken: string;
|
|
101
|
+
/**
|
|
102
|
+
* - The refresh token (may be updated)
|
|
103
|
+
*/
|
|
104
|
+
updatedRefreshToken: string;
|
|
105
|
+
/**
|
|
106
|
+
* - Unix timestamp in seconds when token expires
|
|
107
|
+
*/
|
|
108
|
+
updatedExpiresAt: number;
|
|
109
|
+
/**
|
|
110
|
+
* - The accessToken used in to initiate the refresh flow to be discarded
|
|
111
|
+
*/
|
|
112
|
+
prevAccessToken: string;
|
|
113
|
+
/**
|
|
114
|
+
* - The refreshToken used in to initiate the refresh flow to be discarded
|
|
115
|
+
*/
|
|
116
|
+
prevRefreshToken: string;
|
|
117
|
+
/**
|
|
118
|
+
* - The expiresAt for the previous token pair
|
|
119
|
+
*/
|
|
120
|
+
prevExpiresAt: number;
|
|
21
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* Event map for RefreshableToken
|
|
124
|
+
*/
|
|
22
125
|
export type RefreshableTokenEventMap = {
|
|
23
126
|
"refresh:start": [];
|
|
24
127
|
"refresh:success": [RefreshSuccessEvent];
|
package/lib/token.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["token.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["token.js"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AACH;IAgBE;;OAEG;IACH,oEAFW,oBAAoB,EAoB9B;IAED;;;;OAIG;IACH,kBAHa,OAAO,CAAC,MAAM,CAAC,CAkB3B;IAkGD;;;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;;;;;cAhPa,MAAM;;;;kBACN,MAAM;;;;iBACN,MAAM;;;;oBACN,MAAM;;;;;;cAKN,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;6BA9ByB,aAAa"}
|
package/lib/token.js
CHANGED
|
@@ -13,9 +13,12 @@ import { exchangeToken } from './api-endpoints/auth.js'
|
|
|
13
13
|
/**
|
|
14
14
|
* @typedef {Object} RefreshSuccessEvent
|
|
15
15
|
* @property {string} clientId - The OAuth client ID
|
|
16
|
-
* @property {string}
|
|
17
|
-
* @property {string}
|
|
18
|
-
* @property {number}
|
|
16
|
+
* @property {string} updatedAccessToken - The new access token
|
|
17
|
+
* @property {string} updatedRefreshToken - The refresh token (may be updated)
|
|
18
|
+
* @property {number} updatedExpiresAt - Unix timestamp in seconds when token expires
|
|
19
|
+
* @property {string} prevAccessToken - The accessToken used in to initiate the refresh flow to be discarded
|
|
20
|
+
* @property {string} prevRefreshToken - The refreshToken used in to initiate the refresh flow to be discarded
|
|
21
|
+
* @property {number} prevExpiresAt - The expiresAt for the previous token pair
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -53,7 +56,7 @@ export class RefreshableToken extends EventEmitter {
|
|
|
53
56
|
#bufferSeconds
|
|
54
57
|
/** @type {boolean} */
|
|
55
58
|
#invalid = false
|
|
56
|
-
/** @type {Promise<
|
|
59
|
+
/** @type {Promise<RefreshSuccessEvent> | null} */
|
|
57
60
|
#inFlightRefresh = null
|
|
58
61
|
|
|
59
62
|
/**
|
|
@@ -97,13 +100,14 @@ export class RefreshableToken extends EventEmitter {
|
|
|
97
100
|
return this.#accessToken
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
const { updatedAccessToken } = await this.#refreshAccessToken()
|
|
104
|
+
return updatedAccessToken
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
/**
|
|
104
108
|
* Refresh the access token using the refresh token.
|
|
105
109
|
* Handles in-flight request deduplication.
|
|
106
|
-
* @returns {Promise<
|
|
110
|
+
* @returns {Promise<RefreshSuccessEvent>} New access token
|
|
107
111
|
*/
|
|
108
112
|
async #refreshAccessToken () {
|
|
109
113
|
// If a refresh is already in flight, await the existing one
|
|
@@ -114,8 +118,7 @@ export class RefreshableToken extends EventEmitter {
|
|
|
114
118
|
// Create the refresh promise and set it on the class variable
|
|
115
119
|
try {
|
|
116
120
|
this.#inFlightRefresh = this.#performRefresh()
|
|
117
|
-
|
|
118
|
-
return newAccessToken
|
|
121
|
+
return await this.#inFlightRefresh
|
|
119
122
|
} finally {
|
|
120
123
|
// Clear the in-flight refresh once done
|
|
121
124
|
this.#inFlightRefresh = null
|
|
@@ -124,12 +127,17 @@ export class RefreshableToken extends EventEmitter {
|
|
|
124
127
|
|
|
125
128
|
/**
|
|
126
129
|
* Perform the actual token refresh.
|
|
127
|
-
* @returns {Promise<
|
|
130
|
+
* @returns {Promise<RefreshSuccessEvent>} New access token
|
|
128
131
|
*/
|
|
129
132
|
async #performRefresh () {
|
|
130
133
|
this.emit('refresh:start')
|
|
131
134
|
|
|
132
135
|
try {
|
|
136
|
+
// Snapshot the current tokens
|
|
137
|
+
const prevAccessToken = this.#accessToken
|
|
138
|
+
const prevRefreshToken = this.#refreshToken
|
|
139
|
+
const prevExpiresAt = this.#expiresAt
|
|
140
|
+
|
|
133
141
|
const tokens = await exchangeToken({
|
|
134
142
|
grantType: 'refresh_token',
|
|
135
143
|
refreshToken: this.#refreshToken,
|
|
@@ -151,14 +159,20 @@ export class RefreshableToken extends EventEmitter {
|
|
|
151
159
|
}
|
|
152
160
|
this.#expiresAt = decoded.exp
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
/* @type {RefreshSuccessEvent} */
|
|
163
|
+
const eventPayload = {
|
|
155
164
|
clientId: this.#clientId,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
updatedAccessToken: this.#accessToken,
|
|
166
|
+
updatedRefreshToken: this.#refreshToken,
|
|
167
|
+
updatedExpiresAt: this.#expiresAt,
|
|
168
|
+
prevAccessToken,
|
|
169
|
+
prevRefreshToken,
|
|
170
|
+
prevExpiresAt,
|
|
171
|
+
}
|
|
160
172
|
|
|
161
|
-
|
|
173
|
+
this.emit('refresh:success', eventPayload)
|
|
174
|
+
|
|
175
|
+
return eventPayload
|
|
162
176
|
} catch (err) {
|
|
163
177
|
const error = /** @type {any} */ (err)
|
|
164
178
|
|
|
@@ -228,13 +242,6 @@ export class RefreshableToken extends EventEmitter {
|
|
|
228
242
|
throw new Error('Token is invalid. Refresh token has expired or been revoked.')
|
|
229
243
|
}
|
|
230
244
|
|
|
231
|
-
await this.#refreshAccessToken()
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
clientId: this.#clientId,
|
|
235
|
-
accessToken: this.#accessToken,
|
|
236
|
-
refreshToken: this.#refreshToken,
|
|
237
|
-
expiresAt: this.#expiresAt
|
|
238
|
-
}
|
|
245
|
+
return await this.#refreshAccessToken()
|
|
239
246
|
}
|
|
240
247
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yoto Account initialization options
|
|
3
|
+
* @typedef {Object} YotoAccountOptions
|
|
4
|
+
* @property {YotoClientConstructorOptions} clientOptions - Yoto API client Options
|
|
5
|
+
* @property {YotoDeviceModelOptions} deviceOptions - Yoto Device Model Options
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Error context information
|
|
9
|
+
* @typedef {Object} YotoAccountErrorContext
|
|
10
|
+
* @property {string} source - Error source ('account', 'client', or deviceId)
|
|
11
|
+
* @property {string} [deviceId] - Device ID if error is device-specific
|
|
12
|
+
* @property {string} [operation] - Operation that failed
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Started event metadata
|
|
16
|
+
* @typedef {Object} YotoAccountStartedMetadata
|
|
17
|
+
* @property {number} deviceCount - Number of devices managed
|
|
18
|
+
* @property {string[]} devices - Array of device IDs
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Event map for YotoAccount
|
|
22
|
+
* @typedef {{
|
|
23
|
+
* 'started': [YotoAccountStartedMetadata],
|
|
24
|
+
* 'stopped': [],
|
|
25
|
+
* 'deviceAdded': [string, YotoDeviceModel],
|
|
26
|
+
* 'deviceRemoved': [string],
|
|
27
|
+
* 'error': [Error, YotoAccountErrorContext]
|
|
28
|
+
* }} YotoAccountEventMap
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Yoto Account client that manages all devices for an account
|
|
32
|
+
*
|
|
33
|
+
* Events:
|
|
34
|
+
* - 'started' - Emitted when account starts, passes metadata with deviceCount and devices array
|
|
35
|
+
* - 'stopped' - Emitted when account stops
|
|
36
|
+
* - 'deviceAdded' - Emitted when a device is added, passes (deviceId, deviceModel)
|
|
37
|
+
* - 'deviceRemoved' - Emitted when a device is removed, passes deviceId
|
|
38
|
+
* - 'error' - Emitted when an error occurs, passes (error, context)
|
|
39
|
+
*
|
|
40
|
+
* Note: To listen to individual device events (statusUpdate, configUpdate, playbackUpdate, online, offline, etc.),
|
|
41
|
+
* access the device models directly via account.devices or account.getDevice(deviceId) and attach listeners.
|
|
42
|
+
*
|
|
43
|
+
* @extends {EventEmitter<YotoAccountEventMap>}
|
|
44
|
+
*/
|
|
45
|
+
export class YotoAccount extends EventEmitter<YotoAccountEventMap> {
|
|
46
|
+
/**
|
|
47
|
+
* Create a Yoto account client
|
|
48
|
+
* @param {YotoAccountOptions} options - Account configuration options
|
|
49
|
+
*/
|
|
50
|
+
constructor(options: YotoAccountOptions);
|
|
51
|
+
/**
|
|
52
|
+
* Get the underlying YotoClient instance
|
|
53
|
+
* @returns {YotoClient}
|
|
54
|
+
*/
|
|
55
|
+
get client(): YotoClient;
|
|
56
|
+
/**
|
|
57
|
+
* Get all device models managed by this account
|
|
58
|
+
* @returns {Map<string, YotoDeviceModel>}
|
|
59
|
+
*/
|
|
60
|
+
get devices(): Map<string, YotoDeviceModel>;
|
|
61
|
+
/**
|
|
62
|
+
* Check if account is currently running
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
get running(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Check if account has been initialized
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
get initialized(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Get a specific device by ID
|
|
73
|
+
* @param {string} deviceId - Device ID
|
|
74
|
+
* @returns {YotoDeviceModel | undefined}
|
|
75
|
+
*/
|
|
76
|
+
getDevice(deviceId: string): YotoDeviceModel | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Get all device IDs
|
|
79
|
+
* @returns {string[]}
|
|
80
|
+
*/
|
|
81
|
+
getDeviceIds(): string[];
|
|
82
|
+
/**
|
|
83
|
+
* Start the account client - creates YotoClient, discovers devices, starts all device clients
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
* @throws {Error} If start fails
|
|
86
|
+
*/
|
|
87
|
+
start(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Stop the account client - stops all device clients
|
|
90
|
+
* @returns {Promise<void>}
|
|
91
|
+
*/
|
|
92
|
+
stop(): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Restart the account client - stops and starts again
|
|
95
|
+
* @returns {Promise<void>}
|
|
96
|
+
*/
|
|
97
|
+
restart(): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Refresh device list - discovers new devices and removes missing ones
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
refreshDevices(): Promise<void>;
|
|
103
|
+
#private;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Yoto Account initialization options
|
|
107
|
+
*/
|
|
108
|
+
export type YotoAccountOptions = {
|
|
109
|
+
/**
|
|
110
|
+
* - Yoto API client Options
|
|
111
|
+
*/
|
|
112
|
+
clientOptions: YotoClientConstructorOptions;
|
|
113
|
+
/**
|
|
114
|
+
* - Yoto Device Model Options
|
|
115
|
+
*/
|
|
116
|
+
deviceOptions: YotoDeviceModelOptions;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Error context information
|
|
120
|
+
*/
|
|
121
|
+
export type YotoAccountErrorContext = {
|
|
122
|
+
/**
|
|
123
|
+
* - Error source ('account', 'client', or deviceId)
|
|
124
|
+
*/
|
|
125
|
+
source: string;
|
|
126
|
+
/**
|
|
127
|
+
* - Device ID if error is device-specific
|
|
128
|
+
*/
|
|
129
|
+
deviceId?: string;
|
|
130
|
+
/**
|
|
131
|
+
* - Operation that failed
|
|
132
|
+
*/
|
|
133
|
+
operation?: string;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Started event metadata
|
|
137
|
+
*/
|
|
138
|
+
export type YotoAccountStartedMetadata = {
|
|
139
|
+
/**
|
|
140
|
+
* - Number of devices managed
|
|
141
|
+
*/
|
|
142
|
+
deviceCount: number;
|
|
143
|
+
/**
|
|
144
|
+
* - Array of device IDs
|
|
145
|
+
*/
|
|
146
|
+
devices: string[];
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Event map for YotoAccount
|
|
150
|
+
*/
|
|
151
|
+
export type YotoAccountEventMap = {
|
|
152
|
+
"started": [YotoAccountStartedMetadata];
|
|
153
|
+
"stopped": [];
|
|
154
|
+
"deviceAdded": [string, YotoDeviceModel];
|
|
155
|
+
"deviceRemoved": [string];
|
|
156
|
+
"error": [Error, YotoAccountErrorContext];
|
|
157
|
+
};
|
|
158
|
+
import { EventEmitter } from 'events';
|
|
159
|
+
import { YotoClient } from './api-client.js';
|
|
160
|
+
import { YotoDeviceModel } from './yoto-device.js';
|
|
161
|
+
import type { YotoClientConstructorOptions } from './api-client.js';
|
|
162
|
+
import type { YotoDeviceModelOptions } from './yoto-device.js';
|
|
163
|
+
//# sourceMappingURL=yoto-account.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yoto-account.d.ts","sourceRoot":"","sources":["yoto-account.js"],"names":[],"mappings":"AAaA;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;;;;;GAKG;AAEH;;;;;;;;;GASG;AAMH;;;;;;;;;;;;;;GAcG;AACH;IAOE;;;QAGI;IACJ,qBAFY,kBAAkB,EAS7B;IAMD;;;OAGG;IACH,cAFa,UAAU,CAEc;IAErC;;;OAGG;IACH,eAFa,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAEF;IAEvC;;;OAGG;IACH,eAFa,OAAO,CAEmB;IAEvC;;;OAGG;IACH,mBAFa,OAAO,CAE2B;IAE/C;;;;OAIG;IACH,oBAHW,MAAM,GACJ,eAAe,GAAG,SAAS,CAEmB;IAE3D;;;OAGG;IACH,gBAFa,MAAM,EAAE,CAEsC;IAM3D;;;;OAIG;IACH,SAHa,OAAO,CAAC,IAAI,CAAC,CAyDzB;IAED;;;OAGG;IACH,QAFa,OAAO,CAAC,IAAI,CAAC,CAwCzB;IAED;;;OAGG;IACH,WAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAED;;;OAGG;IACH,kBAFa,OAAO,CAAC,IAAI,CAAC,CAwEzB;;CAqBF;;;;;;;;mBAnUa,4BAA4B;;;;mBAC5B,sBAAsB;;;;;;;;;YAMtB,MAAM;;;;eACN,MAAM;;;;gBACN,MAAM;;;;;;;;;iBAMN,MAAM;;;;aACN,MAAM,EAAE;;;;;kCAKT;IACZ,SAAa,EAAE,CAAC,0BAA0B,CAAC,CAAC;IAC5C,SAAa,EAAE,EAAE,CAAC;IAClB,aAAiB,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,eAAmB,EAAE,CAAC,MAAM,CAAC,CAAC;IAC9B,OAAW,EAAE,CAAC,KAAK,EAAE,uBAAuB,CAAC,CAAA;CAC1C;6BAtCyB,QAAQ;2BACV,iBAAiB;gCACZ,kBAAkB;kDAND,iBAAiB;4CACvB,kBAAkB"}
|