// Dragonflight API client. Wraps UXP's fetch() with the Bearer header and // handles the cross-origin-redirect quirk (UXP strips Authorization across // hosts as a security fix — we follow such redirects manually). // // Persists serverUrl + apiToken in localStorage so the panel reconnects on // reopen. (function () { const API = {}; const LS_URL = 'df.uxp.serverUrl'; const LS_TOKEN = 'df.uxp.apiToken'; API.state = { serverUrl: localStorage.getItem(LS_URL) || '', apiToken: localStorage.getItem(LS_TOKEN) || '', connected: false, }; API.save = function () { localStorage.setItem(LS_URL, API.state.serverUrl); localStorage.setItem(LS_TOKEN, API.state.apiToken); }; API.clear = function () { API.state.serverUrl = ''; API.state.apiToken = ''; API.state.connected = false; localStorage.removeItem(LS_URL); localStorage.removeItem(LS_TOKEN); }; function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); } // Core request. `url` may be a path (joined to serverUrl) or absolute. // Auth header is added only for requests to our own serverUrl. API.request = async function (urlOrPath, opts) { opts = opts || {}; const isAbs = /^https?:\/\//i.test(urlOrPath); const base = trimUrl(API.state.serverUrl); const url = isAbs ? urlOrPath : (base + urlOrPath); const headers = Object.assign({}, opts.headers || {}); if (!isAbs || url.indexOf(base) === 0) { if (API.state.apiToken) headers['Authorization'] = 'Bearer ' + API.state.apiToken; } // UXP fetch supports standard options. Don't pass `credentials` — UXP // doesn't ship a cookie jar and warns about unsupported values. return fetch(url, Object.assign({}, opts, { headers })); }; API.json = async function (path, opts) { const r = await API.request(path, opts); if (!r.ok) { const text = await r.text().catch(() => ''); throw new Error('HTTP ' + r.status + (text ? ' — ' + text.slice(0, 200) : '')); } return r.json(); }; // GET /api/v1/auth/me — used as the connect probe. Returns 200 on a valid // bearer, 401 otherwise. API.connect = async function (serverUrl, apiToken) { API.state.serverUrl = trimUrl(serverUrl); API.state.apiToken = String(apiToken || '').trim(); if (!API.state.serverUrl || !API.state.apiToken) { throw new Error('Server URL and API token are required'); } const me = await API.json('/api/v1/auth/me'); API.state.connected = true; API.save(); return me; }; API.disconnect = function () { API.clear(); }; // Asset list. The web UI calls /api/v1/assets?... — we mirror that. API.listAssets = async function (query) { const params = new URLSearchParams(); if (query) params.set('q', query); params.set('limit', '60'); return API.json('/api/v1/assets?' + params.toString()); }; // Resolve a proxy stream URL → returns an absolute URL we can fetch. // /stream returns { url: '/api/v1/assets//video', type: 'mp4', source }. API.getProxyUrl = async function (assetId) { const data = await API.json('/api/v1/assets/' + assetId + '/stream'); if (!data || !data.url) throw new Error('Asset has no proxy'); const u = data.url; const abs = /^https?:\/\//i.test(u) ? u : trimUrl(API.state.serverUrl) + u; return { url: abs, source: data.source || 'proxy' }; }; // Resolve a hi-res URL → returns a presigned S3 URL (off-host). // /hires returns { url, filename, ext, file_size, type: 'hires' }. API.getHiresInfo = async function (assetId) { const data = await API.json('/api/v1/assets/' + assetId + '/hires'); if (!data || !data.url) throw new Error('Asset has no hi-res source'); return data; }; window.API = API; })();