/** * Atlas (Xyte) HTTP client. * * Atlas is the SaaS that monitors AV hardware (AMPs, displays, etc.) for the * organization. Auth is a single long-lived API key sent verbatim in the * `Authorization` header — no OAuth/rotation, in contrast to the Webex * Service App. Configure with `ATLAS_AUTH_KEY`. * * `atlasGet` returns the response body and wraps transient failures via * withRetry. Missing-key conditions throw `AtlasUnavailableError` so the * upstream renderer can surface a clean banner instead of a stack trace. */ const axios = require('axios'); const { withRetry } = require('../../utils/retry'); const logger = require('../../utils/logger'); const DEFAULT_BASE_URL = 'https://hub.xyte.io/core/v1'; const REQUEST_TIMEOUT_MS = 15000; const RETRY_OPTS = { retries: 2, initialDelayMs: 500 }; class AtlasUnavailableError extends Error { constructor(message) { super(message); this.name = 'AtlasUnavailableError'; } } // We construct the axios instance lazily so process.env changes between test // cases are picked up, and so importing this module never throws when the // key is absent (the renderer prefers a banner over a startup failure). let _client = null; function getClient() { if (_client) return _client; const authKey = process.env.ATLAS_AUTH_KEY; if (!authKey) { throw new AtlasUnavailableError( 'ATLAS_AUTH_KEY is not set. Add it to the environment and restart the bot.' ); } _client = axios.create({ baseURL: process.env.ATLAS_BASE_URL || DEFAULT_BASE_URL, timeout: REQUEST_TIMEOUT_MS, headers: { 'Content-Type': 'application/json', // Atlas accepts the raw key in the Authorization header (no "Bearer "). Authorization: authKey, }, }); return _client; } /** * GET against the Atlas API. Returns `response.data` (or `{}`). Wraps * transient failures via withRetry. * * Throws AtlasUnavailableError when ATLAS_AUTH_KEY is missing, or a plain * Error with status context on hard transport failures. */ async function atlasGet(endpoint, params = {}) { const client = getClient(); const path = String(endpoint).replace(/^\/+/, ''); try { const res = await withRetry( () => client.get(`/${path}`, { params, // Validate manually so 4xx don't burn retry budget. validateStatus: status => status >= 200 && status < 500, }), RETRY_OPTS ); if (res.status >= 400) { const detail = res.data?.message || res.data?.error || `${res.status} ${res.statusText || ''}`.trim(); throw new Error(`Atlas GET /${path} failed: ${detail}`); } return res.data ?? {}; } catch (err) { if (err instanceof AtlasUnavailableError) throw err; logger.error('Atlas request failed', { endpoint: path, error: err.message, status: err.response?.status, }); throw err; } } function resetClientForTests() { _client = null; } module.exports = { atlasGet, AtlasUnavailableError, resetClientForTests, // Exposed so tests can assert defaults without poking the cached client. DEFAULT_BASE_URL, };