homebridge-yoto 0.0.28 → 0.0.31
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/AGENTS.md +42 -157
- package/CHANGELOG.md +13 -5
- package/NOTES.md +87 -0
- package/PLAN.md +320 -504
- package/README.md +18 -314
- package/config.schema.cjs +3 -0
- package/config.schema.json +19 -155
- package/homebridge-ui/server.js +264 -0
- package/index.js +1 -1
- package/index.test.js +1 -1
- package/lib/accessory.js +1870 -0
- package/lib/constants.js +8 -149
- package/lib/platform.js +303 -363
- package/lib/sanitize-name.js +49 -0
- package/lib/settings.js +16 -0
- package/lib/sync-service-names.js +34 -0
- package/logo.png +0 -0
- package/package.json +17 -22
- package/pnpm-workspace.yaml +4 -0
- package/declaration.tsconfig.json +0 -15
- package/lib/auth.js +0 -237
- package/lib/playerAccessory.js +0 -1724
- package/lib/types.js +0 -253
- package/lib/yotoApi.js +0 -270
- package/lib/yotoMqtt.js +0 -570
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility functions for the plugin
|
|
3
|
+
*
|
|
4
|
+
* Includes code adapted from `homebridge-plugin-utils`:
|
|
5
|
+
* - Source: https://github.com/hjdhjd/homebridge-plugin-utils/blob/main/src/util.ts
|
|
6
|
+
*
|
|
7
|
+
* ISC License
|
|
8
|
+
* ===========
|
|
9
|
+
*
|
|
10
|
+
* Copyright (c) 2017-2025, HJD https://github.com/hjdhjd
|
|
11
|
+
*
|
|
12
|
+
* Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
13
|
+
* with or without fee is hereby granted, provided that the above copyright notice
|
|
14
|
+
* and this permission notice appear in all copies.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
17
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
18
|
+
* FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
19
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
20
|
+
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
21
|
+
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
22
|
+
* THIS SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize an accessory/service name according to HomeKit naming conventions.
|
|
27
|
+
*
|
|
28
|
+
* Starts and ends with a letter or number. Exception: may end with a period.
|
|
29
|
+
* May have the following special characters: -"',.#&.
|
|
30
|
+
* Must not include emojis.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} name - The name to sanitize
|
|
33
|
+
* @returns {string} The HomeKit-sanitized version of the name
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeName (name) {
|
|
36
|
+
return name
|
|
37
|
+
// Replace any disallowed char (including emojis) with a space.
|
|
38
|
+
.replace(/[^\p{L}\p{N}\-"'.,#&\s]/gu, ' ')
|
|
39
|
+
// Collapse multiple spaces to one.
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
// Trim spaces at the beginning and end of the string.
|
|
42
|
+
.trim()
|
|
43
|
+
// Strip any leading non-letter/number.
|
|
44
|
+
.replace(/^[^\p{L}\p{N}]+/u, '')
|
|
45
|
+
// Collapse two or more trailing periods into one.
|
|
46
|
+
.replace(/\.{2,}$/g, '.')
|
|
47
|
+
// Remove any other trailing char that's not letter/number/period.
|
|
48
|
+
.replace(/[^\p{L}\p{N}.]$/u, '')
|
|
49
|
+
}
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { configSchema } from '../config.schema.cjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
|
|
5
|
+
*/
|
|
6
|
+
export const PLATFORM_NAME = 'Yoto'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This must match the name of your plugin as defined the package.json `name` property
|
|
10
|
+
*/
|
|
11
|
+
export const PLUGIN_NAME = 'homebridge-yoto'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default OAuth Client ID from config schema
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_CLIENT_ID = configSchema.schema.properties.clientId.default
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @import { Service, Characteristic } from 'homebridge' */
|
|
2
|
+
import { sanitizeName } from './sanitize-name.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Apply HomeKit-visible naming to a service.
|
|
6
|
+
*
|
|
7
|
+
* We set both `Name` and `ConfiguredName` on every service we manage so HomeKit tiles are consistently labeled.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} params
|
|
10
|
+
* @param {Service} params.service
|
|
11
|
+
* @param {string} params.name
|
|
12
|
+
* @param {typeof Characteristic} params.Characteristic
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
export function syncServiceNames ({
|
|
16
|
+
Characteristic,
|
|
17
|
+
service,
|
|
18
|
+
name
|
|
19
|
+
}) {
|
|
20
|
+
const sanitizedName = sanitizeName(name)
|
|
21
|
+
service.displayName = sanitizedName
|
|
22
|
+
|
|
23
|
+
service.updateCharacteristic(Characteristic.Name, sanitizedName)
|
|
24
|
+
|
|
25
|
+
// Set ConfiguredName on all services, not just ones that say they support it.
|
|
26
|
+
// This is the only way to set the service name inside an accessory.
|
|
27
|
+
// const hasConfiguredNameCharacteristic = service.characteristics.some(c => c.UUID === Characteristic.ConfiguredName.UUID)
|
|
28
|
+
// const hasConfiguredNameOptional = service.optionalCharacteristics.some(c => c.UUID === Characteristic.ConfiguredName.UUID)
|
|
29
|
+
// if (!hasConfiguredNameCharacteristic && !hasConfiguredNameOptional) {
|
|
30
|
+
// service.addOptionalCharacteristic(Characteristic.ConfiguredName)
|
|
31
|
+
// }
|
|
32
|
+
|
|
33
|
+
service.updateCharacteristic(Characteristic.ConfiguredName, sanitizedName)
|
|
34
|
+
}
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-yoto",
|
|
3
3
|
"description": "Control your Yoto players through Apple HomeKit with real-time MQTT updates",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.31",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"homebridge-
|
|
11
|
-
"
|
|
10
|
+
"@homebridge/plugin-ui-utils": "^2.1.2",
|
|
11
|
+
"color-convert": "3.1.3",
|
|
12
|
+
"yoto-nodejs-client": "^0.0.7"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"@types/node": "^25.0.0",
|
|
15
16
|
"@voxpelli/tsconfig": "^16.1.0",
|
|
16
17
|
"auto-changelog": "^2.0.0",
|
|
17
18
|
"c8": "^10.0.0",
|
|
19
|
+
"eslint": "^9.39.2",
|
|
18
20
|
"gh-release": "^7.0.0",
|
|
19
|
-
"homebridge": "^
|
|
21
|
+
"homebridge": "^2.0.0-beta.66",
|
|
20
22
|
"neostandard": "^0.12.0",
|
|
21
23
|
"npm-run-all2": "^8.0.1",
|
|
22
24
|
"typescript": "~5.9.3"
|
|
23
25
|
},
|
|
24
26
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
27
|
+
"node": ">=22",
|
|
26
28
|
"npm": ">=10",
|
|
27
29
|
"homebridge": "^1.8.0 || ^2.0.0-beta.0"
|
|
28
30
|
},
|
|
@@ -40,22 +42,6 @@
|
|
|
40
42
|
"type": "git",
|
|
41
43
|
"url": "https://github.com/bcomnes/homebridge-yoto.git"
|
|
42
44
|
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"prepublishOnly": "npm run build && git push --follow-tags && gh-release -y",
|
|
45
|
-
"postpublish": "npm run clean",
|
|
46
|
-
"test": "run-s test:*",
|
|
47
|
-
"test:lint": "eslint",
|
|
48
|
-
"test:tsc": "tsc",
|
|
49
|
-
"test:node-test": "c8 node --test --test-reporter spec",
|
|
50
|
-
"version": "run-s version:*",
|
|
51
|
-
"version:changelog": "auto-changelog -p --template keepachangelog auto-changelog --breaking-pattern 'BREAKING CHANGE:'",
|
|
52
|
-
"version:git": "git add CHANGELOG.md",
|
|
53
|
-
"build": "npm run clean && run-p build:*",
|
|
54
|
-
"build:declaration": "tsc -p declaration.tsconfig.json",
|
|
55
|
-
"clean": "run-p clean:*",
|
|
56
|
-
"clean:declarations-top": "rm -rf $(find . -maxdepth 1 -type f -name '*.d.ts*')",
|
|
57
|
-
"clean:declarations-lib": "rm -rf $(find lib -type f -name '*.d.ts*' ! -name '*-types.d.ts')"
|
|
58
|
-
},
|
|
59
45
|
"funding": {
|
|
60
46
|
"type": "individual",
|
|
61
47
|
"url": "https://github.com/sponsors/bcomnes"
|
|
@@ -65,5 +51,14 @@
|
|
|
65
51
|
"lcov",
|
|
66
52
|
"text"
|
|
67
53
|
]
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "run-s test:*",
|
|
57
|
+
"test:lint": "eslint",
|
|
58
|
+
"test:tsc": "tsc",
|
|
59
|
+
"test:node-test": "c8 node --test --test-reporter spec",
|
|
60
|
+
"version": "run-s version:*",
|
|
61
|
+
"version:changelog": "auto-changelog -p --template keepachangelog auto-changelog --breaking-pattern 'BREAKING CHANGE:'",
|
|
62
|
+
"version:git": "git add CHANGELOG.md"
|
|
68
63
|
}
|
|
69
|
-
}
|
|
64
|
+
}
|
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
|
-
}
|