UXP v2.1.4: api.js — remove redirect:manual (not supported in UXP fetch); UXP auto-follows redirects
This commit is contained in:
parent
baa289f6c3
commit
f3a640a7c5
1 changed files with 23 additions and 48 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
// Dragonflight API client v2.1.0
|
// Dragonflight API client — v2.1.4
|
||||||
// Wraps UXP fetch() with Bearer auth + manual redirect follow (UXP strips
|
// UXP fetch() notes:
|
||||||
// Authorization across origins per Adobe security policy).
|
// • redirect:'manual' is NOT a supported option — UXP auto-follows redirects
|
||||||
// Persists serverUrl + apiToken in localStorage.
|
// • 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 () {
|
(function () {
|
||||||
const API = {};
|
const API = {};
|
||||||
|
|
@ -29,8 +31,8 @@
|
||||||
|
|
||||||
function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); }
|
function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); }
|
||||||
|
|
||||||
// Core request. `urlOrPath` may be a path (/api/...) or absolute URL.
|
// Core request — adds Bearer only for same-server URLs.
|
||||||
// Bearer header added only for same-server requests.
|
// No redirect option — UXP handles redirects automatically.
|
||||||
API.request = async function (urlOrPath, opts) {
|
API.request = async function (urlOrPath, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
const isAbs = /^https?:\/\//i.test(urlOrPath);
|
const isAbs = /^https?:\/\//i.test(urlOrPath);
|
||||||
|
|
@ -43,38 +45,13 @@
|
||||||
return fetch(url, Object.assign({}, opts, { headers }));
|
return fetch(url, Object.assign({}, opts, { headers }));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manual redirect follow: UXP strips Authorization on cross-origin
|
// Fetch an off-origin URL (e.g. presigned S3) — no auth header.
|
||||||
// redirects — so we follow redirects manually and drop Bearer on hop
|
API.requestExternal = async function (url) {
|
||||||
// to a different host (e.g., proxy URL → presigned S3).
|
return fetch(url);
|
||||||
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');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
API.json = async function (path, opts) {
|
API.json = async function (pathOrUrl, opts) {
|
||||||
const r = await API.request(path, opts);
|
const r = await API.request(pathOrUrl, opts);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const text = await r.text().catch(() => '');
|
const text = await r.text().catch(() => '');
|
||||||
throw new Error('HTTP ' + r.status + (text ? ' — ' + text.slice(0, 200) : ''));
|
throw new Error('HTTP ' + r.status + (text ? ' — ' + text.slice(0, 200) : ''));
|
||||||
|
|
@ -109,7 +86,7 @@
|
||||||
return API.json('/api/v1/assets/' + assetId);
|
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) {
|
API.getProxyUrl = async function (assetId) {
|
||||||
const data = await API.json('/api/v1/assets/' + assetId + '/stream');
|
const data = await API.json('/api/v1/assets/' + assetId + '/stream');
|
||||||
if (!data || !data.url) throw new Error('Asset has no proxy');
|
if (!data || !data.url) throw new Error('Asset has no proxy');
|
||||||
|
|
@ -118,7 +95,7 @@
|
||||||
return { url: abs, source: data.source || 'proxy' };
|
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) {
|
API.getHiresInfo = async function (assetId) {
|
||||||
const data = await API.json('/api/v1/assets/' + assetId + '/hires');
|
const data = await API.json('/api/v1/assets/' + assetId + '/hires');
|
||||||
if (!data || !data.url) throw new Error('Asset has no hi-res source');
|
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');
|
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 ─────────────────────────────────────────────────────
|
// ── Projects ─────────────────────────────────────────────────────
|
||||||
API.listProjects = async function () {
|
API.listProjects = async function () {
|
||||||
const data = await API.json('/api/v1/projects');
|
const data = await API.json('/api/v1/projects');
|
||||||
|
|
@ -187,10 +155,17 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Jobs ─────────────────────────────────────────────────────────
|
|
||||||
API.getJob = async function (jobId) {
|
API.getJob = async function (jobId) {
|
||||||
return API.json('/api/v1/jobs/' + 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;
|
window.API = API;
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue