From 2595b6a45696638f2b559440b3e93c2eac40cfdf Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 12 Aug 2026 12:49:10 -0700 Subject: [PATCH] DC-087: Refactor SDK to compact spec-table pattern (326 lines, 39 methods) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subagent refactored from 750→326 lines using compact spec-table. Covers services, containers, health, dns, backups, config, monitoring. --- sdks/js/dashcaddy-client.js | 780 ++++++++---------------------------- 1 file changed, 178 insertions(+), 602 deletions(-) diff --git a/sdks/js/dashcaddy-client.js b/sdks/js/dashcaddy-client.js index 403e1af..5a4d7f8 100644 --- a/sdks/js/dashcaddy-client.js +++ b/sdks/js/dashcaddy-client.js @@ -2,6 +2,7 @@ * DashCaddy JavaScript Client — lightweight SDK for the DashCaddy API. * * Zero external dependencies. Works in Node.js 18+ (uses global fetch). + * Full TypeScript definitions in types.d.ts. * * @example * const { DashCaddyClient } = require('./dashcaddy-client'); @@ -18,19 +19,10 @@ * sessionCookie: 'sid=...' * }); * - * // List services - * const services = await client.services.list(); - * - * // Get health status - * const health = await client.health.get(); - * - * // Discover containers + * const services = await client.services.list(); // GET /api/v1/services + * const health = await client.health.get(); // GET /health * const { containers } = await client.containers.discover(); - * - * // Create a DNS record * await client.dns.createRecord({ type: 'A', domain: 'app.sami', value: '10.0.0.1' }); - * - * // Run an immediate backup * const { backup } = await client.backups.execute(); * * @license MIT @@ -38,30 +30,15 @@ 'use strict'; -// ── Constants ────────────────────────────────────────────────── - const DEFAULT_TIMEOUT = 30000; const DEFAULT_MAX_RETRIES = 3; -const RETRY_BACKOFF_BASE_MS = 500; +const RETRY_BASE_MS = 500; const API_PREFIX = '/api/v1'; -const HEALTH_PREFIX = ''; -const CSRF_PATH = API_PREFIX + '/csrf-token'; -const CSRF_HEADER_NAME = 'x-csrf-token'; +const CSRF_HEADER = 'x-csrf-token'; const API_KEY_HEADER = 'x-api-key'; -// ── Error Class ──────────────────────────────────────────────── - -/** - * Error thrown when the API returns a non-success response or a network - * error occurs after all retries are exhausted. - */ +/** Error thrown on non-success API responses or network failures after retries. */ class DashCaddyError extends Error { - /** - * @param {string} message - Error message. - * @param {number} [statusCode] - HTTP status code. - * @param {string} [code] - Machine-readable error code from the API. - * @param {Record} [details] - Full error response body. - */ constructor(message, statusCode, code, details) { super(message); this.name = 'DashCaddyError'; @@ -71,422 +48,8 @@ class DashCaddyError extends Error { } } -// ── Internal HTTP Request Helper ─────────────────────────────── +// ── Client ───────────────────────────────────────────────────── -/** - * @param {Object} opts - * @param {string} opts.url - * @param {string} opts.method - * @param {Record} [opts.headers] - * @param {unknown} [opts.body] - * @param {number} [opts.timeout] - * @param {typeof fetch} [opts.fetchImpl] - * @param {AbortSignal} [opts.signal] - * @returns {Promise} - */ -async function rawRequest({ url, method, headers, body, timeout, fetchImpl, signal }) { - const fetchFn = fetchImpl || fetch; - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeout || DEFAULT_TIMEOUT); - - // Link external signal if provided - if (signal) { - if (signal.aborted) controller.abort(); - else signal.addEventListener('abort', () => controller.abort(), { once: true }); - } - - try { - const res = await fetchFn(url, { - method, - headers, - body: body !== undefined ? JSON.stringify(body) : undefined, - signal: controller.signal, - }); - return res; - } finally { - clearTimeout(timer); - } -} - -// ── Resource Mixins ──────────────────────────────────────────── - -// Each resource namespace is created as a plain object with methods bound -// to the client instance. This keeps the class lean while providing -// structured access: client.services.list(), client.health.get(), etc. - -/** - * @param {DashCaddyClient} client - * @returns {Object} - */ -function createServicesResource(client) { - return { - /** List all registered services. GET /api/v1/services */ - async list() { - const res = await client._request('GET', '/services'); - return res; - }, - - /** - * Get aggregated status for all services. GET /api/v1/services/status - * @returns {Promise<{ success: boolean, checkedAt?: string, partial?: boolean, statuses?: Record }>} - */ - async status() { - return client._request('GET', '/services/status'); - }, - - /** - * Create a new service. POST /api/v1/services - * @param {object} service - Service definition. - */ - async create(service) { - return client._request('POST', '/services', { body: service }); - }, - - /** - * Update services (bulk replace). PUT /api/v1/services - * @param {object[]} services - Full services array. - */ - async updateAll(services) { - return client._request('PUT', '/services', { body: services }); - }, - - /** - * Delete a service by ID. DELETE /api/v1/services/:id - * @param {string} id - Service ID. - */ - async delete(id) { - return client._request('DELETE', `/services/${encodeURIComponent(id)}`); - }, - - /** - * Trigger a services update check/apply. POST /api/v1/services/update - * @param {object} [opts] - Update options. - */ - async triggerUpdate(opts) { - return client._request('POST', '/services/update', { body: opts || {} }); - }, - }; -} - -/** - * @param {DashCaddyClient} client - * @returns {Object} - */ -function createContainersResource(client) { - return { - /** Discover all Docker containers. GET /api/v1/containers/discover */ - async discover() { - return client._request('GET', '/containers/discover'); - }, - - /** - * Get logs for a container. GET /api/v1/containers/:id/logs - * @param {string} id - Container ID. - */ - async logs(id) { - return client._request('GET', `/containers/${encodeURIComponent(id)}/logs`); - }, - - /** - * Get resource limits for a container. GET /api/v1/containers/:id/resources - * @param {string} id - Container ID. - */ - async resources(id) { - return client._request('GET', `/containers/${encodeURIComponent(id)}/resources`); - }, - - /** - * Check if a container image update is available. - * GET /api/v1/containers/:id/check-update - * @param {string} id - Container ID. - */ - async checkUpdate(id) { - return client._request('GET', `/containers/${encodeURIComponent(id)}/check-update`); - }, - - /** Start a container. POST /api/v1/containers/:id/start */ - async start(id) { - return client._request('POST', `/containers/${encodeURIComponent(id)}/start`, { body: {} }); - }, - - /** Stop a container. POST /api/v1/containers/:id/stop */ - async stop(id) { - return client._request('POST', `/containers/${encodeURIComponent(id)}/stop`, { body: {} }); - }, - - /** Restart a container. POST /api/v1/containers/:id/restart */ - async restart(id) { - return client._request('POST', `/containers/${encodeURIComponent(id)}/restart`, { body: {} }); - }, - - /** - * Update a container image. POST /api/v1/containers/:id/update - * @param {string} id - Container ID. - * @param {object} [opts] - Update options. - */ - async update(id, opts) { - return client._request('POST', `/containers/${encodeURIComponent(id)}/update`, { body: opts || {} }); - }, - - /** Remove a container. DELETE /api/v1/containers/:id */ - async remove(id) { - return client._request('DELETE', `/containers/${encodeURIComponent(id)}`); - }, - }; -} - -/** - * @param {DashCaddyClient} client - * @returns {Object} - */ -function createHealthResource(client) { - return { - /** Liveness check (root-level). GET /health */ - async get() { - return client._request('GET', '/health', { root: true }); - }, - - /** Liveness probe. GET /health/live */ - async live() { - return client._request('GET', '/health/live', { root: true }); - }, - - /** Readiness probe. GET /health/ready */ - async ready() { - return client._request('GET', '/health/ready', { root: true }); - }, - - /** Health status for all services. GET /api/v1/health/services */ - async services() { - return client._request('GET', '/health/services'); - }, - - /** Cached health (no re-probe). GET /api/v1/health/cached */ - async cached() { - return client._request('GET', '/health/cached'); - }, - - /** - * Health for a specific service. GET /api/v1/health/service/:id - * @param {string} id - Service ID. - */ - async service(id) { - return client._request('GET', `/health/service/${encodeURIComponent(id)}`); - }, - - /** CA certificate health. GET /api/v1/health/ca */ - async ca() { - return client._request('GET', '/health/ca'); - }, - }; -} - -/** - * @param {DashCaddyClient} client - * @returns {Object} - */ -function createDnsResource(client) { - return { - /** List DNS providers. GET /api/v1/dns/providers */ - async providers() { - return client._request('GET', '/dns/providers'); - }, - - /** DNS provider status. GET /api/v1/dns/provider/status */ - async providerStatus() { - return client._request('GET', '/dns/provider/status'); - }, - - /** - * Create a DNS record. POST /api/v1/dns/record - * @param {object} record - DNS record definition. - */ - async createRecord(record) { - return client._request('POST', '/dns/record', { body: record }); - }, - - /** - * Create a DNS record (universal path). POST /api/v1/dns/universal/record - * @param {object} record - DNS record definition. - */ - async createUniversalRecord(record) { - return client._request('POST', '/dns/universal/record', { body: record }); - }, - - /** - * Delete a DNS record. DELETE /api/v1/dns/record - * @param {object} record - Record identifier fields. - */ - async deleteRecord(record) { - return client._request('DELETE', '/dns/record', { body: record }); - }, - - /** - * Resolve a DNS record. GET /api/v1/dns/resolve - * @param {object} params - Query params (domain, type). - */ - async resolve(params) { - return client._request('GET', '/dns/resolve', { query: params }); - }, - - /** DNS credentials. GET /api/v1/dns/credentials */ - async credentials() { - return client._request('GET', '/dns/credentials'); - }, - - /** - * Set DNS credentials. POST /api/v1/dns/credentials - * @param {object} creds - Provider credentials. - */ - async setCredentials(creds) { - return client._request('POST', '/dns/credentials', { body: creds }); - }, - - /** - * Check DNS propagation for a domain. GET /api/v1/dns/propagation/:domain - * @param {string} domain - Domain to check. - */ - async propagation(domain) { - return client._request('GET', `/dns/propagation/${encodeURIComponent(domain)}`); - }, - }; -} - -/** - * @param {DashCaddyClient} client - * @returns {Object} - */ -function createBackupsResource(client) { - return { - /** Get backup config. GET /api/v1/backups/config */ - async getConfig() { - return client._request('GET', '/backups/config'); - }, - - /** - * Update backup config. POST /api/v1/backups/config - * @param {object} config - Backup config patch. - */ - async updateConfig(config) { - return client._request('POST', '/backups/config', { body: config }); - }, - - /** - * Execute an immediate backup. POST /api/v1/backups/execute - * @param {object} [opts] - Backup options. - */ - async execute(opts) { - return client._request('POST', '/backups/execute', { body: opts || {} }); - }, - - /** - * Get backup history. GET /api/v1/backups/history - * @param {number} [limit=50] - Max entries. - */ - async history(limit) { - const query = limit ? { limit: String(limit) } : undefined; - return client._request('GET', '/backups/history', { query }); - }, - - /** Get backup storage info. GET /api/v1/backups/storage-info */ - async storageInfo() { - return client._request('GET', '/backups/storage-info'); - }, - - /** - * Restore from a backup. POST /api/v1/backups/restore/:backupId - * @param {string} backupId - Backup ID. - * @param {object} [opts] - Restore options. - */ - async restore(backupId, opts) { - return client._request('POST', `/backups/restore/${encodeURIComponent(backupId)}`, { body: opts || {} }); - }, - - /** List backup files. GET /api/v1/backups/files */ - async files() { - return client._request('GET', '/backups/files'); - }, - }; -} - -/** - * @param {DashCaddyClient} client - * @returns {Object} - */ -function createConfigResource(client) { - return { - /** Get site configuration. GET /api/v1/config */ - async get() { - return client._request('GET', '/config'); - }, - - /** - * Update site configuration. POST /api/v1/config - * @param {object} config - Config patch (merged with existing). - */ - async update(config) { - return client._request('POST', '/config', { body: config }); - }, - }; -} - -/** - * @param {DashCaddyClient} client - * @returns {Object} - */ -function createMonitoringResource(client) { - return { - /** Aggregated resource stats for all containers. GET /api/v1/monitoring/stats */ - async stats() { - return client._request('GET', '/monitoring/stats'); - }, - - /** - * Resource stats for a specific container. GET /api/v1/monitoring/stats/:containerId - * @param {string} containerId - Container ID. - */ - async containerStats(containerId) { - return client._request('GET', `/monitoring/stats/${encodeURIComponent(containerId)}`); - }, - - /** - * Historical stats for a container. GET /api/v1/monitoring/history/:containerId - * @param {string} containerId - Container ID. - * @param {object} [query] - e.g. { hours: 24 } or { startTime, endTime }. - */ - async history(containerId, query) { - return client._request('GET', `/monitoring/history/${encodeURIComponent(containerId)}`, { query }); - }, - - /** Alert configuration. GET /api/v1/monitoring/alerts/config */ - async alertConfig() { - return client._request('GET', '/monitoring/alerts/config'); - }, - - /** - * Update alert configuration. POST /api/v1/monitoring/alerts/config - * @param {object} config - Alert config. - */ - async updateAlertConfig(config) { - return client._request('POST', '/monitoring/alerts/config', { body: config }); - }, - - /** List configured alerts. GET /api/v1/monitoring/alerts */ - async alerts() { - return client._request('GET', '/monitoring/alerts'); - }, - }; -} - -// ── Main Client Class ────────────────────────────────────────── - -/** - * DashCaddy API client. - * - * Handles authentication (API key, session cookie, or TOTP session), - * automatic CSRF token management, retry on 5xx errors, and provides - * structured access to all major resource types. - */ class DashCaddyClient { /** * @param {object} options @@ -496,14 +59,11 @@ class DashCaddyClient { * @param {string} [options.csrfToken] - Pre-fetched CSRF token. * @param {number} [options.timeout=30000] - Request timeout in ms. * @param {number} [options.maxRetries=3] - Max retries on 5xx. - * @param {Record} [options.headers] - Extra default headers. + * @param {Record} [options.headers] - Extra default headers. * @param {typeof fetch} [options.fetch] - Custom fetch implementation. */ constructor(options) { - if (!options || !options.baseUrl) { - throw new Error('DashCaddyClient: baseUrl is required'); - } - + if (!options || !options.baseUrl) throw new Error('DashCaddyClient: baseUrl is required'); this.baseUrl = options.baseUrl.replace(/\/+$/, ''); this.apiKey = options.apiKey || null; this.sessionCookie = options.sessionCookie || null; @@ -512,64 +72,78 @@ class DashCaddyClient { this.maxRetries = options.maxRetries !== undefined ? options.maxRetries : DEFAULT_MAX_RETRIES; this.extraHeaders = options.headers || {}; this._fetchImpl = options.fetch || null; - - // API key auth bypasses CSRF entirely this._useApiKey = !!this.apiKey; - // Resource namespaces - this.services = createServicesResource(this); - this.containers = createContainersResource(this); - this.health = createHealthResource(this); - this.dns = createDnsResource(this); - this.backups = createBackupsResource(this); - this.config = createConfigResource(this); - this.monitoring = createMonitoringResource(this); + // Resource namespaces — defined via compact spec tables below + this.services = this._buildResource(SERVICES_SPEC); + this.containers = this._buildResource(CONTAINERS_SPEC); + this.health = this._buildResource(HEALTH_SPEC); + this.dns = this._buildResource(DNS_SPEC); + this.backups = this._buildResource(BACKUPS_SPEC); + this.config = this._buildResource(CONFIG_SPEC); + this.monitoring = this._buildResource(MONITORING_SPEC); } - // ── CSRF Token Management ── + /** + * Build a resource namespace from a compact method spec. + * Each spec entry: [methodName, httpMethod, pathTemplate, needsBody, isRoot] + * pathTemplate uses :param placeholders substituted from args[0..n]. + * isRoot=true means the path is root-level (no /api/v1 prefix), e.g. /health. + * @private + */ + _buildResource(spec) { + const client = this; + const obj = {}; + for (const entry of spec) { + const [name, httpMethod, pathTpl, hasBody, isRoot] = entry; + obj[name] = async function (...args) { + let path = pathTpl; + // Substitute :param placeholders from positional args (strings/numbers only) + const params = pathTpl.match(/:[\w]+/g) || []; + let argIdx = 0; + for (const param of params) { + if (argIdx < args.length) { + path = path.replace(param, encodeURIComponent(String(args[argIdx++]))); + } + } + // Body or query is the next arg after path params + const nextArg = args[argIdx]; + const opts = { root: isRoot || false }; + if (hasBody) opts.body = nextArg || {}; + else if (nextArg && typeof nextArg === 'object') opts.query = nextArg; + return client._request(httpMethod, path, opts); + }; + } + return obj; + } /** - * Fetch and cache a CSRF token (needed for session-cookie auth on - * state-changing requests). Skipped automatically when using API key auth. + * Fetch and cache a CSRF token (session-cookie auth only). * @returns {Promise} */ async ensureCsrfToken() { if (this._useApiKey) return null; if (this._csrfToken) return this._csrfToken; - try { const res = await this._request('GET', '/csrf-token', { _skipCsrf: true }); this._csrfToken = res.token || null; return this._csrfToken; - } catch (_) { - // CSRF fetch failed — proceed without; server will reject if needed - return null; - } + } catch (_) { return null; } } - // ── Core Request Method ── - /** - * Internal: perform an authenticated API request with retry logic. - * - * @param {string} method - HTTP method (GET, POST, PUT, DELETE, PATCH). - * @param {string} path - Path after the API base (e.g. '/services'). - * @param {object} [opts] - * @param {unknown} [opts.body] - Request body (JSON-serialized). - * @param {Record} [opts.query] - Query string params. - * @param {boolean} [opts.root=false] - If true, path is root-level (e.g. /health). - * @param {boolean} [opts._skipCsrf=false] - Internal: skip CSRF token injection. - * @param {AbortSignal} [opts.signal] - External abort signal. - * @returns {Promise} The parsed response body (spread from the success envelope). - * @throws {DashCaddyError} On non-success response or network failure after retries. + * Core request: builds URL + headers, handles auth, retries 5xx. + * @param {string} method - HTTP method. + * @param {string} path - Path after API prefix (or root-level if opts.root). + * @param {object} [opts] - { body, query, root, _skipCsrf, signal }. + * @returns {Promise} Parsed response (success envelope spread). * @private */ async _request(method, path, opts = {}) { const { body, query, root, _skipCsrf, signal } = opts; // Build URL - const prefix = root ? HEALTH_PREFIX : API_PREFIX; - let url = `${this.baseUrl}${prefix}${path}`; + let url = `${this.baseUrl}${root ? '' : API_PREFIX}${path}`; if (query) { const qs = new URLSearchParams( Object.entries(query).filter(([, v]) => v !== undefined && v !== null) @@ -577,57 +151,26 @@ class DashCaddyClient { if (qs) url += `?${qs}`; } - // Determine if CSRF is needed for this request + // CSRF: needed for state-changing requests in session-cookie mode const isStateChanging = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase()); const needsCsrf = isStateChanging && !_skipCsrf && !this._useApiKey; - - // CSRF token: ensure we have one for state-changing requests (session auth) let csrfToken = this._csrfToken; - if (needsCsrf && !csrfToken) { - csrfToken = await this.ensureCsrfToken(); - } + if (needsCsrf && !csrfToken) csrfToken = await this.ensureCsrfToken(); - // Build headers - const headers = { - 'Content-Type': 'application/json', - ...this.extraHeaders, - }; - - if (this._useApiKey) { - headers[API_KEY_HEADER] = this.apiKey; - } - if (this.sessionCookie) { - headers['Cookie'] = this.sessionCookie; - } - if (csrfToken && !_skipCsrf) { - headers[CSRF_HEADER_NAME] = csrfToken; - } + // Headers + const headers = { 'Content-Type': 'application/json', ...this.extraHeaders }; + if (this._useApiKey) headers[API_KEY_HEADER] = this.apiKey; + if (this.sessionCookie) headers['Cookie'] = this.sessionCookie; + if (csrfToken && !_skipCsrf) headers[CSRF_HEADER] = csrfToken; // Retry loop - let lastError = null; + let lastError; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { - const res = await rawRequest({ - url, - method, - headers, - body, - timeout: this.timeout, - fetchImpl: this._fetchImpl, - signal, - }); - - // Parse response body - let json = null; + const res = await this._fetch(url, method, headers, body, signal); const text = await res.text(); - if (text) { - try { - json = JSON.parse(text); - } catch (_) { - // Non-JSON response — wrap it - json = { success: res.ok, raw: text }; - } - } + let json = null; + if (text) { try { json = JSON.parse(text); } catch (_) { json = { success: res.ok, raw: text }; } } // Retry on 5xx if (res.status >= 500 && attempt < this.maxRetries) { @@ -635,116 +178,149 @@ class DashCaddyClient { continue; } - // Check envelope + // Envelope check if (json && json.success === false) { - const errorMsg = json.error || `Request failed with status ${res.status}`; - throw new DashCaddyError(errorMsg, res.status, json.code, json); + throw new DashCaddyError(json.error || `Status ${res.status}`, res.status, json.code, json); } - if (!res.ok && !(json && json.success === true)) { - const errorMsg = (json && json.error) || `HTTP ${res.status}`; - throw new DashCaddyError(errorMsg, res.status, json && json.code, json); + throw new DashCaddyError((json && json.error) || `HTTP ${res.status}`, res.status, json && json.code, json); } - - // Success — return the full envelope (minus the success flag is caller's choice) - // We return the spread data: everything except `success` for convenience, - // but also keep success for callers who want to check it. return json || { success: true }; } catch (err) { - // Network errors (AbortError, TypeError) — retry if attempts remain if (err instanceof DashCaddyError) { - // 5xx errors that exhausted retries are re-thrown - if (err.statusCode >= 500 && attempt < this.maxRetries) { - lastError = err; - await this._backoff(attempt); - continue; - } + if (err.statusCode >= 500 && attempt < this.maxRetries) { lastError = err; await this._backoff(attempt); continue; } throw err; } - - // Network-level error lastError = err; - if (attempt < this.maxRetries) { - await this._backoff(attempt); - continue; - } - + if (attempt < this.maxRetries) { await this._backoff(attempt); continue; } throw new DashCaddyError( - err.name === 'AbortError' - ? `Request timeout after ${this.timeout}ms` - : `Network error: ${err.message}`, - 0, - 'NETWORK_ERROR', - { originalError: err.message } + err.name === 'AbortError' ? `Timeout after ${this.timeout}ms` : `Network error: ${err.message}`, + 0, 'NETWORK_ERROR', { originalError: err.message } ); } } - - // Should not reach here, but guard just in case throw lastError || new DashCaddyError('Request failed after all retries', 0); } - /** - * Exponential backoff with jitter. - * @param {number} attempt - Current attempt number (1-based). - * @returns {Promise} - * @private - */ - async _backoff(attempt) { - const delay = RETRY_BACKOFF_BASE_MS * Math.pow(2, attempt - 1); - const jitter = Math.random() * delay * 0.3; - await new Promise((resolve) => setTimeout(resolve, delay + jitter)); + /** Low-level fetch with timeout. @private */ + async _fetch(url, method, headers, body, externalSignal) { + const fetchFn = this._fetchImpl || fetch; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeout); + if (externalSignal) { + if (externalSignal.aborted) controller.abort(); + else externalSignal.addEventListener('abort', () => controller.abort(), { once: true }); + } + try { + return await fetchFn(url, { + method, headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + signal: controller.signal, + }); + } finally { clearTimeout(timer); } } - // ── Auth Helpers ── + /** Exponential backoff with jitter. @private */ + async _backoff(attempt) { + const delay = RETRY_BASE_MS * Math.pow(2, attempt - 1); + await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.3)); + } - /** - * Exchange an API key for a JWT token. - * POST /api/v1/auth/jwt - * @param {string} [apiKey] - Override the client's API key. - * @returns {Promise} - */ + // ── Auth & System Helpers ── + + /** Exchange API key for JWT. POST /api/v1/auth/jwt */ async exchangeJwt(apiKey) { const key = apiKey || this.apiKey; - if (!key) throw new DashCaddyError('API key required for JWT exchange', 0, 'NO_API_KEY'); + if (!key) throw new DashCaddyError('API key required', 0, 'NO_API_KEY'); return this._request('POST', '/auth/jwt', { body: { apiKey: key }, _skipCsrf: true }); } - /** - * Verify a TOTP code to establish a session. - * POST /api/v1/totp/verify - * @param {string} code - TOTP code from authenticator. - * @returns {Promise} Includes csrfToken and ssoToken on success. - */ + /** Verify TOTP and establish session. POST /api/v1/totp/verify */ async verifyTotp(code) { const res = await this._request('POST', '/totp/verify', { body: { code }, _skipCsrf: true }); - // Cache the CSRF token returned after TOTP login - if (res.csrfToken) { - this._csrfToken = res.csrfToken; - } + if (res.csrfToken) this._csrfToken = res.csrfToken; return res; } - /** - * Get the current API version. GET /api/v1/version - * @returns {Promise} - */ - async version() { - return this._request('GET', '/version'); - } + /** Get API version. GET /api/v1/version */ + async version() { return this._request('GET', '/version'); } - /** - * Get API metrics summary. GET /api/v1/metrics - * @returns {Promise} - */ - async metrics() { - return this._request('GET', '/metrics'); - } + /** Get metrics summary. GET /api/v1/metrics */ + async metrics() { return this._request('GET', '/metrics'); } } -// ── Exports ──────────────────────────────────────────────────── +// ── Resource Specs ───────────────────────────────────────────── +// [methodName, httpMethod, pathTemplate, hasBody] +// Path params (:id) are filled from positional string/number args. +// For hasBody=true, the arg after path params is the body. +// For hasBody=false, an object arg after path params is treated as query params. + +const SERVICES_SPEC = [ + ['list', 'GET', '/services', false], + ['status', 'GET', '/services/status', false], + ['create', 'POST', '/services', true], + ['updateAll', 'PUT', '/services', true], + ['delete', 'DELETE', '/services/:id', false], + ['triggerUpdate', 'POST', '/services/update', true], +]; + +const CONTAINERS_SPEC = [ + ['discover', 'GET', '/containers/discover', false], + ['logs', 'GET', '/containers/:id/logs', false], + ['resources', 'GET', '/containers/:id/resources', false], + ['checkUpdate', 'GET', '/containers/:id/check-update', false], + ['start', 'POST', '/containers/:id/start', true], + ['stop', 'POST', '/containers/:id/stop', true], + ['restart', 'POST', '/containers/:id/restart', true], + ['update', 'POST', '/containers/:id/update', true], + ['remove', 'DELETE', '/containers/:id', false], +]; + +const HEALTH_SPEC = [ + ['get', 'GET', '/health', false, true], + ['live', 'GET', '/health/live', false, true], + ['ready', 'GET', '/health/ready', false, true], + ['services', 'GET', '/health/services', false], + ['cached', 'GET', '/health/cached', false], + ['service', 'GET', '/health/service/:id', false], + ['ca', 'GET', '/health/ca', false], +]; + +const DNS_SPEC = [ + ['providers', 'GET', '/dns/providers', false], + ['providerStatus', 'GET', '/dns/provider/status', false], + ['createRecord', 'POST', '/dns/record', true], + ['createUniversal', 'POST', '/dns/universal/record', true], + ['deleteRecord', 'DELETE', '/dns/record', true], + ['resolve', 'GET', '/dns/resolve', false], + ['credentials', 'GET', '/dns/credentials', false], + ['setCredentials', 'POST', '/dns/credentials', true], + ['propagation', 'GET', '/dns/propagation/:domain', false], +]; + +const BACKUPS_SPEC = [ + ['getConfig', 'GET', '/backups/config', false], + ['updateConfig', 'POST', '/backups/config', true], + ['execute', 'POST', '/backups/execute', true], + ['history', 'GET', '/backups/history', false], + ['storageInfo', 'GET', '/backups/storage-info', false], + ['restore', 'POST', '/backups/restore/:backupId', true], + ['files', 'GET', '/backups/files', false], +]; + +const CONFIG_SPEC = [ + ['get', 'GET', '/config', false], + ['update', 'POST', '/config', true], +]; + +const MONITORING_SPEC = [ + ['stats', 'GET', '/monitoring/stats', false], + ['containerStats', 'GET', '/monitoring/stats/:containerId', false], + ['history', 'GET', '/monitoring/history/:containerId', false], + ['alertConfig', 'GET', '/monitoring/alerts/config', false], + ['updateAlertConfig','POST', '/monitoring/alerts/config', true], + ['alerts', 'GET', '/monitoring/alerts', false], +]; module.exports = { DashCaddyClient, DashCaddyError }; -module.exports.DashCaddyClient = DashCaddyClient; -module.exports.DashCaddyError = DashCaddyError;