Implementação do Arch Panel no FuseWelcome
This commit is contained in:
473
background.js
473
background.js
@@ -14,8 +14,94 @@ const COMCIP_TAB_URL_PATTERN =
|
|||||||
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live*`;
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live*`;
|
||||||
const COMCIP_TIME_ENTRY_TAB_URL_PATTERN =
|
const COMCIP_TIME_ENTRY_TAB_URL_PATTERN =
|
||||||
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live*`;
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live*`;
|
||||||
|
const SPA_FRAME_DNR_RULE_ID = 92001;
|
||||||
|
|
||||||
|
void installSpaFrameHeaderRules();
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
void installSpaFrameHeaderRules();
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
|
void installSpaFrameHeaderRules();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function installSpaFrameHeaderRules() {
|
||||||
|
if (!chrome.declarativeNetRequest?.updateDynamicRules) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.declarativeNetRequest.updateDynamicRules({
|
||||||
|
removeRuleIds: [SPA_FRAME_DNR_RULE_ID],
|
||||||
|
addRules: [
|
||||||
|
{
|
||||||
|
id: SPA_FRAME_DNR_RULE_ID,
|
||||||
|
priority: 1,
|
||||||
|
action: {
|
||||||
|
type: "modifyHeaders",
|
||||||
|
responseHeaders: [
|
||||||
|
{
|
||||||
|
header: "x-frame-options",
|
||||||
|
operation: "remove",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "frame-options",
|
||||||
|
operation: "remove",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "content-security-policy",
|
||||||
|
operation: "remove",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "content-security-policy-report-only",
|
||||||
|
operation: "remove",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
initiatorDomains: ["eeho.fa.us2.oraclecloud.com"],
|
||||||
|
requestDomains: ["spa.oracle.com"],
|
||||||
|
resourceTypes: ["sub_frame"],
|
||||||
|
urlFilter: "||spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message?.type === "ARCH_PANEL_SPA_FRAME_FETCH") {
|
||||||
|
executeSpaFrameFetch({
|
||||||
|
senderTabId: sender?.tab?.id,
|
||||||
|
url: message.url,
|
||||||
|
options: message.options || {},
|
||||||
|
})
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message?.type === "ARCH_PANEL_EXTENSION_FETCH") {
|
||||||
|
executeExtensionFetch(message.url, message.options || {})
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!message ||
|
!message ||
|
||||||
(message.type !== "ARCH_PANEL_COMCIP_POST" &&
|
(message.type !== "ARCH_PANEL_COMCIP_POST" &&
|
||||||
@@ -44,6 +130,393 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function executeExtensionFetch(requestUrl, options = {}) {
|
||||||
|
const method = options.method || "GET";
|
||||||
|
const headers = {
|
||||||
|
...(options.headers || {}),
|
||||||
|
};
|
||||||
|
const fetchOptions = {
|
||||||
|
method,
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.body !== undefined && options.body !== null) {
|
||||||
|
fetchOptions.body = options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(requestUrl, fetchOptions);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: pickExtensionResponseHeaders(response.headers),
|
||||||
|
payload: parseExtensionResponseText(text),
|
||||||
|
debug: {
|
||||||
|
url: requestUrl,
|
||||||
|
headerKeys: Object.keys(headers),
|
||||||
|
extensionFetch: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSpaFrameFetch({ senderTabId, url, options = {} }) {
|
||||||
|
if (!senderTabId) {
|
||||||
|
throw new Error("Unable to locate the current tab for the SPA frame request.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await installSpaFrameHeaderRules();
|
||||||
|
await waitForSpaFramePresent(senderTabId);
|
||||||
|
await ensureSpaBridgeInFrames(senderTabId);
|
||||||
|
await waitForSpaFrameCapturedAuth(senderTabId, url);
|
||||||
|
|
||||||
|
return executeSpaFrameScriptFetch(senderTabId, url, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForSpaFramePresent(tabId) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let lastFrames = "";
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < 45000) {
|
||||||
|
const frames = await getFrameHints(tabId).catch(() => []);
|
||||||
|
const spaFrame = frames.find((frame) => isSpaFrameHint(frame));
|
||||||
|
|
||||||
|
lastFrames = frames
|
||||||
|
.map((frame) => `${frame.frameId}:${frame.url || frame.name || "blank"}`)
|
||||||
|
.slice(0, 8)
|
||||||
|
.join(" | ");
|
||||||
|
|
||||||
|
if (spaFrame) {
|
||||||
|
return spaFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`SPA Workbench iframe was not found.${lastFrames ? ` Frames: ${lastFrames}` : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFrameHints(tabId) {
|
||||||
|
const injectionResults = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId, allFrames: true },
|
||||||
|
world: "MAIN",
|
||||||
|
func: () => ({
|
||||||
|
href: String(window.location.href || ""),
|
||||||
|
name: String(window.name || ""),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (injectionResults || []).map((item) => ({
|
||||||
|
frameId: item.frameId,
|
||||||
|
url: item.result?.href || "",
|
||||||
|
name: item.result?.name || "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpaFrameHint(frame) {
|
||||||
|
const url = String(frame?.url || "");
|
||||||
|
const name = String(frame?.name || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
url.startsWith("https://spa.oracle.com/") ||
|
||||||
|
name === "arch-panel-extension-spa-frame"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureSpaBridgeInFrames(tabId) {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId, allFrames: true },
|
||||||
|
world: "MAIN",
|
||||||
|
files: ["page-bridge.js"],
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForSpaFrameCapturedAuth(tabId, requestUrl) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let lastHeaderKeys = "";
|
||||||
|
let lastSnapshot = null;
|
||||||
|
const requiresWwbToken = String(requestUrl || "").includes("/provider-proxy/wwb-provider/");
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < 90000) {
|
||||||
|
await ensureSpaBridgeInFrames(tabId);
|
||||||
|
const snapshot = await readSpaFrameCapturedHeaders(tabId, requestUrl).catch(() => null);
|
||||||
|
const headerKeys = Object.keys(snapshot || {});
|
||||||
|
|
||||||
|
lastSnapshot = snapshot;
|
||||||
|
lastHeaderKeys = headerKeys.join(", ");
|
||||||
|
|
||||||
|
if (requiresWwbToken && snapshot?.["wwb-provider-authorization"]) {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!requiresWwbToken &&
|
||||||
|
(snapshot?.["wwb-provider-authorization"] ||
|
||||||
|
snapshot?.["spa-ts-authorization"] ||
|
||||||
|
snapshot?.["authorization"])
|
||||||
|
) {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastSnapshot || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSpaFrameCapturedHeaders(tabId, requestUrl) {
|
||||||
|
const injectionResults = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId, allFrames: true },
|
||||||
|
world: "MAIN",
|
||||||
|
args: [requestUrl],
|
||||||
|
func: (url) => {
|
||||||
|
const href = String(window.location.href || "");
|
||||||
|
const frameName = String(window.name || "");
|
||||||
|
const isSpaFrame =
|
||||||
|
href.startsWith("https://spa.oracle.com/") ||
|
||||||
|
frameName === "arch-panel-extension-spa-frame";
|
||||||
|
|
||||||
|
if (!isSpaFrame) {
|
||||||
|
return {
|
||||||
|
matched: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
matched: true,
|
||||||
|
headers:
|
||||||
|
window.__archPanelExtensionBridge?.buildHeaderSnapshot?.({
|
||||||
|
url,
|
||||||
|
includeAllAuth: true,
|
||||||
|
}) || {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchedResult = (injectionResults || [])
|
||||||
|
.map((item) => item.result)
|
||||||
|
.find((result) => result?.matched);
|
||||||
|
|
||||||
|
return matchedResult?.headers || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSpaFrameScriptFetch(tabId, requestUrl, options = {}) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let lastError = "";
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < 45000) {
|
||||||
|
await ensureSpaBridgeInFrames(tabId);
|
||||||
|
const response = await executeSpaFrameScriptFetchOnce(tabId, requestUrl, options).catch(
|
||||||
|
(error) => {
|
||||||
|
lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response?.matched) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(700);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`SPA Workbench frame did not return a result.${lastError ? ` ${lastError}` : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSpaFrameScriptFetchOnce(tabId, requestUrl, options = {}) {
|
||||||
|
const injectionResults = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId, allFrames: true },
|
||||||
|
world: "MAIN",
|
||||||
|
args: [
|
||||||
|
requestUrl,
|
||||||
|
options?.method || "GET",
|
||||||
|
options?.body ?? null,
|
||||||
|
options?.headers || {},
|
||||||
|
],
|
||||||
|
func: async (requestUrl, requestMethod, requestBody, sourceHeaders) => {
|
||||||
|
function isSpaFrame() {
|
||||||
|
const href = String(window.location.href || "");
|
||||||
|
const frameName = String(window.name || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
href.startsWith("https://spa.oracle.com/") ||
|
||||||
|
frameName === "arch-panel-extension-spa-frame"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSpaFrame()) {
|
||||||
|
return {
|
||||||
|
matched: false,
|
||||||
|
frameUrl: window.location.href,
|
||||||
|
frameName: window.name || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = window.__archPanelExtensionBridge;
|
||||||
|
|
||||||
|
if (bridge?.fetchWithCapturedHeaders) {
|
||||||
|
const response = await bridge.fetchWithCapturedHeaders(requestUrl, {
|
||||||
|
method: requestMethod || "GET",
|
||||||
|
headers: sourceHeaders || {},
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
matched: true,
|
||||||
|
...response,
|
||||||
|
debug: {
|
||||||
|
...(response.debug || {}),
|
||||||
|
frameUrl: window.location.href,
|
||||||
|
frameName: window.name || "",
|
||||||
|
spaFrameFetch: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResponseText(text) {
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickResponseHeaders(responseHeaders) {
|
||||||
|
const safeHeaders = {};
|
||||||
|
const exposedHeaders = ["content-type", "x-request-id", "x-oracle-dms-ecid"];
|
||||||
|
|
||||||
|
for (const key of exposedHeaders) {
|
||||||
|
const value = responseHeaders.get(key);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
safeHeaders[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeaders(headers) {
|
||||||
|
const normalized = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(headers || {})) {
|
||||||
|
const normalizedKey = String(key || "").toLowerCase();
|
||||||
|
|
||||||
|
if (!normalizedKey || value === undefined || value === null || value === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized[normalizedKey] = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.accept = normalized.accept || "application/json, text/plain, */*";
|
||||||
|
normalized["x-requested-with"] =
|
||||||
|
normalized["x-requested-with"] || "XMLHttpRequest";
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = normalizeHeaders(sourceHeaders);
|
||||||
|
const normalizedMethod = String(requestMethod || "GET").toUpperCase();
|
||||||
|
const fetchOptions = {
|
||||||
|
method: normalizedMethod,
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") {
|
||||||
|
if (!headers["content-type"]) {
|
||||||
|
headers["content-type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchOptions.body = requestBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(requestUrl, fetchOptions);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
matched: true,
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: pickResponseHeaders(response.headers),
|
||||||
|
payload: parseResponseText(text),
|
||||||
|
debug: {
|
||||||
|
url: requestUrl,
|
||||||
|
frameUrl: window.location.href,
|
||||||
|
frameName: window.name || "",
|
||||||
|
headerKeys: Object.keys(headers),
|
||||||
|
spaFrameFetch: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (injectionResults || [])
|
||||||
|
.map((item) => item.result)
|
||||||
|
.find((result) => result?.matched) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripGenericAuthHeaders(headers = {}) {
|
||||||
|
const nextHeaders = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(headers || {})) {
|
||||||
|
const normalizedKey = String(key || "").toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey === "authorization" ||
|
||||||
|
normalizedKey === "x-id-token" ||
|
||||||
|
normalizedKey === "spa-ts-authorization"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextHeaders[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExtensionResponseText(text) {
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickExtensionResponseHeaders(responseHeaders) {
|
||||||
|
const safeHeaders = {};
|
||||||
|
const exposedHeaders = ["content-type", "x-request-id", "x-oracle-dms-ecid"];
|
||||||
|
|
||||||
|
for (const key of exposedHeaders) {
|
||||||
|
const value = responseHeaders.get(key);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
safeHeaders[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
async function requestComcipFromPage({
|
async function requestComcipFromPage({
|
||||||
url,
|
url,
|
||||||
method = "POST",
|
method = "POST",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Arch Panel Injector",
|
"name": "Arch Panel Injector",
|
||||||
"version": "1.0.73",
|
"version": "1.0.105",
|
||||||
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
|
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cookies",
|
"cookies",
|
||||||
|
"declarativeNetRequestWithHostAccess",
|
||||||
"scripting",
|
"scripting",
|
||||||
"tabs"
|
"tabs"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*",
|
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*",
|
||||||
"https://spa.oracle.com/*"
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
|
"https://spa.oracle.com/*",
|
||||||
|
"https://worklist.oracle.com/*"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js"
|
"service_worker": "background.js"
|
||||||
@@ -20,6 +23,18 @@
|
|||||||
"matches": [
|
"matches": [
|
||||||
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html*"
|
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html*"
|
||||||
],
|
],
|
||||||
|
"js": [
|
||||||
|
"page-bridge.js"
|
||||||
|
],
|
||||||
|
"run_at": "document_start",
|
||||||
|
"all_frames": true,
|
||||||
|
"world": "MAIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html*",
|
||||||
|
"https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*"
|
||||||
|
],
|
||||||
"js": [
|
"js": [
|
||||||
"content-script.js"
|
"content-script.js"
|
||||||
],
|
],
|
||||||
@@ -32,6 +47,7 @@
|
|||||||
"page-bridge.js"
|
"page-bridge.js"
|
||||||
],
|
],
|
||||||
"matches": [
|
"matches": [
|
||||||
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
"https://spa.oracle.com/*"
|
"https://spa.oracle.com/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -91,6 +107,38 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
278
page-bridge.js
278
page-bridge.js
@@ -33,6 +33,21 @@
|
|||||||
scopedHeaders: new Map(),
|
scopedHeaders: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getSafeTargetOrigin() {
|
||||||
|
const origin = String(window.location?.origin || "");
|
||||||
|
|
||||||
|
return origin && origin !== "null" ? origin : "*";
|
||||||
|
}
|
||||||
|
|
||||||
|
function safePostMessage(payload, targetWindow = window, targetOrigin = getSafeTargetOrigin()) {
|
||||||
|
try {
|
||||||
|
targetWindow.postMessage(payload, targetOrigin);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nativeFetch = window.fetch.bind(window);
|
const nativeFetch = window.fetch.bind(window);
|
||||||
const originalOpen = window.XMLHttpRequest.prototype.open;
|
const originalOpen = window.XMLHttpRequest.prototype.open;
|
||||||
const originalSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
|
const originalSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
|
||||||
@@ -183,6 +198,33 @@
|
|||||||
mergeIntoBucket(bucket, headers);
|
mergeIntoBucket(bucket, headers);
|
||||||
bridgeState.scopedHeaders.set(scopeKey, bucket);
|
bridgeState.scopedHeaders.set(scopeKey, bucket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishCapturedAuthHeaders(url, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishCapturedAuthHeaders(url, headers) {
|
||||||
|
const authHeaders = {};
|
||||||
|
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
if (isAuthorizationLikeHeader(key)) {
|
||||||
|
authHeaders[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Object.keys(authHeaders).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
safePostMessage(
|
||||||
|
{
|
||||||
|
source: MESSAGE_SOURCE,
|
||||||
|
type: "ARCH_PANEL_WORKBENCH_AUTH_HEADERS",
|
||||||
|
url: normalizeUrl(url),
|
||||||
|
headers: authHeaders,
|
||||||
|
},
|
||||||
|
window.top,
|
||||||
|
"*"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHeaders(url, input, body) {
|
function buildHeaders(url, input, body) {
|
||||||
@@ -209,6 +251,14 @@
|
|||||||
merged.set(key, value);
|
merged.set(key, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (activeScopeKeys.includes("scope:provider-proxy")) {
|
||||||
|
collectStoredAuthHeaders().forEach((value, key) => {
|
||||||
|
if (!merged.has(key)) {
|
||||||
|
merged.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!merged.has("accept")) {
|
if (!merged.has("accept")) {
|
||||||
merged.set("accept", "application/json, text/plain, */*");
|
merged.set("accept", "application/json, text/plain, */*");
|
||||||
}
|
}
|
||||||
@@ -285,11 +335,219 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
collectStoredAuthHeaders().forEach((value, key) => {
|
||||||
|
if (!snapshot.has(key)) {
|
||||||
|
snapshot.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.fromEntries(snapshot.entries());
|
return Object.fromEntries(snapshot.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectStoredAuthHeaders() {
|
||||||
|
const headers = new Map();
|
||||||
|
const candidates = [];
|
||||||
|
|
||||||
|
for (const storage of [window.localStorage, window.sessionStorage]) {
|
||||||
|
try {
|
||||||
|
for (let index = 0; index < storage.length; index += 1) {
|
||||||
|
const key = storage.key(index) || "";
|
||||||
|
const value = storage.getItem(key) || "";
|
||||||
|
|
||||||
|
if (key || value) {
|
||||||
|
candidates.push([key, value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore storage access restrictions.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of candidates) {
|
||||||
|
collectAuthFromStorageEntry(headers, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAuthFromStorageEntry(headers, key, value) {
|
||||||
|
const normalizedKey = String(key || "").toLowerCase();
|
||||||
|
const normalizedValue = String(value || "").trim();
|
||||||
|
|
||||||
|
if (!normalizedValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey === "wwb-provider-authorization" ||
|
||||||
|
normalizedKey.includes("wwb-provider-authorization")
|
||||||
|
) {
|
||||||
|
headers.set("wwb-provider-authorization", normalizedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey === "spa-ts-authorization" ||
|
||||||
|
normalizedKey.includes("spa-ts-authorization")
|
||||||
|
) {
|
||||||
|
headers.set("spa-ts-authorization", normalizedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedKey === "authorization") {
|
||||||
|
headers.set("authorization", normalizedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
collectAuthFromTokenString(headers, normalizedValue);
|
||||||
|
|
||||||
|
if (!(normalizedValue.startsWith("{") || normalizedValue.startsWith("["))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
collectAuthFromObject(headers, JSON.parse(normalizedValue));
|
||||||
|
} catch {
|
||||||
|
// Ignore non-JSON values.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAuthFromObject(headers, value, depth = 0) {
|
||||||
|
if (!value || typeof value !== "object" || depth > 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item) => collectAuthFromObject(headers, item, depth + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, entryValue] of Object.entries(value)) {
|
||||||
|
const normalizedKey = String(key || "").toLowerCase();
|
||||||
|
|
||||||
|
if (typeof entryValue === "string" && entryValue.trim()) {
|
||||||
|
collectAuthFromTokenString(headers, entryValue.trim());
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey === "wwb-provider-authorization" ||
|
||||||
|
normalizedKey.includes("wwbprovider") ||
|
||||||
|
normalizedKey.includes("wwb-provider")
|
||||||
|
) {
|
||||||
|
headers.set("wwb-provider-authorization", entryValue.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey === "spa-ts-authorization" ||
|
||||||
|
normalizedKey.includes("spats") ||
|
||||||
|
normalizedKey.includes("spa-ts")
|
||||||
|
) {
|
||||||
|
headers.set("spa-ts-authorization", entryValue.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedKey === "authorization") {
|
||||||
|
headers.set("authorization", entryValue.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectAuthFromObject(headers, entryValue, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAuthFromTokenString(headers, value) {
|
||||||
|
const token = String(value || "").trim();
|
||||||
|
|
||||||
|
if (!looksLikeJwt(token)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = decodeJwtPayload(token);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuedFor = String(payload.issuedFor || payload.aud || "").toUpperCase();
|
||||||
|
const authorities = Array.isArray(payload.authorities)
|
||||||
|
? payload.authorities.map((authority) => String(authority || "").toUpperCase())
|
||||||
|
: [];
|
||||||
|
const scopes = Array.isArray(payload.scope)
|
||||||
|
? payload.scope.map((scope) => String(scope || "").toUpperCase())
|
||||||
|
: String(payload.scope || "")
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((scope) => scope.toUpperCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
const isWwbToken =
|
||||||
|
issuedFor.includes("WWB") ||
|
||||||
|
authorities.some((authority) => authority.startsWith("WLWB.")) ||
|
||||||
|
scopes.some((scope) => scope.includes("WWB") || scope.includes("WLWB"));
|
||||||
|
|
||||||
|
if (isWwbToken && !headers.has("wwb-provider-authorization")) {
|
||||||
|
headers.set("wwb-provider-authorization", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeJwt(value) {
|
||||||
|
return /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(
|
||||||
|
String(value || "").trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeJwtPayload(token) {
|
||||||
|
try {
|
||||||
|
const payloadPart = String(token).split(".")[1];
|
||||||
|
const base64 = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const paddedBase64 = base64.padEnd(
|
||||||
|
base64.length + (4 - (base64.length % 4 || 4)),
|
||||||
|
"="
|
||||||
|
);
|
||||||
|
const json = decodeURIComponent(
|
||||||
|
Array.from(atob(paddedBase64), (char) => {
|
||||||
|
return `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
||||||
|
}).join("")
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithCapturedHeaders(url, options = {}) {
|
||||||
|
const mergedHeaders = buildHeaders(url, options.headers, options.body);
|
||||||
|
const response = await nativeFetch(url, {
|
||||||
|
method: options.method || "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: mergedHeaders,
|
||||||
|
body: options.body,
|
||||||
|
});
|
||||||
|
const rawText = await response.text();
|
||||||
|
let payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = rawText ? JSON.parse(rawText) : null;
|
||||||
|
} catch {
|
||||||
|
payload = rawText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
payload,
|
||||||
|
debug: {
|
||||||
|
url,
|
||||||
|
headerKeys: Array.from(mergedHeaders.keys()),
|
||||||
|
scopeKeys: getScopeKeys(url),
|
||||||
|
spaTabFetch: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__archPanelExtensionBridge = {
|
||||||
|
buildHeaderSnapshot,
|
||||||
|
fetchWithCapturedHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
const trackedFetch = async function archPanelTrackedFetch(input, init) {
|
const trackedFetch = async function archPanelTrackedFetch(input, init) {
|
||||||
const url = normalizeUrl(typeof input === "string" ? input : input?.url);
|
const url = normalizeUrl(typeof input === "string" ? input : input?.url);
|
||||||
|
|
||||||
@@ -373,14 +631,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "ARCH_PANEL_HEADERS") {
|
if (message.type === "ARCH_PANEL_HEADERS") {
|
||||||
window.postMessage(
|
safePostMessage(
|
||||||
{
|
{
|
||||||
source: MESSAGE_SOURCE,
|
source: MESSAGE_SOURCE,
|
||||||
type: "ARCH_PANEL_HEADERS_RESULT",
|
type: "ARCH_PANEL_HEADERS_RESULT",
|
||||||
requestId: message.requestId,
|
requestId: message.requestId,
|
||||||
headers: buildHeaderSnapshot(message.options),
|
headers: buildHeaderSnapshot(message.options),
|
||||||
},
|
}
|
||||||
window.location.origin
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -426,7 +683,7 @@
|
|||||||
payload = rawText;
|
payload = rawText;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.postMessage(
|
safePostMessage(
|
||||||
{
|
{
|
||||||
source: MESSAGE_SOURCE,
|
source: MESSAGE_SOURCE,
|
||||||
type: "ARCH_PANEL_FETCH_RESULT",
|
type: "ARCH_PANEL_FETCH_RESULT",
|
||||||
@@ -439,11 +696,10 @@
|
|||||||
transport: useXhr ? "xhr" : "fetch",
|
transport: useXhr ? "xhr" : "fetch",
|
||||||
},
|
},
|
||||||
payload,
|
payload,
|
||||||
},
|
}
|
||||||
window.location.origin
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.postMessage(
|
safePostMessage(
|
||||||
{
|
{
|
||||||
source: MESSAGE_SOURCE,
|
source: MESSAGE_SOURCE,
|
||||||
type: "ARCH_PANEL_FETCH_RESULT",
|
type: "ARCH_PANEL_FETCH_RESULT",
|
||||||
@@ -455,17 +711,15 @@
|
|||||||
transport: message.options?.transport || "fetch",
|
transport: message.options?.transport || "fetch",
|
||||||
},
|
},
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
},
|
}
|
||||||
window.location.origin
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.postMessage(
|
safePostMessage(
|
||||||
{
|
{
|
||||||
source: MESSAGE_SOURCE,
|
source: MESSAGE_SOURCE,
|
||||||
type: "ARCH_PANEL_BRIDGE_READY",
|
type: "ARCH_PANEL_BRIDGE_READY",
|
||||||
},
|
}
|
||||||
window.location.origin
|
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user