// Drop-in JavaScript client library for the Production Board HTTP API. // // Save this file alongside your source as `prod_client.js` and import // the operation functions you need: // // import { setToken, account_list, account_create } from "./prod_client.js" // setToken("pat_...") // const rows = await account_list({ limit: 20, sort: "-created_at" }) // const fresh = await account_create({ name: "Example GmbH" }) // // CommonJS works too: // // const { setToken, account_list } = require("./prod_client.js") // // Every endpoint exposed by the HTTP API is wrapped as a typed // `_` function. List endpoints take an options object; // get/update/delete endpoints take the row id as their first argument. // // Provided as-is, with no warranty. Vendor freely; modify if you need // to. Targets ES2020+; uses the global `fetch` (Node 18+ / browser). // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site instead. // Local edits will be overwritten by the once-per-day version check. const _g = (typeof globalThis !== 'undefined') ? globalThis : (typeof self !== 'undefined' ? self : this) // `require` only exists in CommonJS, `__filename` only in CommonJS. // Reach for them through the global so a strict ESM build still loads // the file. Both are wrapped in feature checks before any actual call. let _nodeRequire = null try { _nodeRequire = (_g && _g.require) || (typeof require === 'function' ? require : null) } catch (_e) { _nodeRequire = null } let _nodeFilename = null try { _nodeFilename = (typeof __filename !== 'undefined') ? __filename : null } catch (_e) { _nodeFilename = null } // ── Identity (substituted at generation time) ──────────────────────── export const APP_SLUG = "prod" export const APP_NAME = "Production Board" export const MODULE_NAME = "prod_client" export const CLIENT_VERSION = "0.3.12" export const LANGUAGE = "javascript" const DEFAULT_BASE = "https://qtssystem.com" // Per-type metadata baked at generation time. Inspect at runtime when // calling code needs to know the legal filters / sort columns / // max_limit for a model without a second round-trip. export const TYPES = JSON.parse(String.raw`{"board":{"ops":["list","read","create","update","delete"],"create_fields":["name","description","accent","settings","tags","columns"],"update_fields":["name","description","accent","settings","tags","columns"],"allowed_filters":["data__name","data__accent","data__tags","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__name"],"default_sort":"created_at","max_limit":50,"fields":[{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"accent","type":"enum","values":["slate","gray","blue","indigo","violet","fuchsia","amber","orange","emerald","green","rose","red"]},{"name":"settings","type":"dict"},{"name":"description","type":"string","max_len":2000}]},"card":{"ops":["list","read","create","update","delete"],"create_fields":["title","description","status","position","priority","tags","assignee","due_date","board_id"],"update_fields":["title","description","status","position","priority","tags","assignee","due_date","board_id"],"allowed_filters":["data__status","data__priority","data__tags","data__assignee","data__board_id","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__position","data__status","data__priority","data__due_date"],"default_sort":"data__position","max_limit":200,"fields":[{"name":"tags","type":"tags"},{"name":"title","type":"string","max_len":200},{"name":"status","type":"string","max_len":64},{"name":"assignee","type":"string","max_len":64},{"name":"board_id","type":"string","max_len":64,"ref":{"type":"board","owned":true,"optional":true}},{"name":"due_date","type":"string","max_len":32},{"name":"position","type":"number"},{"name":"priority","type":"enum","values":["low","medium","high","critical"]},{"name":"description","type":"string","max_len":4000}]}}`) // ── Token + base-URL configuration ─────────────────────────────────── let _token = null export function setToken(token) { _token = (token == null ? "" : String(token)).trim() || null } export function getToken() { if (_token) return _token const env = _g.process && _g.process.env if (env && env.XCLIENT_TOKEN) return env.XCLIENT_TOKEN return null } function _baseUrl() { const env = _g.process && _g.process.env if (env && env.XCLIENT_BASE_URL) return String(env.XCLIENT_BASE_URL).replace(/\/+$/, "") return DEFAULT_BASE.replace(/\/+$/, "") } // ── Identifier persistence ─────────────────────────────────────────── function _isNode() { return !!(_g.process && _g.process.versions && _g.process.versions.node) } class _MemoryStorage { constructor() { this.map = new Map() } read(key) { return this.map.has(key) ? this.map.get(key) : null } write(key, value) { this.map.set(key, value) } } class _NodeStorage { constructor() { this.dir = null; this.fs = null; this.path = null if (!_nodeRequire) return try { const os = _nodeRequire("os") this.path = _nodeRequire("path") this.fs = _nodeRequire("fs") this.dir = this.path.join(os.homedir(), "." + MODULE_NAME) this.fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 }) } catch (_e) { this.dir = null; this.fs = null; this.path = null } } available() { return this.dir !== null && this.fs !== null } read(key) { if (!this.available()) return null try { const f = this.path.join(this.dir, key + ".json") const obj = JSON.parse(this.fs.readFileSync(f, "utf-8")) return (obj && typeof obj.value === "string") ? obj.value : null } catch (_e) { return null } } write(key, value) { if (!this.available()) return try { const f = this.path.join(this.dir, key + ".json") this.fs.writeFileSync(f, JSON.stringify({ value }), { mode: 0o600 }) } catch (_e) { /* best-effort */ } } } class _WebStorage { read(key) { try { const v = _g.localStorage.getItem(MODULE_NAME + ":" + key); return v == null ? null : v } catch (_e) { return null } } write(key, value) { try { _g.localStorage.setItem(MODULE_NAME + ":" + key, value) } catch (_e) { /* best-effort */ } } } let _storage if (_isNode() && _nodeRequire) { const ns = new _NodeStorage() _storage = ns.available() ? ns : new _MemoryStorage() } else if (_g.localStorage) { _storage = new _WebStorage() } else { _storage = new _MemoryStorage() } function _uuid() { try { if (_g.crypto && typeof _g.crypto.randomUUID === "function") return _g.crypto.randomUUID() } catch (_e) { /* fall through */ } const hex = [] for (let i = 0; i < 16; i++) hex.push(Math.floor(Math.random() * 256).toString(16).padStart(2, "0")) hex[6] = ((parseInt(hex[6], 16) & 0x0f) | 0x40).toString(16).padStart(2, "0") hex[8] = ((parseInt(hex[8], 16) & 0x3f) | 0x80).toString(16).padStart(2, "0") return hex.slice(0, 4).join("") + "-" + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10, 16).join("") } function _deviceId() { const cur = _storage.read("device") if (cur && cur.length >= 32) return cur const fresh = _uuid() _storage.write("device", fresh) return fresh } let _sessionIdCache = null function _sessionId() { if (!_sessionIdCache) _sessionIdCache = _uuid() return _sessionIdCache } function _autoupdateEnabled() { const env = _g.process && _g.process.env if (env) { const v = String(env.XCLIENT_NO_AUTOUPDATE || "").toLowerCase() if (v === "1" || v === "true" || v === "yes") return false } return true } // ── Editor / runtime fingerprint ───────────────────────────────────── function _fingerprint() { const out = {} try { const env = _g.process && _g.process.env if (env) { out.term_program = env.TERM_PROGRAM || null out.editor_env = env.EDITOR || null out.ci = !!(env.CI || env.GITHUB_ACTIONS) out.claude_code = !!(env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) out.codex = !!env.CODEX_HOME const tp = String(env.TERM_PROGRAM || "").toLowerCase() out.vscode = tp === "vscode" && !env.CURSOR_TRACE_ID out.cursor = !!env.CURSOR_TRACE_ID out.antigravity = !!env.ANTIGRAVITY_TRACE_ID out.jetbrains = tp.indexOf("jetbrains") !== -1 out.node_version = (_g.process.versions && _g.process.versions.node) || null out.platform = _g.process.platform || null } else if (_g.navigator) { out.user_agent = _g.navigator.userAgent out.language = _g.navigator.language out.platform = _g.navigator.platform } } catch (_e) { /* best-effort */ } return out } // ── HTTP transport ─────────────────────────────────────────────────── export class ApiError extends Error { constructor(status, message, body) { super("HTTP " + status + ": " + message) this.name = "ApiError" this.status = status this.bodyRaw = body == null ? null : body } } const _RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504]) const _MAX_RETRIES = 3 const _DEFAULT_TIMEOUT = 30000 function _backoff(attempt, retryAfter) { if (retryAfter !== null && retryAfter !== undefined && retryAfter >= 0) return Math.min(retryAfter, 60) * 1000 return Math.min(Math.pow(2, attempt), 60) * 1000 } function _userAgent() { const node = _g.process && _g.process.versions && _g.process.versions.node if (node) return MODULE_NAME + "/" + CLIENT_VERSION + " (lib/" + LANGUAGE + "; node/" + node + ")" return MODULE_NAME + "/" + CLIENT_VERSION + " (lib/" + LANGUAGE + "; web)" } function _sleep(ms) { return new Promise(function (r) { setTimeout(r, ms) }) } let _autoupdateAttempted = false async function _request(method, path, opts) { opts = opts || {} if (!_autoupdateAttempted) { _autoupdateAttempted = true _maybeAutoupdate().catch(function () { /* never throw into the caller */ }) } let url = _baseUrl() + path if (opts.params) { const qs = new URLSearchParams() for (const k of Object.keys(opts.params)) { const v = opts.params[k] if (v === undefined || v === null) continue qs.append(k, String(v)) } const tail = qs.toString() if (tail) url += (url.indexOf("?") !== -1 ? "&" : "?") + tail } let headers = { "Accept": "application/json", "User-Agent": _userAgent(), "X-Client-Channel": "client_" + LANGUAGE, "X-Client-Version": CLIENT_VERSION, "X-Analytics-Device-Id": _deviceId(), "X-Analytics-Session-Id": _sessionId(), } const tok = getToken() if (tok) headers.Authorization = "Bearer " + tok let body if (opts.body !== undefined) { body = JSON.stringify(opts.body) headers["Content-Type"] = "application/json" } let lastErr = null for (let attempt = 0; attempt < _MAX_RETRIES; attempt++) { const controller = new AbortController() const timer = setTimeout(function () { controller.abort() }, opts.timeout || _DEFAULT_TIMEOUT) try { const resp = await _fetchFollowingRedirects(url, { method: method.toUpperCase(), headers: headers, body: body, signal: controller.signal, }) clearTimeout(timer) _maybePersistRefresh(resp.headers) if (_RETRYABLE.has(resp.status) && attempt + 1 < _MAX_RETRIES) { const ra = parseFloat(resp.headers.get("Retry-After") || "") await _sleep(_backoff(attempt, isFinite(ra) ? ra : null)) continue } if (!resp.ok) { const ctype = (resp.headers.get("Content-Type") || "").toLowerCase() let parsed = null try { parsed = ctype.indexOf("application/json") !== -1 ? await resp.json() : await resp.text() } catch (_e) { parsed = null } const msg = (parsed && typeof parsed === "object" && (parsed.detail || parsed.message)) || resp.statusText || "request failed" _emitCallEvent(method, path, resp.status, false) throw new ApiError(resp.status, String(msg), parsed) } _emitCallEvent(method, path, resp.status, true) if (opts.expectEmpty || resp.status === 204) return null const ctype = (resp.headers.get("Content-Type") || "").toLowerCase() if (ctype.indexOf("application/json") !== -1) return await resp.json() return await resp.text() } catch (e) { clearTimeout(timer) if (e instanceof ApiError) throw e lastErr = e if (attempt + 1 < _MAX_RETRIES) { await _sleep(_backoff(attempt, null)) continue } _emitCallEvent(method, path, 0, false) throw new ApiError(0, e && e.message ? e.message : "request failed") } } _emitCallEvent(method, path, 0, false) throw new ApiError(0, lastErr && lastErr.message ? lastErr.message : "request failed") } // Manual redirect-following so we can drop `Authorization` whenever the // new URL points at a different origin. Platform fetch keeps every // header on cross-origin redirects by default - a misconfigured proxy // bouncing requests to an internal host would otherwise leak the PAT. // Cap at 5 hops. async function _fetchFollowingRedirects(url, init) { const maxHops = 5 let currentUrl = url let headers = Object.assign({}, init.headers) let method = init.method let body = init.body for (let hop = 0; hop < maxHops; hop++) { const resp = await fetch(currentUrl, { method: method, headers: headers, body: body, signal: init.signal, redirect: "manual", }) if (resp.status < 300 || resp.status >= 400 || resp.status === 304) return resp const loc = resp.headers.get("Location") if (!loc) return resp let nextUrl try { nextUrl = new URL(loc, currentUrl) } catch (_e) { return resp } let curOrigin = "" try { curOrigin = new URL(currentUrl).origin } catch (_e) { curOrigin = "" } if (nextUrl.origin !== curOrigin && headers.Authorization) { headers = Object.assign({}, headers) delete headers.Authorization } if (resp.status === 303 || ((resp.status === 301 || resp.status === 302) && method !== "GET" && method !== "HEAD")) { method = "GET" body = undefined delete headers["Content-Type"] } currentUrl = nextUrl.toString() } return fetch(currentUrl, { method: method, headers: headers, body: body, signal: init.signal, redirect: "manual" }) } function _maybePersistRefresh(headers) { try { const fresh = headers.get("x-auth-refresh-token") if (fresh) _token = fresh } catch (_e) { /* best-effort */ } } // ── Analytics ──────────────────────────────────────────────────────── let _metaSentOnce = false function _emitCallEvent(method, path, status, ok) { try { const meta = { channel: "client_" + LANGUAGE, client_version: CLIENT_VERSION, module_name: MODULE_NAME, language: LANGUAGE, } if (_g.process && _g.process.versions) { meta.os = _g.process.platform meta.node = _g.process.versions.node } else if (_g.navigator) { meta.os = _g.navigator.platform } if (!_metaSentOnce) { meta.env = _fingerprint(); _metaSentOnce = true } const evt = { type: "client.call", ts_client: Math.floor(Date.now() / 1000), meta: { method: method.toUpperCase(), path: String(path).split("?")[0].slice(0, 128), status: status, ok: !!ok, }, } const body = JSON.stringify({ device_id: _deviceId(), session_id: _sessionId(), events: [evt], meta: meta, }) fetch(_baseUrl() + "/xapi2/analytics/track", { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": _userAgent() }, body: body, keepalive: true, }).catch(function () { /* fire and forget */ }) } catch (_e) { /* fire and forget */ } } // ── Auto-update ────────────────────────────────────────────────────── async function _maybeAutoupdate() { if (!_autoupdateEnabled()) return if (!_isNode() || !_nodeRequire) return // browser bundles can't self-rewrite const last = _storage.read("update_check") if (last && (Date.now() / 1000 - parseInt(last, 10) < 86400)) return try { const probe = await fetch(_baseUrl() + "/xapi2/clients/version", { method: "GET" }) const payload = await probe.json() _storage.write("update_check", String(Math.floor(Date.now() / 1000))) if (!payload || !payload.version || payload.version === CLIENT_VERSION) return const fresh = await fetch(_baseUrl() + "/xapi2/clients/script." + LANGUAGE) const text = await fresh.text() if (!_looksValid(text)) return const fs = _nodeRequire("fs") const here = _nodeFilename if (!here) return const tmp = here + ".tmp." + Date.now() fs.writeFileSync(tmp, text) fs.renameSync(tmp, here) } catch (_e) { /* best-effort */ } } function _looksValid(blob) { if (typeof blob !== "string" || blob.length < 2000) return false for (const m of ["MODULE_NAME", "CLIENT_VERSION", "APP_SLUG", "_request"]) { if (blob.indexOf(m) === -1) return false } return true } // ── Generated per-type wrapper functions ───────────────────────────── // Every model that exposes an op gets one `_` function // below. The runtime above does the heavy lifting; these wrappers just // pin the URL + HTTP verb. export async function board_list(opts) { opts = opts || {} const params = {} if (opts.limit !== undefined) params.limit = opts.limit if (opts.offset !== undefined) params.offset = opts.offset if (opts.sort) params.sort = opts.sort if (opts.q) params.q = opts.q if (opts.filters) for (const k of Object.keys(opts.filters)) { const v = opts.filters[k]; if (v !== undefined && v !== null) params[k] = v } return _request('GET', '/xapi2/data/board', { params }) } export async function board_get(id) { return _request('GET', '/xapi2/data/board/' + id) } export async function board_create(data) { return _request('POST', '/xapi2/data/board', { body: data }) } export async function board_update(id, data) { return _request('PATCH', '/xapi2/data/board/' + id, { body: data }) } export async function board_delete(id) { await _request('DELETE', '/xapi2/data/board/' + id, { expectEmpty: true }) return true } export async function card_list(opts) { opts = opts || {} const params = {} if (opts.limit !== undefined) params.limit = opts.limit if (opts.offset !== undefined) params.offset = opts.offset if (opts.sort) params.sort = opts.sort if (opts.q) params.q = opts.q if (opts.filters) for (const k of Object.keys(opts.filters)) { const v = opts.filters[k]; if (v !== undefined && v !== null) params[k] = v } return _request('GET', '/xapi2/data/card', { params }) } export async function card_get(id) { return _request('GET', '/xapi2/data/card/' + id) } export async function card_create(data) { return _request('POST', '/xapi2/data/card', { body: data }) } export async function card_update(id, data) { return _request('PATCH', '/xapi2/data/card/' + id, { body: data }) } export async function card_delete(id) { await _request('DELETE', '/xapi2/data/card/' + id, { expectEmpty: true }) return true }