homebridge-yoto 0.0.28 → 0.0.32

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/lib/auth.js DELETED
@@ -1,237 +0,0 @@
1
- /**
2
- * @fileoverview OAuth2 Device Authorization Flow implementation for Yoto API
3
- */
4
-
5
- /** @import { Logger } from 'homebridge' */
6
- /** @import { YotoApiTokenResponse, YotoApiDeviceCodeResponse } from './types.js' */
7
-
8
- import {
9
- YOTO_OAUTH_DEVICE_CODE_URL,
10
- YOTO_OAUTH_TOKEN_URL,
11
- OAUTH_CLIENT_ID,
12
- OAUTH_AUDIENCE,
13
- OAUTH_SCOPE,
14
- OAUTH_POLLING_INTERVAL,
15
- OAUTH_DEVICE_CODE_TIMEOUT,
16
- ERROR_MESSAGES,
17
- LOG_PREFIX
18
- } from './constants.js'
19
-
20
- /**
21
- * OAuth2 authentication handler for Yoto API
22
- */
23
- export class YotoAuth {
24
- /**
25
- * @param {Logger} log - Homebridge logger
26
- * @param {string} [clientId] - OAuth client ID (uses default if not provided)
27
- */
28
- constructor (log, clientId) {
29
- this.log = log
30
- this.clientId = clientId || OAUTH_CLIENT_ID
31
- }
32
-
33
- /**
34
- * Initiate device authorization flow
35
- * @returns {Promise<YotoApiDeviceCodeResponse>}
36
- */
37
- async initiateDeviceFlow () {
38
- this.log.debug(LOG_PREFIX.AUTH, 'Initiating device authorization flow...')
39
- this.log.debug(LOG_PREFIX.AUTH, `Client ID: ${this.clientId}`)
40
- this.log.debug(LOG_PREFIX.AUTH, `Endpoint: ${YOTO_OAUTH_DEVICE_CODE_URL}`)
41
- this.log.debug(LOG_PREFIX.AUTH, `Scope: ${OAUTH_SCOPE}`)
42
- this.log.debug(LOG_PREFIX.AUTH, `Audience: ${OAUTH_AUDIENCE}`)
43
-
44
- try {
45
- const params = new URLSearchParams({
46
- client_id: this.clientId,
47
- scope: OAUTH_SCOPE,
48
- audience: OAUTH_AUDIENCE
49
- })
50
- this.log.debug(LOG_PREFIX.AUTH, 'Request body:', params.toString())
51
-
52
- const response = await fetch(YOTO_OAUTH_DEVICE_CODE_URL, {
53
- method: 'POST',
54
- headers: {
55
- 'Content-Type': 'application/x-www-form-urlencoded'
56
- },
57
- body: params
58
- })
59
-
60
- this.log.debug(LOG_PREFIX.AUTH, `Response status: ${response.status}`)
61
- this.log.debug(LOG_PREFIX.AUTH, 'Response headers:', Object.fromEntries(response.headers.entries()))
62
-
63
- if (!response.ok) {
64
- const errorText = await response.text()
65
- this.log.error(LOG_PREFIX.AUTH, `Device code request failed with status ${response.status}`)
66
- this.log.error(LOG_PREFIX.AUTH, `Error response: ${errorText}`)
67
- throw new Error(`Device code request failed: ${response.status} ${errorText}`)
68
- }
69
-
70
- const data = /** @type {YotoApiDeviceCodeResponse} */ (await response.json())
71
-
72
- this.log.info(LOG_PREFIX.AUTH, '='.repeat(60))
73
- this.log.info(LOG_PREFIX.AUTH, 'YOTO AUTHENTICATION REQUIRED')
74
- this.log.info(LOG_PREFIX.AUTH, '='.repeat(60))
75
- this.log.info(LOG_PREFIX.AUTH, '')
76
- this.log.info(LOG_PREFIX.AUTH, `1. Visit: ${data.verification_uri}`)
77
- this.log.info(LOG_PREFIX.AUTH, `2. Enter code: ${data.user_code}`)
78
- this.log.info(LOG_PREFIX.AUTH, '')
79
- this.log.info(LOG_PREFIX.AUTH, `Or visit: ${data.verification_uri_complete}`)
80
- this.log.info(LOG_PREFIX.AUTH, '')
81
- this.log.info(LOG_PREFIX.AUTH, `Code expires in ${Math.floor(data.expires_in / 60)} minutes`)
82
- this.log.info(LOG_PREFIX.AUTH, '='.repeat(60))
83
-
84
- return data
85
- } catch (error) {
86
- this.log.error(LOG_PREFIX.AUTH, 'Failed to initiate device flow:', error)
87
- throw error
88
- }
89
- }
90
-
91
- /**
92
- * Poll for authorization completion
93
- * @param {string} deviceCode - Device code from initiation
94
- * @returns {Promise<YotoApiTokenResponse>}
95
- */
96
- async pollForAuthorization (deviceCode) {
97
- const startTime = Date.now()
98
- const timeout = OAUTH_DEVICE_CODE_TIMEOUT
99
-
100
- this.log.debug(LOG_PREFIX.AUTH, 'Waiting for user authorization...')
101
-
102
- while (Date.now() - startTime < timeout) {
103
- try {
104
- const params = new URLSearchParams({
105
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
106
- device_code: deviceCode,
107
- client_id: this.clientId,
108
- audience: OAUTH_AUDIENCE
109
- })
110
-
111
- const response = await fetch(YOTO_OAUTH_TOKEN_URL, {
112
- method: 'POST',
113
- headers: {
114
- 'Content-Type': 'application/x-www-form-urlencoded'
115
- },
116
- body: params
117
- })
118
-
119
- if (response.ok) {
120
- const tokenData = /** @type {YotoApiTokenResponse} */ (await response.json())
121
- this.log.info(LOG_PREFIX.AUTH, '✓ Authorization successful!')
122
- return tokenData
123
- }
124
-
125
- const errorData = /** @type {any} */ (await response.json().catch(() => ({})))
126
-
127
- // Handle specific OAuth errors
128
- if (errorData.error === 'authorization_pending') {
129
- // Still waiting for user to authorize
130
- await this.sleep(OAUTH_POLLING_INTERVAL)
131
- continue
132
- }
133
-
134
- if (errorData.error === 'slow_down') {
135
- // Server wants us to slow down polling
136
- await this.sleep(OAUTH_POLLING_INTERVAL * 2)
137
- continue
138
- }
139
-
140
- if (errorData.error === 'expired_token') {
141
- throw new Error('Device code expired. Please restart the authorization process.')
142
- }
143
-
144
- if (errorData.error === 'access_denied') {
145
- throw new Error('Authorization was denied by the user.')
146
- }
147
-
148
- // Unknown error
149
- throw new Error(`Authorization failed: ${errorData.error || response.statusText}`)
150
- } catch (error) {
151
- if (error instanceof Error && error.message.includes('expired')) {
152
- throw error
153
- }
154
- this.log.debug(LOG_PREFIX.AUTH, 'Polling error:', error)
155
- await this.sleep(OAUTH_POLLING_INTERVAL)
156
- }
157
- }
158
-
159
- throw new Error('Authorization timed out. Please try again.')
160
- }
161
-
162
- /**
163
- * Refresh access token using refresh token
164
- * @param {string} refreshToken - Refresh token
165
- * @returns {Promise<YotoApiTokenResponse>}
166
- */
167
- async refreshAccessToken (refreshToken) {
168
- this.log.debug(LOG_PREFIX.AUTH, 'Refreshing access token...')
169
-
170
- try {
171
- const params = new URLSearchParams({
172
- grant_type: 'refresh_token',
173
- refresh_token: refreshToken,
174
- client_id: this.clientId
175
- })
176
-
177
- const response = await fetch(YOTO_OAUTH_TOKEN_URL, {
178
- method: 'POST',
179
- headers: {
180
- 'Content-Type': 'application/x-www-form-urlencoded'
181
- },
182
- body: params
183
- })
184
-
185
- if (!response.ok) {
186
- const errorText = await response.text()
187
- throw new Error(`Token refresh failed: ${response.status} ${errorText}`)
188
- }
189
-
190
- const tokenData = /** @type {YotoApiTokenResponse} */ (await response.json())
191
- this.log.info(LOG_PREFIX.AUTH, '✓ Token refreshed successfully')
192
- return tokenData
193
- } catch (error) {
194
- this.log.error(LOG_PREFIX.AUTH, ERROR_MESSAGES.TOKEN_REFRESH_FAILED, error)
195
- throw error
196
- }
197
- }
198
-
199
- /**
200
- * Complete device authorization flow
201
- * @returns {Promise<YotoApiTokenResponse>}
202
- */
203
- async authorize () {
204
- const deviceCodeResponse = await this.initiateDeviceFlow()
205
- const tokenResponse = await this.pollForAuthorization(deviceCodeResponse.device_code)
206
- return tokenResponse
207
- }
208
-
209
- /**
210
- * Check if token is expired or expiring soon
211
- * @param {number} expiresAt - Token expiration timestamp (seconds since epoch)
212
- * @param {number} bufferSeconds - Seconds before expiry to consider expired
213
- * @returns {boolean}
214
- */
215
- isTokenExpired (expiresAt, bufferSeconds = 300) {
216
- const now = Math.floor(Date.now() / 1000)
217
- return expiresAt <= now + bufferSeconds
218
- }
219
-
220
- /**
221
- * Calculate token expiration timestamp
222
- * @param {number} expiresIn - Seconds until token expires
223
- * @returns {number} - Unix timestamp when token expires
224
- */
225
- calculateExpiresAt (expiresIn) {
226
- return Math.floor(Date.now() / 1000) + expiresIn
227
- }
228
-
229
- /**
230
- * Sleep for specified milliseconds
231
- * @param {number} ms - Milliseconds to sleep
232
- * @returns {Promise<void>}
233
- */
234
- sleep (ms) {
235
- return new Promise(resolve => setTimeout(resolve, ms))
236
- }
237
- }