daikin-controller-cloud
Version:
Interact with Daikin Cloud devices and retrieve Tokens
187 lines • 8.6 kB
JavaScript
;
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