yoto-nodejs-client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +736 -0
- package/bin/auth.d.ts +3 -0
- package/bin/auth.d.ts.map +1 -0
- package/bin/auth.js +130 -0
- package/bin/content.d.ts +3 -0
- package/bin/content.d.ts.map +1 -0
- package/bin/content.js +117 -0
- package/bin/devices.d.ts +3 -0
- package/bin/devices.d.ts.map +1 -0
- package/bin/devices.js +239 -0
- package/bin/groups.d.ts +3 -0
- package/bin/groups.d.ts.map +1 -0
- package/bin/groups.js +80 -0
- package/bin/icons.d.ts +3 -0
- package/bin/icons.d.ts.map +1 -0
- package/bin/icons.js +100 -0
- package/bin/lib/cli-helpers.d.ts +21 -0
- package/bin/lib/cli-helpers.d.ts.map +1 -0
- package/bin/lib/cli-helpers.js +140 -0
- package/bin/lib/token-helpers.d.ts +14 -0
- package/bin/lib/token-helpers.d.ts.map +1 -0
- package/bin/lib/token-helpers.js +151 -0
- package/bin/refresh-token.d.ts +3 -0
- package/bin/refresh-token.d.ts.map +1 -0
- package/bin/refresh-token.js +168 -0
- package/bin/token-info.d.ts +3 -0
- package/bin/token-info.d.ts.map +1 -0
- package/bin/token-info.js +351 -0
- package/index.d.ts +218 -0
- package/index.d.ts.map +1 -0
- package/index.js +689 -0
- package/lib/api-endpoints/auth.d.ts +56 -0
- package/lib/api-endpoints/auth.d.ts.map +1 -0
- package/lib/api-endpoints/auth.js +209 -0
- package/lib/api-endpoints/auth.test.js +27 -0
- package/lib/api-endpoints/constants.d.ts +6 -0
- package/lib/api-endpoints/constants.d.ts.map +1 -0
- package/lib/api-endpoints/constants.js +31 -0
- package/lib/api-endpoints/content.d.ts +275 -0
- package/lib/api-endpoints/content.d.ts.map +1 -0
- package/lib/api-endpoints/content.js +518 -0
- package/lib/api-endpoints/content.test.js +250 -0
- package/lib/api-endpoints/devices.d.ts +202 -0
- package/lib/api-endpoints/devices.d.ts.map +1 -0
- package/lib/api-endpoints/devices.js +404 -0
- package/lib/api-endpoints/devices.test.js +483 -0
- package/lib/api-endpoints/family-library-groups.d.ts +75 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.js +247 -0
- package/lib/api-endpoints/family-library-groups.test.js +272 -0
- package/lib/api-endpoints/family.d.ts +39 -0
- package/lib/api-endpoints/family.d.ts.map +1 -0
- package/lib/api-endpoints/family.js +166 -0
- package/lib/api-endpoints/family.test.js +184 -0
- package/lib/api-endpoints/helpers.d.ts +29 -0
- package/lib/api-endpoints/helpers.d.ts.map +1 -0
- package/lib/api-endpoints/helpers.js +104 -0
- package/lib/api-endpoints/icons.d.ts +62 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -0
- package/lib/api-endpoints/icons.js +201 -0
- package/lib/api-endpoints/icons.test.js +118 -0
- package/lib/api-endpoints/media.d.ts +37 -0
- package/lib/api-endpoints/media.d.ts.map +1 -0
- package/lib/api-endpoints/media.js +155 -0
- package/lib/api-endpoints/test-helpers.d.ts +7 -0
- package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/test-helpers.js +64 -0
- package/lib/mqtt/client.d.ts +124 -0
- package/lib/mqtt/client.d.ts.map +1 -0
- package/lib/mqtt/client.js +558 -0
- package/lib/mqtt/commands.d.ts +69 -0
- package/lib/mqtt/commands.d.ts.map +1 -0
- package/lib/mqtt/commands.js +238 -0
- package/lib/mqtt/factory.d.ts +12 -0
- package/lib/mqtt/factory.d.ts.map +1 -0
- package/lib/mqtt/factory.js +107 -0
- package/lib/mqtt/index.d.ts +5 -0
- package/lib/mqtt/index.d.ts.map +1 -0
- package/lib/mqtt/index.js +81 -0
- package/lib/mqtt/mqtt.test.js +168 -0
- package/lib/mqtt/topics.d.ts +34 -0
- package/lib/mqtt/topics.d.ts.map +1 -0
- package/lib/mqtt/topics.js +295 -0
- package/lib/pkg.cjs +3 -0
- package/lib/pkg.d.cts +70 -0
- package/lib/pkg.d.cts.map +1 -0
- package/lib/token.d.ts +29 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +240 -0
- package/package.json +91 -0
- package/yoto.png +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function getEventsTopic(deviceId: string): string[];
|
|
2
|
+
export function getStatusTopic(deviceId: string): string[];
|
|
3
|
+
export function getResponseTopic(deviceId: string): string;
|
|
4
|
+
export function getSubscriptionTopics(deviceId: string): string[];
|
|
5
|
+
export function getCommandTopic(deviceId: string, resource: string, action?: string): string;
|
|
6
|
+
export function parseTopic(topic: string): {
|
|
7
|
+
deviceId: string;
|
|
8
|
+
messageType: YotoMqttTopicType | "unknown";
|
|
9
|
+
};
|
|
10
|
+
export function getEventsRequestTopic(deviceId: string): string;
|
|
11
|
+
export function getStatusRequestTopic(deviceId: string): string;
|
|
12
|
+
export function getVolumeSetTopic(deviceId: string): string;
|
|
13
|
+
export function getAmbientsSetTopic(deviceId: string): string;
|
|
14
|
+
export function getSleepTimerSetTopic(deviceId: string): string;
|
|
15
|
+
export function getRebootTopic(deviceId: string): string;
|
|
16
|
+
export function getCardStartTopic(deviceId: string): string;
|
|
17
|
+
export function getCardStopTopic(deviceId: string): string;
|
|
18
|
+
export function getCardPauseTopic(deviceId: string): string;
|
|
19
|
+
export function getCardResumeTopic(deviceId: string): string;
|
|
20
|
+
export function getBluetoothOnTopic(deviceId: string): string;
|
|
21
|
+
export function getBluetoothOffTopic(deviceId: string): string;
|
|
22
|
+
export function getBluetoothDeleteBondsTopic(deviceId: string): string;
|
|
23
|
+
export function getBluetoothConnectTopic(deviceId: string): string;
|
|
24
|
+
export function getBluetoothDisconnectTopic(deviceId: string): string;
|
|
25
|
+
export function getBluetoothStateTopic(deviceId: string): string;
|
|
26
|
+
export function getDisplayPreviewTopic(deviceId: string): string;
|
|
27
|
+
export const MQTT_BROKER_URL: "wss://aqrphjqbp3u2z-ats.iot.eu-west-2.amazonaws.com";
|
|
28
|
+
export const MQTT_AUTH_NAME: "PublicJWTAuthorizer";
|
|
29
|
+
export const MQTT_PORT: 443;
|
|
30
|
+
export const MQTT_PROTOCOL: "wss";
|
|
31
|
+
export const MQTT_KEEPALIVE: 300;
|
|
32
|
+
export const MQTT_ALPN_PROTOCOLS: string[];
|
|
33
|
+
export type YotoMqttTopicType = "events" | "status" | "response";
|
|
34
|
+
//# sourceMappingURL=topics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topics.d.ts","sourceRoot":"","sources":["topics.js"],"names":[],"mappings":"AAoDA,yCAHW,MAAM,GACJ,MAAM,EAAE,CAOpB;AAQD,yCAHW,MAAM,GACJ,MAAM,EAAE,CAOpB;AAOD,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAcD,gDAHW,MAAM,GACJ,MAAM,EAAE,CAQpB;AASD,0CALW,MAAM,YACN,MAAM,WACN,MAAM,GACJ,MAAM,CAKlB;AAOD,kCAHW,MAAM,GACJ;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAA;CAAE,CAuB5E;AASD,gDAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,gDAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,4CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,8CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,gDAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,yCAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,4CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,4CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,6CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,8CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,+CAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,uDAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,mDAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,sDAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,iDAHW,MAAM,GACJ,MAAM,CAIlB;AAOD,iDAHW,MAAM,GACJ,MAAM,CAIlB;AAnRD,8BAA+B,qDAAqD,CAAA;AAKpF,6BAA8B,qBAAqB,CAAA;AAKnD,wBAAyB,GAAG,CAAA;AAK5B,4BAA6B,KAAK,CAAA;AAKlC,6BAA8B,GAAG,CAAA;AAKjC,2CAAqD;gCA/BxC,QAAQ,GAAG,QAAQ,GAAG,UAAU"}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MQTT Topics for Yoto Players
|
|
3
|
+
*
|
|
4
|
+
* Topic builders and constants for Yoto MQTT communication
|
|
5
|
+
* @see https://yoto.dev/players-mqtt/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// MQTT Topics: Topic builders and constants
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MQTT topic type for subscriptions
|
|
14
|
+
* @typedef {'events' | 'status' | 'response'} YotoMqttTopicType
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MQTT broker URL for Yoto devices
|
|
19
|
+
*/
|
|
20
|
+
export const MQTT_BROKER_URL = 'wss://aqrphjqbp3u2z-ats.iot.eu-west-2.amazonaws.com'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* MQTT authorizer name for Yoto authentication
|
|
24
|
+
*/
|
|
25
|
+
export const MQTT_AUTH_NAME = 'PublicJWTAuthorizer'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* MQTT connection port
|
|
29
|
+
*/
|
|
30
|
+
export const MQTT_PORT = 443
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MQTT protocol
|
|
34
|
+
*/
|
|
35
|
+
export const MQTT_PROTOCOL = 'wss'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* MQTT keepalive interval in seconds
|
|
39
|
+
*/
|
|
40
|
+
export const MQTT_KEEPALIVE = 300
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* ALPN protocols for AWS IoT
|
|
44
|
+
*/
|
|
45
|
+
export const MQTT_ALPN_PROTOCOLS = ['x-amzn-mqtt-ca']
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the events topics for a device (both old and new formats)
|
|
49
|
+
* Devices publish to both paths, so both are returned
|
|
50
|
+
* @param {string} deviceId - Device ID
|
|
51
|
+
* @returns {string[]} Events topics [old format, new format]
|
|
52
|
+
*/
|
|
53
|
+
export function getEventsTopic (deviceId) {
|
|
54
|
+
return [
|
|
55
|
+
`device/${deviceId}/events`, // Old path (still published)
|
|
56
|
+
`device/${deviceId}/data/events` // New path (also published)
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the status topics for a device (both old and new formats)
|
|
62
|
+
* Devices publish to both paths, so both are returned
|
|
63
|
+
* @param {string} deviceId - Device ID
|
|
64
|
+
* @returns {string[]} Status topics [old format, new format]
|
|
65
|
+
*/
|
|
66
|
+
export function getStatusTopic (deviceId) {
|
|
67
|
+
return [
|
|
68
|
+
`device/${deviceId}/status`, // Old path (still published)
|
|
69
|
+
`device/${deviceId}/data/status` // New path (also published)
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the response topic for a device (subscribe)
|
|
75
|
+
* @param {string} deviceId - Device ID
|
|
76
|
+
* @returns {string} Response topic
|
|
77
|
+
*/
|
|
78
|
+
export function getResponseTopic (deviceId) {
|
|
79
|
+
return `device/${deviceId}/response`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get all subscription topics for a device
|
|
84
|
+
*
|
|
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
|
|
88
|
+
*
|
|
89
|
+
* This ensures all messages are received regardless of which path the device uses.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} deviceId - Device ID
|
|
92
|
+
* @returns {string[]} Array of topics to subscribe to (includes both old and new formats)
|
|
93
|
+
*/
|
|
94
|
+
export function getSubscriptionTopics (deviceId) {
|
|
95
|
+
return [
|
|
96
|
+
...getEventsTopic(deviceId), // Both events topics
|
|
97
|
+
...getStatusTopic(deviceId), // Both status topics
|
|
98
|
+
getResponseTopic(deviceId) // Response topic (no old/new variants)
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get a command topic for a device (publish)
|
|
104
|
+
* @param {string} deviceId - Device ID
|
|
105
|
+
* @param {string} resource - Command resource (e.g., 'volume', 'ambients', 'card')
|
|
106
|
+
* @param {string} [action] - Command action (e.g., 'set', 'start', 'stop')
|
|
107
|
+
* @returns {string} Command topic
|
|
108
|
+
*/
|
|
109
|
+
export function getCommandTopic (deviceId, resource, action) {
|
|
110
|
+
const base = `device/${deviceId}/command/${resource}`
|
|
111
|
+
return action ? `${base}/${action}` : base
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse a topic string to extract device ID and message type
|
|
116
|
+
* @param {string} topic - Full MQTT topic string
|
|
117
|
+
* @returns {{ deviceId: string, messageType: YotoMqttTopicType | 'unknown' }} Parsed topic info
|
|
118
|
+
*/
|
|
119
|
+
export function parseTopic (topic) {
|
|
120
|
+
const parts = topic.split('/')
|
|
121
|
+
|
|
122
|
+
if (parts.length < 3 || parts[0] !== 'device') {
|
|
123
|
+
return { deviceId: '', messageType: 'unknown' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const deviceId = parts[1] || ''
|
|
127
|
+
|
|
128
|
+
// Handle both old format (device/{id}/events) and new format (device/{id}/data/events)
|
|
129
|
+
let messageType = parts[2]
|
|
130
|
+
if (messageType === 'data' && parts.length >= 4) {
|
|
131
|
+
messageType = parts[3] // Get the actual message type after /data/
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validate message type
|
|
135
|
+
if (messageType !== 'events' && messageType !== 'status' && messageType !== 'response') {
|
|
136
|
+
return { deviceId, messageType: 'unknown' }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { deviceId, messageType: /** @type {YotoMqttTopicType} */ (messageType) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Command topic builders for specific commands
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the events request command topic
|
|
146
|
+
* @param {string} deviceId - Device ID
|
|
147
|
+
* @returns {string} Events request topic
|
|
148
|
+
*/
|
|
149
|
+
export function getEventsRequestTopic (deviceId) {
|
|
150
|
+
return getCommandTopic(deviceId, 'events', 'request')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the status request command topic
|
|
155
|
+
* @param {string} deviceId - Device ID
|
|
156
|
+
* @returns {string} Status request topic
|
|
157
|
+
*/
|
|
158
|
+
export function getStatusRequestTopic (deviceId) {
|
|
159
|
+
return getCommandTopic(deviceId, 'status', 'request')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the volume set command topic
|
|
164
|
+
* @param {string} deviceId - Device ID
|
|
165
|
+
* @returns {string} Volume set topic
|
|
166
|
+
*/
|
|
167
|
+
export function getVolumeSetTopic (deviceId) {
|
|
168
|
+
return getCommandTopic(deviceId, 'volume', 'set')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the ambients set command topic
|
|
173
|
+
* @param {string} deviceId - Device ID
|
|
174
|
+
* @returns {string} Ambients set topic
|
|
175
|
+
*/
|
|
176
|
+
export function getAmbientsSetTopic (deviceId) {
|
|
177
|
+
return getCommandTopic(deviceId, 'ambients', 'set')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the sleep timer set command topic
|
|
182
|
+
* @param {string} deviceId - Device ID
|
|
183
|
+
* @returns {string} Sleep timer set topic
|
|
184
|
+
*/
|
|
185
|
+
export function getSleepTimerSetTopic (deviceId) {
|
|
186
|
+
return getCommandTopic(deviceId, 'sleep-timer', 'set')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get the reboot command topic
|
|
191
|
+
* @param {string} deviceId - Device ID
|
|
192
|
+
* @returns {string} Reboot topic
|
|
193
|
+
*/
|
|
194
|
+
export function getRebootTopic (deviceId) {
|
|
195
|
+
return getCommandTopic(deviceId, 'reboot')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get the card start command topic
|
|
200
|
+
* @param {string} deviceId - Device ID
|
|
201
|
+
* @returns {string} Card start topic
|
|
202
|
+
*/
|
|
203
|
+
export function getCardStartTopic (deviceId) {
|
|
204
|
+
return getCommandTopic(deviceId, 'card', 'start')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the card stop command topic
|
|
209
|
+
* @param {string} deviceId - Device ID
|
|
210
|
+
* @returns {string} Card stop topic
|
|
211
|
+
*/
|
|
212
|
+
export function getCardStopTopic (deviceId) {
|
|
213
|
+
return getCommandTopic(deviceId, 'card', 'stop')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get the card pause command topic
|
|
218
|
+
* @param {string} deviceId - Device ID
|
|
219
|
+
* @returns {string} Card pause topic
|
|
220
|
+
*/
|
|
221
|
+
export function getCardPauseTopic (deviceId) {
|
|
222
|
+
return getCommandTopic(deviceId, 'card', 'pause')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get the card resume command topic
|
|
227
|
+
* @param {string} deviceId - Device ID
|
|
228
|
+
* @returns {string} Card resume topic
|
|
229
|
+
*/
|
|
230
|
+
export function getCardResumeTopic (deviceId) {
|
|
231
|
+
return getCommandTopic(deviceId, 'card', 'resume')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get the bluetooth on command topic
|
|
236
|
+
* @param {string} deviceId - Device ID
|
|
237
|
+
* @returns {string} Bluetooth on topic
|
|
238
|
+
*/
|
|
239
|
+
export function getBluetoothOnTopic (deviceId) {
|
|
240
|
+
return getCommandTopic(deviceId, 'bluetooth', 'on')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the bluetooth off command topic
|
|
245
|
+
* @param {string} deviceId - Device ID
|
|
246
|
+
* @returns {string} Bluetooth off topic
|
|
247
|
+
*/
|
|
248
|
+
export function getBluetoothOffTopic (deviceId) {
|
|
249
|
+
return getCommandTopic(deviceId, 'bluetooth', 'off')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get the bluetooth delete bonds command topic
|
|
254
|
+
* @param {string} deviceId - Device ID
|
|
255
|
+
* @returns {string} Bluetooth delete bonds topic
|
|
256
|
+
*/
|
|
257
|
+
export function getBluetoothDeleteBondsTopic (deviceId) {
|
|
258
|
+
return getCommandTopic(deviceId, 'bluetooth', 'delete-bonds')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get the bluetooth connect command topic
|
|
263
|
+
* @param {string} deviceId - Device ID
|
|
264
|
+
* @returns {string} Bluetooth connect topic
|
|
265
|
+
*/
|
|
266
|
+
export function getBluetoothConnectTopic (deviceId) {
|
|
267
|
+
return getCommandTopic(deviceId, 'bluetooth', 'connect')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get the bluetooth disconnect command topic
|
|
272
|
+
* @param {string} deviceId - Device ID
|
|
273
|
+
* @returns {string} Bluetooth disconnect topic
|
|
274
|
+
*/
|
|
275
|
+
export function getBluetoothDisconnectTopic (deviceId) {
|
|
276
|
+
return getCommandTopic(deviceId, 'bluetooth', 'disconnect')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the bluetooth state command topic
|
|
281
|
+
* @param {string} deviceId - Device ID
|
|
282
|
+
* @returns {string} Bluetooth state topic
|
|
283
|
+
*/
|
|
284
|
+
export function getBluetoothStateTopic (deviceId) {
|
|
285
|
+
return getCommandTopic(deviceId, 'bluetooth', 'state')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get the display preview command topic
|
|
290
|
+
* @param {string} deviceId - Device ID
|
|
291
|
+
* @returns {string} Display preview topic
|
|
292
|
+
*/
|
|
293
|
+
export function getDisplayPreviewTopic (deviceId) {
|
|
294
|
+
return getCommandTopic(deviceId, 'display', 'preview')
|
|
295
|
+
}
|
package/lib/pkg.cjs
ADDED
package/lib/pkg.d.cts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const pkg: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
version: string;
|
|
5
|
+
author: string;
|
|
6
|
+
bugs: {
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
dependencies: {
|
|
10
|
+
"jwt-decode": string;
|
|
11
|
+
mqtt: string;
|
|
12
|
+
undici: string;
|
|
13
|
+
};
|
|
14
|
+
devDependencies: {
|
|
15
|
+
"@types/node": string;
|
|
16
|
+
"@voxpelli/tsconfig": string;
|
|
17
|
+
argsclopts: string;
|
|
18
|
+
"auto-changelog": string;
|
|
19
|
+
c8: string;
|
|
20
|
+
"gh-release": string;
|
|
21
|
+
neostandard: string;
|
|
22
|
+
"npm-run-all2": string;
|
|
23
|
+
typescript: string;
|
|
24
|
+
};
|
|
25
|
+
engines: {
|
|
26
|
+
node: string;
|
|
27
|
+
npm: string;
|
|
28
|
+
};
|
|
29
|
+
homepage: string;
|
|
30
|
+
keywords: string[];
|
|
31
|
+
license: string;
|
|
32
|
+
type: string;
|
|
33
|
+
module: string;
|
|
34
|
+
main: string;
|
|
35
|
+
types: string;
|
|
36
|
+
files: string[];
|
|
37
|
+
bin: {
|
|
38
|
+
"yoto-auth": string;
|
|
39
|
+
"yoto-token-info": string;
|
|
40
|
+
};
|
|
41
|
+
repository: {
|
|
42
|
+
type: string;
|
|
43
|
+
url: string;
|
|
44
|
+
};
|
|
45
|
+
scripts: {
|
|
46
|
+
prepublishOnly: string;
|
|
47
|
+
postpublish: string;
|
|
48
|
+
test: string;
|
|
49
|
+
"test:lint": string;
|
|
50
|
+
"test:tsc": string;
|
|
51
|
+
"test:node-test": string;
|
|
52
|
+
version: string;
|
|
53
|
+
"version:changelog": string;
|
|
54
|
+
"version:git": string;
|
|
55
|
+
build: string;
|
|
56
|
+
"build:declaration": string;
|
|
57
|
+
clean: string;
|
|
58
|
+
"clean:declarations-top": string;
|
|
59
|
+
"clean:declarations-lib": string;
|
|
60
|
+
"clean:declarations-bin": string;
|
|
61
|
+
};
|
|
62
|
+
funding: {
|
|
63
|
+
type: string;
|
|
64
|
+
url: string;
|
|
65
|
+
};
|
|
66
|
+
c8: {
|
|
67
|
+
reporter: string[];
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=pkg.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkg.d.cts","sourceRoot":"","sources":["pkg.cjs"],"names":[],"mappings":""}
|
package/lib/token.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class RefreshableToken extends EventEmitter<RefreshableTokenEventMap> {
|
|
2
|
+
constructor({ clientId, refreshToken, accessToken, bufferSeconds }: RefreshableTokenOpts);
|
|
3
|
+
getAccessToken(): Promise<string>;
|
|
4
|
+
isValid(): boolean;
|
|
5
|
+
getExpiresAt(): number;
|
|
6
|
+
getTimeRemaining(): number;
|
|
7
|
+
refresh(): Promise<RefreshSuccessEvent>;
|
|
8
|
+
#private;
|
|
9
|
+
}
|
|
10
|
+
export type RefreshableTokenOpts = {
|
|
11
|
+
clientId: string;
|
|
12
|
+
refreshToken: string;
|
|
13
|
+
accessToken: string;
|
|
14
|
+
bufferSeconds?: number;
|
|
15
|
+
};
|
|
16
|
+
export type RefreshSuccessEvent = {
|
|
17
|
+
clientId: string;
|
|
18
|
+
accessToken: string;
|
|
19
|
+
refreshToken: string;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
};
|
|
22
|
+
export type RefreshableTokenEventMap = {
|
|
23
|
+
"refresh:start": [];
|
|
24
|
+
"refresh:success": [RefreshSuccessEvent];
|
|
25
|
+
"refresh:error": [Error];
|
|
26
|
+
"invalid": [Error];
|
|
27
|
+
};
|
|
28
|
+
import { EventEmitter } from 'node:events';
|
|
29
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +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"}
|
package/lib/token.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { jwtDecode } from 'jwt-decode'
|
|
3
|
+
import { exchangeToken } from './api-endpoints/auth.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} RefreshableTokenOpts
|
|
7
|
+
* @property {string} clientId - OAuth client ID
|
|
8
|
+
* @property {string} refreshToken - OAuth refresh token
|
|
9
|
+
* @property {string} accessToken - Initial OAuth access token (JWT)
|
|
10
|
+
* @property {number} [bufferSeconds=30] - Seconds before expiration to consider token expired
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} RefreshSuccessEvent
|
|
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
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Event map for RefreshableToken
|
|
23
|
+
* @typedef {{
|
|
24
|
+
* 'refresh:start': [],
|
|
25
|
+
* 'refresh:success': [RefreshSuccessEvent],
|
|
26
|
+
* 'refresh:error': [Error],
|
|
27
|
+
* 'invalid': [Error]
|
|
28
|
+
* }} RefreshableTokenEventMap
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A refreshable OAuth token that automatically refreshes when expired.
|
|
33
|
+
* Handles in-flight refresh deduplication to prevent multiple concurrent refresh requests.
|
|
34
|
+
*
|
|
35
|
+
* Events:
|
|
36
|
+
* - 'refresh:start' - Emitted when token refresh begins
|
|
37
|
+
* - 'refresh:success' - Emitted when token refresh succeeds, passes { clientId, accessToken, refreshToken, expiresAt }
|
|
38
|
+
* - 'refresh:error' - Emitted when token refresh fails (transient errors), passes error
|
|
39
|
+
* - 'invalid' - Emitted when refresh token is permanently invalid, passes error
|
|
40
|
+
*
|
|
41
|
+
* @extends {EventEmitter<RefreshableTokenEventMap>}
|
|
42
|
+
*/
|
|
43
|
+
export class RefreshableToken extends EventEmitter {
|
|
44
|
+
/** @type {string} */
|
|
45
|
+
#clientId
|
|
46
|
+
/** @type {string} */
|
|
47
|
+
#refreshToken
|
|
48
|
+
/** @type {string} */
|
|
49
|
+
#accessToken
|
|
50
|
+
/** @type {number} - Unix timestamp in seconds (from JWT exp claim) */
|
|
51
|
+
#expiresAt
|
|
52
|
+
/** @type {number} - Buffer time in seconds before expiration */
|
|
53
|
+
#bufferSeconds
|
|
54
|
+
/** @type {boolean} */
|
|
55
|
+
#invalid = false
|
|
56
|
+
/** @type {Promise<string> | null} */
|
|
57
|
+
#inFlightRefresh = null
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {RefreshableTokenOpts} opts
|
|
61
|
+
*/
|
|
62
|
+
constructor ({ clientId, refreshToken, accessToken, bufferSeconds = 30 }) {
|
|
63
|
+
super()
|
|
64
|
+
this.#clientId = clientId
|
|
65
|
+
this.#refreshToken = refreshToken
|
|
66
|
+
this.#accessToken = accessToken
|
|
67
|
+
this.#bufferSeconds = bufferSeconds
|
|
68
|
+
|
|
69
|
+
// Decode the JWT to get expiration
|
|
70
|
+
try {
|
|
71
|
+
const decoded = jwtDecode(accessToken)
|
|
72
|
+
if (!decoded.exp) {
|
|
73
|
+
throw new Error('Access token does not contain expiration claim (exp)')
|
|
74
|
+
}
|
|
75
|
+
this.#expiresAt = decoded.exp
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const error = /** @type {Error} */ (err)
|
|
78
|
+
throw new Error(`Failed to decode access token: ${error.message}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a valid access token, refreshing if necessary.
|
|
84
|
+
* @returns {Promise<string>} Valid access token
|
|
85
|
+
* @throws {Error} If token is invalid or refresh fails
|
|
86
|
+
*/
|
|
87
|
+
async getAccessToken () {
|
|
88
|
+
// Check if token has been marked invalid
|
|
89
|
+
if (this.#invalid) {
|
|
90
|
+
throw new Error('Token is invalid. Refresh token has expired or been revoked.')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const now = Math.floor(Date.now() / 1000)
|
|
94
|
+
const needsRefresh = now >= (this.#expiresAt - this.#bufferSeconds)
|
|
95
|
+
|
|
96
|
+
if (!needsRefresh) {
|
|
97
|
+
return this.#accessToken
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return await this.#refreshAccessToken()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Refresh the access token using the refresh token.
|
|
105
|
+
* Handles in-flight request deduplication.
|
|
106
|
+
* @returns {Promise<string>} New access token
|
|
107
|
+
*/
|
|
108
|
+
async #refreshAccessToken () {
|
|
109
|
+
// If a refresh is already in flight, await the existing one
|
|
110
|
+
if (this.#inFlightRefresh) {
|
|
111
|
+
return await this.#inFlightRefresh
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create the refresh promise and set it on the class variable
|
|
115
|
+
try {
|
|
116
|
+
this.#inFlightRefresh = this.#performRefresh()
|
|
117
|
+
const newAccessToken = await this.#inFlightRefresh
|
|
118
|
+
return newAccessToken
|
|
119
|
+
} finally {
|
|
120
|
+
// Clear the in-flight refresh once done
|
|
121
|
+
this.#inFlightRefresh = null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Perform the actual token refresh.
|
|
127
|
+
* @returns {Promise<string>} New access token
|
|
128
|
+
*/
|
|
129
|
+
async #performRefresh () {
|
|
130
|
+
this.emit('refresh:start')
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const tokens = await exchangeToken({
|
|
134
|
+
grantType: 'refresh_token',
|
|
135
|
+
refreshToken: this.#refreshToken,
|
|
136
|
+
clientId: this.#clientId
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Update the access token and expiration
|
|
140
|
+
this.#accessToken = tokens.access_token
|
|
141
|
+
|
|
142
|
+
// If we got a new refresh token, update it
|
|
143
|
+
if (tokens.refresh_token) {
|
|
144
|
+
this.#refreshToken = tokens.refresh_token
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Decode the new token to get expiration
|
|
148
|
+
const decoded = jwtDecode(tokens.access_token)
|
|
149
|
+
if (!decoded.exp) {
|
|
150
|
+
throw new Error('Refreshed access token does not contain expiration claim (exp)')
|
|
151
|
+
}
|
|
152
|
+
this.#expiresAt = decoded.exp
|
|
153
|
+
|
|
154
|
+
this.emit('refresh:success', {
|
|
155
|
+
clientId: this.#clientId,
|
|
156
|
+
accessToken: this.#accessToken,
|
|
157
|
+
refreshToken: this.#refreshToken,
|
|
158
|
+
expiresAt: this.#expiresAt
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return this.#accessToken
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const error = /** @type {any} */ (err)
|
|
164
|
+
|
|
165
|
+
// Check for errors that indicate the refresh token is invalid
|
|
166
|
+
const invalidRefreshErrors = [
|
|
167
|
+
'invalid_grant',
|
|
168
|
+
'invalid_token',
|
|
169
|
+
'expired_token',
|
|
170
|
+
'token_expired',
|
|
171
|
+
'refresh_token_expired'
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
if (error.body?.error && invalidRefreshErrors.includes(error.body.error)) {
|
|
175
|
+
// Mark this token as permanently invalid
|
|
176
|
+
this.#invalid = true
|
|
177
|
+
const statusCode = error.statusCode ? ` (${error.statusCode})` : ''
|
|
178
|
+
const invalidError = new Error(`Refresh token is invalid or expired${statusCode}: ${error.body.error}${error.body.error_description ? ` - ${error.body.error_description}` : ''}`)
|
|
179
|
+
this.emit('invalid', invalidError)
|
|
180
|
+
throw invalidError
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// For other errors, rethrow without marking as invalid (might be transient)
|
|
184
|
+
this.emit('refresh:error', error)
|
|
185
|
+
throw error
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if the token is currently valid (not expired and not marked invalid).
|
|
191
|
+
* @returns {boolean} True if token is valid
|
|
192
|
+
*/
|
|
193
|
+
isValid () {
|
|
194
|
+
if (this.#invalid) {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const now = Math.floor(Date.now() / 1000)
|
|
199
|
+
return now < (this.#expiresAt - this.#bufferSeconds)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get the expiration timestamp of the current access token.
|
|
204
|
+
* @returns {number} Unix timestamp (seconds since epoch)
|
|
205
|
+
*/
|
|
206
|
+
getExpiresAt () {
|
|
207
|
+
return this.#expiresAt
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the time remaining until token expiration.
|
|
212
|
+
* @returns {number} Seconds until expiration (may be negative if expired)
|
|
213
|
+
*/
|
|
214
|
+
getTimeRemaining () {
|
|
215
|
+
const now = Math.floor(Date.now() / 1000)
|
|
216
|
+
return this.#expiresAt - now
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Manually trigger a token refresh, regardless of expiration status.
|
|
221
|
+
* Useful for proactive refresh or testing.
|
|
222
|
+
* @returns {Promise<RefreshSuccessEvent>} Token information including clientId, accessToken, refreshToken, and expiresAt
|
|
223
|
+
* @throws {Error} If token is invalid or refresh fails
|
|
224
|
+
*/
|
|
225
|
+
async refresh () {
|
|
226
|
+
// Check if token has been marked invalid
|
|
227
|
+
if (this.#invalid) {
|
|
228
|
+
throw new Error('Token is invalid. Refresh token has expired or been revoked.')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await this.#refreshAccessToken()
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
clientId: this.#clientId,
|
|
235
|
+
accessToken: this.#accessToken,
|
|
236
|
+
refreshToken: this.#refreshToken,
|
|
237
|
+
expiresAt: this.#expiresAt
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|