diff --git a/services/premiere-plugin-uxp/src/api.js b/services/premiere-plugin-uxp/src/api.js index a0c7eb8..35fe87f 100644 --- a/services/premiere-plugin-uxp/src/api.js +++ b/services/premiere-plugin-uxp/src/api.js @@ -1,7 +1,9 @@ -// Dragonflight API client v2.1.0 -// Wraps UXP fetch() with Bearer auth + manual redirect follow (UXP strips -// Authorization across origins per Adobe security policy). -// Persists serverUrl + apiToken in localStorage. +// Dragonflight API client — v2.1.4 +// UXP fetch() notes: +// • redirect:'manual' is NOT a supported option — UXP auto-follows redirects +// • Authorization is stripped by UXP on cross-origin redirects (security fix) +// • For same-origin API calls: add Bearer header +// • For off-origin URLs (S3 presigned): do NOT add Bearer (function () { const API = {}; @@ -29,8 +31,8 @@ function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); } - // Core request. `urlOrPath` may be a path (/api/...) or absolute URL. - // Bearer header added only for same-server requests. + // Core request — adds Bearer only for same-server URLs. + // No redirect option — UXP handles redirects automatically. API.request = async function (urlOrPath, opts) { opts = opts || {}; const isAbs = /^https?:\/\//i.test(urlOrPath); @@ -43,38 +45,13 @@ return fetch(url, Object.assign({}, opts, { headers })); }; - // Manual redirect follow: UXP strips Authorization on cross-origin - // redirects — so we follow redirects manually and drop Bearer on hop - // to a different host (e.g., proxy URL → presigned S3). - API.requestFollow = async function (urlOrPath, opts) { - opts = opts || {}; - const isAbs = /^https?:\/\//i.test(urlOrPath); - const base = trimUrl(API.state.serverUrl); - let current = isAbs ? urlOrPath : (base + urlOrPath); - - for (let hop = 0; hop < 6; hop++) { - const headers = Object.assign({}, opts.headers || {}); - const sameOrigin = current.startsWith(base); - if (sameOrigin && API.state.apiToken) { - headers['Authorization'] = 'Bearer ' + API.state.apiToken; - } else { - delete headers['Authorization']; - } - const r = await fetch(current, Object.assign({}, opts, { headers, redirect: 'manual' })); - if (r.status >= 200 && r.status < 300) return r; - if (r.status >= 300 && r.status < 400) { - const loc = r.headers.get('location'); - if (!loc) throw new Error('Redirect with no Location header'); - current = /^https?:\/\//i.test(loc) ? loc : new URL(loc, current).toString(); - continue; - } - throw new Error('HTTP ' + r.status); - } - throw new Error('Too many redirects'); + // Fetch an off-origin URL (e.g. presigned S3) — no auth header. + API.requestExternal = async function (url) { + return fetch(url); }; - API.json = async function (path, opts) { - const r = await API.request(path, opts); + API.json = async function (pathOrUrl, opts) { + const r = await API.request(pathOrUrl, opts); if (!r.ok) { const text = await r.text().catch(() => ''); throw new Error('HTTP ' + r.status + (text ? ' — ' + text.slice(0, 200) : '')); @@ -109,7 +86,7 @@ return API.json('/api/v1/assets/' + assetId); }; - // /stream → { url, type, source } + // /stream → { url, type, source } (same-origin proxy video URL) 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'); @@ -118,7 +95,7 @@ return { url: abs, source: data.source || 'proxy' }; }; - // /hires → { url, filename, ext, file_size, type } + // /hires → { url, filename, ext, file_size, type } (presigned S3 URL) 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'); @@ -130,15 +107,6 @@ return API.json('/api/v1/assets/' + assetId + '/live-path'); }; - // Batch trim: POST /api/v1/assets/batch-trim - API.batchTrim = async function (clips) { - return API.json('/api/v1/assets/batch-trim', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ clips }), - }); - }; - // ── Projects ───────────────────────────────────────────────────── API.listProjects = async function () { const data = await API.json('/api/v1/projects'); @@ -187,10 +155,17 @@ }); }; - // ── Jobs ───────────────────────────────────────────────────────── API.getJob = async function (jobId) { return API.json('/api/v1/jobs/' + jobId); }; + API.batchTrim = async function (clips) { + return API.json('/api/v1/assets/batch-trim', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ clips }), + }); + }; + window.API = API; })();