UNPKG

daikin-controller-cloud

Version:

Interact with Daikin Cloud devices and retrieve Tokens

187 lines 8.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OnectaClient = void 0; const promises_1 = require("node:fs/promises"); const node_crypto_1 = require("node:crypto"); const openid_client_1 = require("openid-client"); const oidc_utils_js_1 = require("./oidc-utils.js"); const oidc_callback_server_js_1 = require("./oidc-callback-server.js"); const index_1 = require("../index"); const ONE_DAY_S = 24 * 60 * 60; openid_client_1.custom.setHttpOptionsDefaults({ timeout: 10_000, // Default 3.5s is too less sometimes as it seems }); class OnectaClient { #config; #client; #tokenSet; #emitter; #getTokenSetQueue; #blockedUntil = 0; constructor(config, emitter) { this.#config = config; this.#emitter = emitter; this.#client = new oidc_utils_js_1.onecta_oidc_issuer.Client({ client_id: config.oidcClientId, client_secret: config.oidcClientSecret, }); this.#tokenSet = config.tokenSet ? new openid_client_1.TokenSet(config.tokenSet) : null; this.#getTokenSetQueue = []; } get blockedUntil() { return this.#blockedUntil; } async #getAuthCodeWithCustomReceiver() { const { customOidcCodeReceiver: receiver, oidcCallbackServerBaseUrl: redirectUri } = this.#config; if (!receiver || !redirectUri) { throw new Error('Config params "customOidcCodeReceiver" and "oidcCallbackServerBaseUrl" are both required when using a custom OIDC authorization grant receiver'); } const reqState = (0, node_crypto_1.randomBytes)(32).toString('hex'); const authUrl = this.#client.authorizationUrl({ scope: oidc_utils_js_1.OnectaOIDCScope.basic, state: reqState, redirect_uri: redirectUri, }); return { authCode: await receiver(authUrl, reqState), redirectUri }; } async #getAuthCodeWithServer() { const reqState = (0, node_crypto_1.randomBytes)(32).toString('hex'); const server = new oidc_callback_server_js_1.OnectaOIDCCallbackServer(this.#config); const redirectUri = await server.listen(); const authUrl = this.#client.authorizationUrl({ scope: oidc_utils_js_1.OnectaOIDCScope.basic, state: reqState, redirect_uri: redirectUri, }); this.#emitter.emit('authorization_request', redirectUri); return { authCode: await server.waitForAuthCodeAndClose(reqState, authUrl), redirectUri }; } async #authorize() { const config = this.#config; const { authCode, redirectUri } = config.customOidcCodeReceiver ? await this.#getAuthCodeWithCustomReceiver() : await this.#getAuthCodeWithServer(); return await this.#client.grant({ grant_type: 'authorization_code', client_id: this.#config.oidcClientId, client_secret: this.#config.oidcClientSecret, code: authCode, redirect_uri: redirectUri, }); } async #refresh(refreshToken) { return await this.#client.grant({ grant_type: 'refresh_token', client_id: this.#config.oidcClientId, client_secret: this.#config.oidcClientSecret, refresh_token: refreshToken, }); } async #loadTokenSet() { if (this.#config.oidcTokenSetFilePath) { try { const data = await (0, promises_1.readFile)(this.#config.oidcTokenSetFilePath, 'utf8'); return new openid_client_1.TokenSet(JSON.parse(data)); } catch (err) { if (err.code !== 'ENOENT') { this.#emitter.emit('error', 'Could not load OIDC tokenset from disk: ' + err.message); } } } return null; } async #storeTokenSet(set) { this.#emitter.emit('token_update', set); if (this.#config.oidcTokenSetFilePath) { try { await (0, promises_1.writeFile)(this.#config.oidcTokenSetFilePath, JSON.stringify(set, null, 2)); } catch (err) { this.#emitter.emit('error', 'Could not store OIDC tokenset to disk: ' + err.message); } } } ; async #getTokenSet() { let tokenSet = this.#tokenSet; if (!tokenSet && (tokenSet = await this.#loadTokenSet())) { this.#tokenSet = tokenSet; } if (!tokenSet || !tokenSet.refresh_token) { tokenSet = await this.#authorize(); } else if (!tokenSet.expires_at || tokenSet.expires_at < (Date.now() / 1000) + 10) { tokenSet = await this.#refresh(tokenSet.refresh_token); } if (this.#tokenSet !== tokenSet) { await this.#storeTokenSet(tokenSet); } this.#tokenSet = tokenSet; return tokenSet; } async #getTokenSetQueued() { return new Promise((resolve, reject) => { this.#getTokenSetQueue.push({ resolve, reject }); if (this.#getTokenSetQueue.length === 1) { this.#getTokenSet() .then((tokenSet) => { this.#getTokenSetQueue.forEach(({ resolve }) => resolve(tokenSet)); this.#getTokenSetQueue = []; }) .catch((err) => { this.#getTokenSetQueue.forEach(({ reject }) => reject(err)); this.#getTokenSetQueue = []; }); } }); } #getRateLimitStatus(res) { // See "Rate limitation" at https://developer.cloud.daikineurope.com/docs/b0dffcaa-7b51-428a-bdff-a7c8a64195c0/general_api_guidelines return { limitMinute: (0, oidc_utils_js_1.maybeParseInt)(res.headers['x-ratelimit-limit-minute']), remainingMinute: (0, oidc_utils_js_1.maybeParseInt)(res.headers['x-ratelimit-remaining-minute']), limitDay: (0, oidc_utils_js_1.maybeParseInt)(res.headers['x-ratelimit-limit-day']), remainingDay: (0, oidc_utils_js_1.maybeParseInt)(res.headers['x-ratelimit-remaining-day']), }; } async requestResource(path, opts) { if (!opts?.ignoreRateLimit && this.#blockedUntil > Date.now()) { const retryAfter = Math.ceil((this.#blockedUntil - Date.now()) / 1000); throw new index_1.RateLimitedError(`API request blocked because of rate-limits for ${retryAfter} seconds`, retryAfter); } const reqOpts = { ...opts }; delete reqOpts.ignoreRateLimit; const tokenSet = await this.#getTokenSetQueued(); const url = `${oidc_utils_js_1.OnectaAPIBaseUrl.prod}${path}`; const res = await this.#client.requestResource(url, tokenSet, reqOpts); oidc_utils_js_1.RESOLVED.then(() => this.#emitter.emit('rate_limit_status', this.#getRateLimitStatus(res))); switch (res.statusCode) { case 200: case 204: return res.body ? JSON.parse(res.body.toString()) : null; case 400: throw new Error(`Bad Request (400): ${res.body ? res.body.toString() : 'No body response from the API'}`); case 404: throw new Error(`Not Found (404): ${res.body ? res.body.toString() : 'No body response from the API'}`); case 409: throw new Error(`Conflict (409): ${res.body ? res.body.toString() : 'No body response from the API'}`); case 422: throw new Error(`Unprocessable Entity (422): ${res.body ? res.body.toString() : 'No body response from the API'}`); case 429: { // See "Rate limitation" at https://developer.cloud.daikineurope.com/docs/b0dffcaa-7b51-428a-bdff-a7c8a64195c0/general_api_guidelines const retryAfter = (0, oidc_utils_js_1.maybeParseInt)(res.headers['retry-after']); let blockedFor = retryAfter; if (retryAfter !== undefined) { blockedFor = retryAfter > ONE_DAY_S ? ONE_DAY_S : retryAfter; this.#blockedUntil = Date.now() + blockedFor * 1000; } throw new index_1.RateLimitedError(`API request rate-limited, retry after ${retryAfter} seconds. API requests blocked for ${blockedFor} seconds`, blockedFor); } case 500: default: throw new Error(`Unexpected API error`); } } } exports.OnectaClient = OnectaClient; //# sourceMappingURL=oidc-client.js.map