/** ZPLCraft saved-label API client. Save as zplcraft.mjs. Node.js 20+; server-side only. */ export class ZplCraftError extends Error { constructor(status, details, retryAfter = null) { super(typeof details?.error === 'string' ? details.error : `ZPLCraft request failed (${status}).`); this.name = 'ZplCraftError'; this.status = status; this.code = details?.code ?? null; this.fields = Array.isArray(details?.fields) ? details.fields : []; this.requiredScope = details?.required_scope ?? null; this.retryAfter = retryAfter; } } const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function labelPath(id) { if (typeof id !== 'string' || !UUID.test(id)) throw new TypeError('A label UUID is required.'); return `/labels/${id}`; } export class ZplCraftClient { #baseUrl; #apiKey; #fetch; #timeoutMs; constructor({ baseUrl, apiKey, timeoutMs = 30000, fetch: fetchImpl = globalThis.fetch }) { const url = new URL(baseUrl); const local = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname); if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) || url.username || url.password || url.search || url.hash) { throw new TypeError('Use an HTTPS API base URL without credentials, query or fragment.'); } if (typeof apiKey !== 'string' || !/^zplc_[0-9a-f]{48}$/.test(apiKey)) throw new TypeError('A valid ZPLCraft API key is required.'); if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) throw new TypeError('timeoutMs must be between 1 and 2147483647.'); if (typeof fetchImpl !== 'function') throw new TypeError('A fetch implementation is required.'); this.#baseUrl = url.toString().replace(/\/+$/, ''); this.#apiKey = apiKey; this.#fetch = fetchImpl; this.#timeoutMs = timeoutMs; } async #request(path, { body, json = false } = {}) { const response = await this.#fetch(`${this.#baseUrl}${path}`, { method: body === undefined ? 'GET' : 'POST', headers: { 'x-api-key': this.#apiKey, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'error', signal: AbortSignal.timeout(this.#timeoutMs), }); if (!response.ok) { let details; try { details = await response.json(); } catch { details = {}; } const rawRetry = response.headers.get('Retry-After'); const retry = rawRetry && /^\d+$/.test(rawRetry) ? Number(rawRetry) : null; throw new ZplCraftError(response.status, details, retry); } if (json) return response.json(); const contentType = (response.headers.get('Content-Type') || '').split(';')[0].trim(); const data = ['text/plain', 'application/vnd.zebra-zpl'].includes(contentType) ? await response.text() : new Uint8Array(await response.arrayBuffer()); const count = response.headers.get('X-ZPLCraft-Label-Count'); return { data, contentType, outputFormat: response.headers.get('X-ZPLCraft-Output-Format'), labelCount: count && /^\d+$/.test(count) ? Number(count) : null, }; } listLabels({ limit = 50, offset = 0 } = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > 100 || !Number.isInteger(offset) || offset < 0 || offset > 10000) { throw new RangeError('limit must be 1–100 and offset must be 0–10000.'); } return this.#request(`/labels?limit=${limit}&offset=${offset}`, { json: true }); } getLabel(id) { return this.#request(labelPath(id), { json: true }); } getStoredZpl(id) { return this.#request(`${labelPath(id)}/zpl`); } render(id, { variables = {}, format = 'zpl' } = {}) { if (!['zpl', 'png', 'pdf'].includes(format)) throw new TypeError('format must be zpl, png or pdf.'); return this.#request(`${labelPath(id)}/render`, { body: { variables, format } }); } createPrintJob(id, options = {}) { if (options.variables !== undefined && options.records !== undefined) throw new TypeError('Use variables or records, not both.'); return this.#request(`${labelPath(id)}/print`, { body: options }); } }