從您的擴充功能呼叫Workfront和Fusion API
Fusion內容參考會提供您登入使用者的IMS權杖,因此自然而然的下一步是呼叫Workfront或Fusion API並顯示真實資料。 這是因為CORS。 本文說明如何使用App Builder執行階段動作做為伺服器端Proxy來繞過此限制,並包含範例(事件訂閱儀表板)。
為何直接瀏覽器呼叫失敗(CORS)
您的可見UI會在Adobe的CDN (https://<your-app>.adobeio-static.net)所提供的<iframe>中執行。 當該頁面對 不同 來源上的Workfront或Fusion API執行fetch(...)操作時,瀏覽器會強制執行跨來源資源共用(CORS)。 除非API針對您的CDN來源明確傳回Access-Control-Allow-Origin,否則瀏覽器會封鎖回應。 這些API不允許列出任意擴充功能來源,因此來自訪客的直接呼叫會失敗。
如需CORS的相關資訊,請參閱CORS。
在沒有CORS的情況下呼叫您自己的執行階段動作
此問題的修正是呼叫您自己的執行階段動作,而不使用CORS。
App Builder應用程式可包含執行階段動作,這些是在伺服器端Adobe I/O Runtime上執行的小型無伺服器函式。 伺服器對伺服器呼叫不受瀏覽器CORS限制。 而且由於動作屬於應用程式的一部分,訪客可使用相對URL呼叫它,該URL為相同來源,因此不會遭到封鎖。
Guest UI (browser, adobeio-static.net)
│ fetch('/api/v1/web/<app>/wf-proxy?...') ← relative = same-origin, no CORS
▼
Your runtime action (Adobe I/O Runtime, server-side)
│ fetch('https://fusion.adobe.com/api/v3/...') ← server-to-server, no CORS
▼
Workfront / Fusion API
動作會從訪客接收使用者的IMS權杖並將其轉送至上游,因此仍會代表使用者使用其許可權發出呼叫。
步驟1:宣告動作
執行階段動作會在app.config.yaml中宣告於擴充功能的runtimeManifest之下。 在您的擴充功能旁新增wf-proxy動作:
extensions:
fusion/nav-organization/1:
$include: src/fusion-nav-organization-1/ext.config.yaml
runtimeManifest:
packages:
fusion-uix-guest: # ← your package name; part of the action URL
license: Apache-2.0
actions:
wf-proxy:
function: src/fusion-nav-organization-1/actions/wf-proxy/index.js
web: 'yes' # exposes it at /api/v1/web/<package>/wf-proxy
runtime: nodejs:22
inputs:
LOG_LEVEL: debug
annotations:
require-adobe-auth: false # see note below
final: true
此動作在下列位置變為可存取:
/api/v1/web/<package>/<action> e.g. /api/v1/web/fusion-uix-guest/wf-proxy
require-adobe-auth: true與false
此註解會控制Adobe的閘道是否在您的動作執行前驗證IMS權杖。
true:安全預設值。 閘道會拒絕未驗證的呼叫。 不過,驗證器會嚴格限制所預期的標頭,且可能會拒絕請求或捨棄上游呼叫所需的自訂標頭。 如果發生這種狀況,即使您的權杖有效,也會顯示為401。false:略過閘道檢查。 然後您的動作會公開叫用,因此您 必須 自己強制執行授權。 動作中需要Authorization持有者,如果遺失則拒絕,然後向上游轉送,由Workfront和Fusion驗證。 結合步驟2中所述的嚴格目標允許清單,這是需要傳遞自訂標題的Proxy的可靠路徑。
true。 如果您看見無法說明的401,因為權杖有效且在其他位置運作,請切換至false 且保留動作中的持有者支票與允許清單,這樣安全性仍會強制上游執行。步驟2:為已加入允許清單的Proxy寫入動作
建立src/fusion-nav-organization-1/actions/wf-proxy/index.js。 有兩個規則可保持此安全性:上游目標的允許清單(此動作不可作為開放轉送)以及轉送上游的必要持有人權杖。
const fetch = require('node-fetch')
const { Core } = require('@adobe/aio-sdk')
const { errorResponse, getBearerToken, checkMissingRequestInputs } = require('../utils')
// Page-through query params (see "Paginate list results" below).
const pageQuery = (p) => {
const q = new URLSearchParams()
if (p.start != null) q.set('start', p.start)
if (p.limit != null) q.set('limit', p.limit)
return q
}
// Only these upstreams may be reached. Never build the URL from arbitrary input.
const TARGETS = {
subscriptions: {
method: 'GET',
url: () => 'https://<your-wf-host>/attask/eventsubscription/api/v1/subscriptions',
},
hooks: {
method: 'GET',
// Fusion hooks are team-scoped: teamId is a REQUIRED query param (see below).
url: (p) => {
const q = pageQuery(p)
if (p.teamId) q.set('teamId', p.teamId)
return `https://fusion.adobe.com/api/v3/hooks?${q.toString()}`
},
},
scenarios: {
method: 'GET',
url: (p) => {
const q = pageQuery(p)
if (p.fusionOrgId) q.set('organizationId', p.fusionOrgId)
return `https://fusion.adobe.com/api/v3/scenarios?${q.toString()}`
},
},
}
async function main (params) {
const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' })
try {
const missing = checkMissingRequestInputs(params, ['target'], ['Authorization'])
if (missing) return errorResponse(400, missing, logger)
const target = TARGETS[params.target]
if (!target) return errorResponse(400, `unknown target '${params.target}'`, logger)
const token = getBearerToken(params) // reads params.__ow_headers.authorization
const headers = { authorization: `Bearer ${token}`, 'content-type': 'application/json' }
if (params.orgId) headers['x-gw-ims-org-id'] = params.orgId // Adobe IMS org id
if (params.fusionOrgId) headers['x-organization-id'] = params.fusionOrgId // Fusion tenant id
if (params.teamId) headers['x-team-id'] = params.teamId // Fusion team id
const res = await fetch(target.url(params), { method: target.method, headers })
const text = await res.text()
let body
try { body = JSON.parse(text) } catch (e) { body = text }
if (!res.ok) {
return { statusCode: res.status, body: { error: `upstream ${res.status}`, target: params.target, details: body } }
}
return { statusCode: 200, body }
} catch (error) {
logger.error(error)
return errorResponse(500, 'server error: ' + error.message, logger)
}
}
exports.main = main
getBearerToken、errorResponse和checkMissingRequestInputs來自產生的actions/utils.js,範本將它們支撐在那裡。 getBearerToken讀取params.__ow_headers.authorization,閘道會將傳入的Authorization標頭放在這個位置。
步驟3:從訪客呼叫動作
從您的UI中,fetch具有相對(相同來源) URL的動作,並以持有者身分傳送IMS權杖。 以查詢引數的形式傳遞上游所需的組織和團隊ID。
const PROXY_URL = "/api/v1/web/fusion-uix-guest/wf-proxy";
async function callProxy(target, token, { imsOrgId, fusionOrgId, teamId, start, limit } = {}) {
const params = new URLSearchParams({ target });
if (imsOrgId) params.set("orgId", imsOrgId); // → x-gw-ims-org-id
if (fusionOrgId) params.set("fusionOrgId", fusionOrgId); // → x-organization-id
if (teamId) params.set("teamId", teamId); // → x-team-id
if (start != null) params.set("start", start); // pagination offset
if (limit != null) params.set("limit", limit); // pagination page size
const res = await fetch(`${PROXY_URL}?${params.toString()}`, {
headers: { authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`${target} request failed: ${res.status}`);
return res.json();
}
從內容取得token、imsOrgId、fusionOrgId和teamId:
const token = connection.sharedContext.get("imsToken");
const imsOrgId = connection.sharedContext.get("imsOrgId");
const fusionOrgId = connection.sharedContext.get("organization")?.id; // Fusion tenant id
const teamId = connection.sharedContext.get("team")?.id;
如需內容的相關資訊,請參閱Fusion內容參考。
Fusion v3 API細節
針對https://fusion.adobe.com/api/v3的儀表板適用的內容:
AuthorizationBearer <imsToken>x-organization-idorganization.idx-team-idteam.idx-gw-ims-org-idimsOrgId請注意下列警告:
GET /api/v3/hooks是團隊範圍:teamId是必要的查詢引數 (/api/v3/hooks?teamId=...)。 沒有它,您會得到400。 這表示勾點只會針對作用中團隊回來;以涵蓋組織、回圈團隊和合併。GET /api/v3/scenarios與organizationId(/api/v3/scenarios?organizationId=...)搭配使用。
分頁清單結果
Fusion v3清單端點(鉤點、案例)一次傳回一個頁面,而不是整個集合。 回應如下所示:
{
"items": [ /* ...this page of records... */ ],
"_page": { "start": 0, "limit": 100, "total": 342 }
}
記錄在 items 之下,分頁中繼資料在 _page 之下。 您使用start (位移)和limit (頁面大小)查詢引數來頁面。 以上的Proxy會同時傳遞,因此在收集所有內容之前,請以循環方式在訪客中建立頁面:
const PAGE_LIMIT = 100;
async function fetchAllPages(target, token, opts = {}) {
const all = [];
let start = 0;
// Stop when a page returns fewer than PAGE_LIMIT items, or when _page.total is reached.
for (;;) {
const res = await callProxy(target, token, { ...opts, start, limit: PAGE_LIMIT });
const items = res.items ?? [];
all.push(...items);
const total = res._page?.total;
const done = items.length < PAGE_LIMIT || (total != null && all.length >= total);
if (done) break;
start += PAGE_LIMIT;
}
return all;
}
若您想讓分頁離開瀏覽器,請在執行階段動作內執行相同的回圈,並在一個回應中傳回合併的items陣列。 無論如何,請勿假設第一頁是整個結果集。 如果團隊擁有多頁鉤點,則團隊會看起來像是缺少情境。
安全性檢查清單
- 允許清單上游。 切勿從原始使用者端輸入建構目標URL。 如步驟2所示,將短的
target金鑰對應至固定URL。 這可防止您的動作變成開放式轉送。 - 在動作中需要持有人權杖,並將其向上游轉送。 讓Workfront和Fusion強制執行使用者的許可權。
- 永遠不要記錄權杖。
imsToken是認證。 讓LOG_LEVEL留意stringParameters列印的內容。 - 僅透過HTTPS轉送到信任的Adobe和Workfront主機。
工作範例:事件訂閱儀表板
示範儀表板會聯結三個來源,針對每個Workfront事件訂閱顯示相符的Fusion案例是否狀況良好:
fetchSubscriptions()個→件訂閱(含已接收/通過的計數器)。- 使用中團隊的
fetchHooks(teamId)個→ Fusion鉤點(以fetchAllPages分頁)。 - 組織的
fetchScenarios(fusionOrgId)→ Fusion案例(以fetchAllPages分頁)。
join會連結它們,但值得指出的是:Workfront訂閱和它的Fusion連結指向 不同主機 上的即時狀態,因此它們的URL欄位不是逐位元組相等。 穩定是webhook URL (最後一個路徑區段)結尾的權杖。 符合該結尾語彙基元,而非完整URL。 接著,勾點的scenarioId會符合情境的id:
subscription.targetUrl ──(trailing token)──▶ hook.url
hook.scenarioId ──▶ scenario.id
// Reduce a webhook URL to its trailing token so hosts/bases can differ.
function hookKey(url) {
if (!url) return "";
const path = String(url).trim().toLowerCase().split(/[?#]/)[0].replace(/\/+$/, "");
const i = path.lastIndexOf("/");
return i >= 0 ? path.slice(i + 1) : path;
}
// Index hooks by token, then look each subscription up by the same token.
const hooksByToken = new Map(hooks.map((h) => [hookKey(pick(h, ["url", "address", "targetUrl"], "")), h]));
const hook = hooksByToken.get(hookKey(pick(sub, ["url", "endpointUrl", "targetUrl", "target.url", "callbackUrl"], "")));
狀態衍生自聯結:
- 已中斷:沒有相符的連結,或連結是
gone。 - 篩選:相符,但
passed < received(事件已送達,但在案例執行前已篩選掉)。 - 狀況良好:符合併通過。
由於實際的裝載形狀不同,使用者端會防禦性地對應欄位,對每個欄位嘗試數個候選金鑰,因此API的細微差異不會破壞表格:
function pick(obj, keys, fallback) {
for (const key of keys) {
const value = key.split(".").reduce((acc, part) => (acc == null ? acc : acc[part]), obj);
if (value != null) return value;
}
return fallback;
}
這只是個範例。 相同的Proxy模式適用於您需要的任何Workfront或Fusion API。