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.
Files changed (92) hide show
  1. package/README.md +523 -30
  2. package/bin/auth.js +36 -46
  3. package/bin/content.js +0 -0
  4. package/bin/device-model.d.ts +3 -0
  5. package/bin/device-model.d.ts.map +1 -0
  6. package/bin/device-model.js +360 -0
  7. package/bin/device-tui.TODO.md +125 -0
  8. package/bin/device-tui.d.ts +31 -0
  9. package/bin/device-tui.d.ts.map +1 -0
  10. package/bin/device-tui.js +1123 -0
  11. package/bin/devices.js +166 -28
  12. package/bin/groups.js +0 -0
  13. package/bin/icons.js +0 -0
  14. package/bin/lib/cli-helpers.d.ts +33 -1
  15. package/bin/lib/cli-helpers.d.ts.map +1 -1
  16. package/bin/lib/cli-helpers.js +5 -5
  17. package/bin/lib/token-helpers.d.ts +32 -0
  18. package/bin/lib/token-helpers.d.ts.map +1 -1
  19. package/bin/refresh-token.js +6 -6
  20. package/bin/token-info.js +3 -3
  21. package/index.d.ts +4 -217
  22. package/index.d.ts.map +1 -1
  23. package/index.js +11 -689
  24. package/lib/api-client.d.ts +576 -0
  25. package/lib/api-client.d.ts.map +1 -0
  26. package/lib/api-client.js +681 -0
  27. package/lib/api-endpoints/auth.d.ts +280 -4
  28. package/lib/api-endpoints/auth.d.ts.map +1 -1
  29. package/lib/api-endpoints/auth.js +224 -7
  30. package/lib/api-endpoints/auth.test.js +54 -2
  31. package/lib/api-endpoints/constants.d.ts +30 -2
  32. package/lib/api-endpoints/constants.d.ts.map +1 -1
  33. package/lib/api-endpoints/constants.js +17 -10
  34. package/lib/api-endpoints/content.d.ts +760 -0
  35. package/lib/api-endpoints/content.d.ts.map +1 -1
  36. package/lib/api-endpoints/content.test.js +1 -1
  37. package/lib/api-endpoints/devices.d.ts +917 -48
  38. package/lib/api-endpoints/devices.d.ts.map +1 -1
  39. package/lib/api-endpoints/devices.js +114 -52
  40. package/lib/api-endpoints/devices.test.js +1 -1
  41. package/lib/api-endpoints/endpoint-test-helpers.d.ts +28 -0
  42. package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -0
  43. package/lib/api-endpoints/family-library-groups.d.ts +187 -0
  44. package/lib/api-endpoints/family-library-groups.d.ts.map +1 -1
  45. package/lib/api-endpoints/family-library-groups.test.js +1 -1
  46. package/lib/api-endpoints/family.d.ts +88 -0
  47. package/lib/api-endpoints/family.d.ts.map +1 -1
  48. package/lib/api-endpoints/family.test.js +1 -1
  49. package/lib/api-endpoints/helpers.d.ts +37 -3
  50. package/lib/api-endpoints/helpers.d.ts.map +1 -1
  51. package/lib/api-endpoints/icons.d.ts +196 -0
  52. package/lib/api-endpoints/icons.d.ts.map +1 -1
  53. package/lib/api-endpoints/icons.test.js +1 -1
  54. package/lib/api-endpoints/media.d.ts +83 -0
  55. package/lib/api-endpoints/media.d.ts.map +1 -1
  56. package/lib/helpers/power-state.d.ts +53 -0
  57. package/lib/helpers/power-state.d.ts.map +1 -0
  58. package/lib/helpers/power-state.js +73 -0
  59. package/lib/helpers/power-state.test.js +100 -0
  60. package/lib/helpers/temperature.d.ts +24 -0
  61. package/lib/helpers/temperature.d.ts.map +1 -0
  62. package/lib/helpers/temperature.js +61 -0
  63. package/lib/helpers/temperature.test.js +58 -0
  64. package/lib/helpers/typed-keys.d.ts +7 -0
  65. package/lib/helpers/typed-keys.d.ts.map +1 -0
  66. package/lib/helpers/typed-keys.js +8 -0
  67. package/lib/mqtt/client.d.ts +610 -7
  68. package/lib/mqtt/client.d.ts.map +1 -1
  69. package/lib/mqtt/client.js +213 -31
  70. package/lib/mqtt/commands.d.ts +195 -0
  71. package/lib/mqtt/commands.d.ts.map +1 -1
  72. package/lib/mqtt/factory.d.ts +62 -1
  73. package/lib/mqtt/factory.d.ts.map +1 -1
  74. package/lib/mqtt/factory.js +27 -5
  75. package/lib/mqtt/mqtt.test.js +85 -28
  76. package/lib/mqtt/topics.d.ts +186 -1
  77. package/lib/mqtt/topics.d.ts.map +1 -1
  78. package/lib/mqtt/topics.js +54 -20
  79. package/lib/pkg.d.cts +9 -0
  80. package/lib/token.d.ts +106 -3
  81. package/lib/token.d.ts.map +1 -1
  82. package/lib/token.js +30 -23
  83. package/lib/yoto-account.d.ts +163 -0
  84. package/lib/yoto-account.d.ts.map +1 -0
  85. package/lib/yoto-account.js +340 -0
  86. package/lib/yoto-device.d.ts +656 -0
  87. package/lib/yoto-device.d.ts.map +1 -0
  88. package/lib/yoto-device.js +2850 -0
  89. package/package.json +22 -15
  90. package/lib/api-endpoints/test-helpers.d.ts +0 -7
  91. package/lib/api-endpoints/test-helpers.d.ts.map +0 -1
  92. /package/lib/api-endpoints/{test-helpers.js → endpoint-test-helpers.js} +0 -0
@@ -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 (both old and new formats)
49
- * Devices publish to both paths, so both are returned
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 [old format, new format]
54
+ * @returns {string[]} Events topics
52
55
  */
53
56
  export function getEventsTopic (deviceId) {
54
57
  return [
55
- `device/${deviceId}/events`, // Old path (still published)
56
- `device/${deviceId}/data/events` // New path (also published)
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 (both old and new formats)
62
- * Devices publish to both paths, so both are returned
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 [old format, new format]
83
+ * @returns {string[]} Status topics (both documented and legacy)
65
84
  */
66
85
  export function getStatusTopic (deviceId) {
67
86
  return [
68
- `device/${deviceId}/status`, // Old path (still published)
69
- `device/${deviceId}/data/status` // New path (also published)
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 both old and new topic formats because Yoto devices publish to both:
86
- * - Old format: device/{id}/events, device/{id}/status
87
- * - New format: device/{id}/data/events, device/{id}/data/status
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
- * This ensures all messages are received regardless of which path the device uses.
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 (includes both old and new formats)
115
+ * @returns {string[]} Array of topics to subscribe to
93
116
  */
94
117
  export function getSubscriptionTopics (deviceId) {
95
118
  return [
96
- ...getEventsTopic(deviceId), // Both events topics
97
- ...getStatusTopic(deviceId), // Both status topics
98
- getResponseTopic(deviceId) // Response topic (no old/new variants)
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 old format (device/{id}/events) and new format (device/{id}/data/events)
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
- accessToken: string;
19
- refreshToken: string;
20
- expiresAt: number;
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];
@@ -1 +1 @@
1
- {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["token.js"],"names":[],"mappings":"AA0CA;IAmBE,oEAFW,oBAAoB,EAoB9B;IAOD,kBAHa,OAAO,CAAC,MAAM,CAAC,CAiB3B;IA4FD,WAFa,OAAO,CASnB;IAMD,gBAFa,MAAM,CAIlB;IAMD,oBAFa,MAAM,CAKlB;IAQD,WAHa,OAAO,CAAC,mBAAmB,CAAC,CAiBxC;;CACF;;cAzOa,MAAM;kBACN,MAAM;iBACN,MAAM;oBACN,MAAM;;;cAKN,MAAM;iBACN,MAAM;kBACN,MAAM;eACN,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;6BA3ByB,aAAa"}
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} accessToken - The new access token
17
- * @property {string} refreshToken - The refresh token (may be updated)
18
- * @property {number} expiresAt - Unix timestamp in seconds when token expires
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<string> | null} */
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
- return await this.#refreshAccessToken()
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<string>} New access token
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
- const newAccessToken = await this.#inFlightRefresh
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<string>} New access token
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
- this.emit('refresh:success', {
162
+ /* @type {RefreshSuccessEvent} */
163
+ const eventPayload = {
155
164
  clientId: this.#clientId,
156
- accessToken: this.#accessToken,
157
- refreshToken: this.#refreshToken,
158
- expiresAt: this.#expiresAt
159
- })
165
+ updatedAccessToken: this.#accessToken,
166
+ updatedRefreshToken: this.#refreshToken,
167
+ updatedExpiresAt: this.#expiresAt,
168
+ prevAccessToken,
169
+ prevRefreshToken,
170
+ prevExpiresAt,
171
+ }
160
172
 
161
- return this.#accessToken
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"}