拡張機能からのWorkfrontおよびFusion APIの呼び出し
FusionのコンテキストリファレンスはサインインユーザーのIMS トークンを提供するので、次の自然なステップはWorkfrontまたはFusion APIを呼び出して実際のデータを表示することです。 これはCORSが原因で不可能です。 この記事では、App Builder ランタイムアクションをサーバーサイドプロキシとして使用して、その制限を回避する方法を説明します。また、例(イベントサブスクリプションダッシュボード)を含みます。
ダイレクトブラウザーコール(CORS)が失敗する理由
表示されるUIは、AdobeのCDN (https://<your-app>.adobeio-static.net)から提供される<iframe>で実行されます。 そのページがdifferent オリジン上のWorkfrontまたはFusion APIにfetch(...)を実行すると、ブラウザーはクロスオリジン リソース共有(CORS)を適用します。 APIがCDN オリジンに対してAccess-Control-Allow-Originを明示的に返さない限り、ブラウザーは応答をブロックします。 これらのAPIは任意の拡張機能のオリジンを許可リストに加えるしないので、ゲストからのダイレクトコールは失敗します。
CORSについて詳しくは、CORSを参照してください。
CORSなしで独自のランタイムアクションを呼び出す
この場合の修正は、CORSを使用せずに独自のランタイムアクションを呼び出すことです。
App Builder アプリケーションには、Adobe I/O Runtimeでサーバーサイドで実行される小さなサーバーレス関数であるランタイムアクションが含まれます。 サーバー間の呼び出しは、ブラウザーのCORSの対象ではありません。 また、アクションはアプリの一部であるため、ゲストは相対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:アクションの宣言
ランタイムアクションは、拡張機能のruntimeManifestの下のapp.config.yamlで宣言されます。 拡張機能の横に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で説明した厳格なターゲット許可リストと組み合わせると、カスタムヘッダーを渡す必要があるプロキシの信頼性の高いパスになります。
trueをお試しください。 トークンが有効で、他の場所で機能するので説明できない401が表示された場合は、false およびに切り替えて、ベアラーのチェックと移動を許可リストに加えるし、セキュリティが上流で適用されるようにします。手順2:許可リストに加えるしたプロキシのアクションの記述
src/fusion-nav-organization-1/actions/wf-proxy/index.jsを作成します。 2つのルールにより、アクションをオープンリレーとして使用できないようにアップストリームターゲットを許可リストに加えるし、アップストリームに転送される必要なベアラートークンを設定します。
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から、相対(同一生成元) URLを持つアクションをfetchし、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=...)で動作します。
401/400を返した場合は、まずこれらのヘッダーを再確認してください。リストの結果をページネーション
Fusion v3 リストエンドポイント(フック、シナリオ)は、セット全体ではなく、一度に1つの ページ を返します。 応答は次のようになります。
{
"items": [ /* ...this page of records... */ ],
"_page": { "start": 0, "limit": 100, "total": 342 }
}
レコードは items 未満で、ページネーション メタデータは _page 未満です。 start (オフセット)およびlimit (ページサイズ)クエリパラメーターを使用してページを作成します。 上記のプロキシは両方を通過するので、すべてを収集するまでループしてゲストのページを作成します。
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;
}
ブラウザーからページングを維持する場合は、ランタイムアクション内で同じループを実行し、1つの応答で結合されたitems配列を返します。 いずれにしても、最初のページが結果セット全体であると仮定しないでください。 複数のページのフックを持つチームは、そうでない場合、シナリオが欠落しているように見えます。
セキュリティチェックリスト
- 上流を許可リストに加えるします。 生のクライアント入力からターゲット URLを作成しないでください。 手順2に従って、短い
targetキーを固定URLにマッピングします。 これにより、あなたの行動がオープンなリレーになるのを防ぎます。 - アクションでベアラートークンを要求し、アップストリームに転送します。 WorkfrontとFusionがユーザーの権限を適用します。
- トークンを記録しません。
imsTokenは資格情報です。LOG_LEVELさんがstringParametersさんの印刷物に注意してください。 - 信頼できるAdobeおよびWorkfront ホストに転送できるのは、HTTPS経由のみです。
動作した例:イベント購読ダッシュボード
デモダッシュボードは、3つのソースを結合して、一致するFusion シナリオが正常かどうかをWorkfront イベントサブスクリプションごとに示します。
fetchSubscriptions()→ Workfront イベント サブスクリプション (受信/渡されたカウンターあり)。- アクティブなチームの
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;
}
例をいくつか紹介します。 同じプロキシパターンが、必要なWorkfrontまたはFusion APIでも機能します。