Commit inicial
This commit is contained in:
24
README.md
Normal file
24
README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Arch Panel Injector
|
||||||
|
|
||||||
|
Extensao Chrome Manifest V3 que adiciona o botao `Arch panel` ao header da pagina Oracle:
|
||||||
|
|
||||||
|
`https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench`
|
||||||
|
|
||||||
|
## O que ela faz
|
||||||
|
|
||||||
|
- roda apenas na pagina alvo do Oracle Workload Workbench
|
||||||
|
- injeta o botao imediatamente a esquerda de `.oj-oal-ux-global-header-content`
|
||||||
|
- reaproveita as cores e a tipografia do proprio header para manter compatibilidade visual
|
||||||
|
- observa mudancas da SPA para recolocar o botao se o layout for remontado
|
||||||
|
|
||||||
|
## Como instalar no Chrome
|
||||||
|
|
||||||
|
1. Abra `chrome://extensions`
|
||||||
|
2. Ative `Developer mode`
|
||||||
|
3. Clique em `Load unpacked`
|
||||||
|
4. Selecione a pasta `C:\Codex\Projects\arch-central-extension-browser`
|
||||||
|
|
||||||
|
## Arquivos principais
|
||||||
|
|
||||||
|
- `manifest.json`: define a extensao e o carregamento do content script
|
||||||
|
- `content-script.js`: valida a URL, encontra o header e injeta o botao
|
||||||
306
background.js
Normal file
306
background.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
const COMCIP_ORIGIN = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const COMCIP_APP_URL =
|
||||||
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live/webApps/Dashboard/?page=shell&shell=main&main=service-requests-detailed-view`;
|
||||||
|
const COMCIP_QUERY_URL =
|
||||||
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPIElastic/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries`;
|
||||||
|
const COMCIP_CLIENT_ID_PROBE_URL =
|
||||||
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/countries`;
|
||||||
|
const COMCIP_TAB_URL_PATTERN =
|
||||||
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live*`;
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
|
if (
|
||||||
|
!message ||
|
||||||
|
(message.type !== "ARCH_PANEL_COMCIP_POST" &&
|
||||||
|
message.type !== "ARCH_PANEL_COMCIP_REQUEST")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestComcipFromPage({
|
||||||
|
url: message.url || COMCIP_QUERY_URL,
|
||||||
|
method: message.method || "POST",
|
||||||
|
payload: message.payload,
|
||||||
|
headers: message.headers,
|
||||||
|
})
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function requestComcipFromPage({ url, method = "POST", payload, headers = {} }) {
|
||||||
|
const target = await getComcipTab();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForComcipTabReady(target.tab.id);
|
||||||
|
await waitForComcipSessionReady(target.tab.id, headers);
|
||||||
|
|
||||||
|
let response = await executeComcipFetch(target.tab.id, url, method, payload, headers);
|
||||||
|
|
||||||
|
if (isAuthorizationFailure(response)) {
|
||||||
|
await delay(1500);
|
||||||
|
await waitForComcipSessionReady(target.tab.id, headers);
|
||||||
|
response = await executeComcipFetch(target.tab.id, url, method, payload, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} finally {
|
||||||
|
if (target.created && target.tab.id) {
|
||||||
|
await chrome.tabs.remove(target.tab.id).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getComcipTab() {
|
||||||
|
const tabs = await chrome.tabs.query({ url: COMCIP_TAB_URL_PATTERN });
|
||||||
|
const existing = tabs.find((tab) => tab.id && !tab.discarded);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return {
|
||||||
|
tab: existing,
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await chrome.tabs.create({
|
||||||
|
url: COMCIP_APP_URL,
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tab: created,
|
||||||
|
created: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForComcipTabReady(tabId) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < 45000) {
|
||||||
|
const tab = await chrome.tabs.get(tabId);
|
||||||
|
const url = String(tab.url || "");
|
||||||
|
|
||||||
|
if (tab.status === "complete" && url.startsWith(COMCIP_ORIGIN)) {
|
||||||
|
await delay(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Timed out while loading the COMCIP origin page.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForComcipSessionReady(tabId, headers = {}) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let lastStatus = "";
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < 60000) {
|
||||||
|
const probe = await executeComcipProbe(tabId, headers).catch((error) => ({
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (probe?.ok && probe?.appBuilderClientId) {
|
||||||
|
return probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastStatus = probe?.status
|
||||||
|
? `${probe.status} ${probe.statusText || ""}`.trim()
|
||||||
|
: probe?.error || "not ready";
|
||||||
|
await delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Timed out while waiting for COMCIP authenticated session (${lastStatus}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeComcipProbe(tabId, headers = {}) {
|
||||||
|
const [injectionResult] = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
world: "MAIN",
|
||||||
|
args: [COMCIP_CLIENT_ID_PROBE_URL, headers || {}],
|
||||||
|
func: async (probeUrl, sourceHeaders) => {
|
||||||
|
const APP_VERSION = "version_1754044416761";
|
||||||
|
const response = await fetch(probeUrl, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
accept: "*/*",
|
||||||
|
authorization: "Session",
|
||||||
|
"accept-language":
|
||||||
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
||||||
|
"x-vb-application-version":
|
||||||
|
sourceHeaders["x-vb-application-version"] || APP_VERSION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
appBuilderClientId: response.headers.get("x-appbuilder-client-id") || "",
|
||||||
|
href: window.location.href,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!injectionResult) {
|
||||||
|
throw new Error("COMCIP probe did not return a result.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return injectionResult.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeComcipFetch(tabId, requestUrl, method, payload, headers) {
|
||||||
|
const [injectionResult] = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
world: "MAIN",
|
||||||
|
args: [requestUrl, method || "POST", payload || null, headers || {}],
|
||||||
|
func: async (requestUrl, requestMethod, requestPayload, sourceHeaders) => {
|
||||||
|
const APP_VERSION = "version_1754044416761";
|
||||||
|
const CLIENT_ID_PROBE_URL =
|
||||||
|
`${window.location.origin}/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/countries`;
|
||||||
|
|
||||||
|
function parseResponseText(text) {
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickResponseHeaders(headers) {
|
||||||
|
const safeHeaders = {};
|
||||||
|
const exposedHeaders = [
|
||||||
|
"content-type",
|
||||||
|
"x-appbuilder-client-id",
|
||||||
|
"x-invalid-appbuilder-client-id",
|
||||||
|
"x-appbuilder-repeat-request",
|
||||||
|
"vb-proxy-status-actual",
|
||||||
|
"vb-proxy-version",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of exposedHeaders) {
|
||||||
|
const value = headers.get(key);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
safeHeaders[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveAppBuilderClientId() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(CLIENT_ID_PROBE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
accept: "*/*",
|
||||||
|
authorization: "Session",
|
||||||
|
"accept-language":
|
||||||
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
||||||
|
"x-vb-application-version":
|
||||||
|
sourceHeaders["x-vb-application-version"] || APP_VERSION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.headers.get("x-appbuilder-client-id") || "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appBuilderClientId =
|
||||||
|
sourceHeaders["x-appbuilder-client-id"] || (await resolveAppBuilderClientId());
|
||||||
|
const requestHeaders = {
|
||||||
|
accept: "*/*",
|
||||||
|
authorization: "Session",
|
||||||
|
"vb-proxy-header-preference": "transient",
|
||||||
|
"x-vb-application-version":
|
||||||
|
sourceHeaders["x-vb-application-version"] || APP_VERSION,
|
||||||
|
"accept-language":
|
||||||
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (appBuilderClientId) {
|
||||||
|
requestHeaders["x-appbuilder-client-id"] = appBuilderClientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedMethod = String(requestMethod || "POST").toUpperCase();
|
||||||
|
const fetchOptions = {
|
||||||
|
method: normalizedMethod,
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: requestHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") {
|
||||||
|
requestHeaders["content-type"] = "application/json";
|
||||||
|
fetchOptions.body = JSON.stringify(requestPayload || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
...fetchOptions,
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: pickResponseHeaders(response.headers),
|
||||||
|
payload: parseResponseText(text),
|
||||||
|
debug: {
|
||||||
|
href: window.location.href,
|
||||||
|
origin: window.location.origin,
|
||||||
|
headerKeys: Object.keys(requestHeaders),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!injectionResult) {
|
||||||
|
throw new Error("COMCIP page script did not return a result.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return injectionResult.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthorizationFailure(response) {
|
||||||
|
if (!response) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadText =
|
||||||
|
typeof response.payload === "string"
|
||||||
|
? response.payload
|
||||||
|
: JSON.stringify(response.payload || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
response.status === 401 ||
|
||||||
|
/401 Authorization Required/i.test(payloadText) ||
|
||||||
|
/Authorization Required/i.test(payloadText)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(milliseconds) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, milliseconds);
|
||||||
|
});
|
||||||
|
}
|
||||||
7343
content-script.js
Normal file
7343
content-script.js
Normal file
File diff suppressed because it is too large
Load Diff
38
manifest.json
Normal file
38
manifest.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Arch Panel Injector",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
|
||||||
|
"permissions": [
|
||||||
|
"cookies",
|
||||||
|
"scripting",
|
||||||
|
"tabs"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*"
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html*"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"content-script.js"
|
||||||
|
],
|
||||||
|
"run_at": "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"page-bridge.js"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"https://spa.oracle.com/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
471
page-bridge.js
Normal file
471
page-bridge.js
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
(() => {
|
||||||
|
if (window.__archPanelPageBridgeInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__archPanelPageBridgeInstalled = true;
|
||||||
|
|
||||||
|
const MESSAGE_SOURCE = "arch-panel-extension";
|
||||||
|
const API_PATH_FRAGMENT = "/oalcrm/web/api/";
|
||||||
|
const EXTRA_CAPTURE_ORIGINS = new Set([
|
||||||
|
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com",
|
||||||
|
]);
|
||||||
|
const SAFE_HEADER_BLOCKLIST = new Set([
|
||||||
|
"accept-encoding",
|
||||||
|
"accept-language",
|
||||||
|
"connection",
|
||||||
|
"content-length",
|
||||||
|
"cookie",
|
||||||
|
"host",
|
||||||
|
"origin",
|
||||||
|
"referer",
|
||||||
|
"sec-ch-ua",
|
||||||
|
"sec-ch-ua-mobile",
|
||||||
|
"sec-ch-ua-platform",
|
||||||
|
"sec-fetch-dest",
|
||||||
|
"sec-fetch-mode",
|
||||||
|
"sec-fetch-site",
|
||||||
|
"user-agent",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const bridgeState = {
|
||||||
|
globalHeaders: new Map(),
|
||||||
|
scopedHeaders: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const nativeFetch = window.fetch.bind(window);
|
||||||
|
const originalOpen = window.XMLHttpRequest.prototype.open;
|
||||||
|
const originalSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
|
||||||
|
const originalSend = window.XMLHttpRequest.prototype.send;
|
||||||
|
|
||||||
|
function isIntegrationOrigin(url) {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
new URL(url, window.location.origin).origin ===
|
||||||
|
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com"
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUrl(input) {
|
||||||
|
try {
|
||||||
|
return new URL(input, window.location.origin).toString();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCapture(url) {
|
||||||
|
if (!url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes(API_PATH_FRAGMENT)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return EXTRA_CAPTURE_ORIGINS.has(new URL(url).origin);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeaders(input) {
|
||||||
|
const normalized = new Map();
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = new Headers(input);
|
||||||
|
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
const lowerKey = key.toLowerCase();
|
||||||
|
|
||||||
|
if (SAFE_HEADER_BLOCKLIST.has(lowerKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowerKey.startsWith("sec-")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.set(lowerKey, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScopeKeys(url) {
|
||||||
|
const normalizedUrl = normalizeUrl(url);
|
||||||
|
|
||||||
|
if (!normalizedUrl) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(normalizedUrl);
|
||||||
|
const path = parsedUrl.pathname;
|
||||||
|
const keys = [`path:${path}`];
|
||||||
|
const providerProxyMatch = path.match(
|
||||||
|
/\/provider-proxy\/([^/]+)\//
|
||||||
|
);
|
||||||
|
const serviceMatch = path.match(
|
||||||
|
/\/provider-proxy\/[^/]+\/service\/([^/]+)\//
|
||||||
|
);
|
||||||
|
|
||||||
|
if (path.includes("/provider-proxy/")) {
|
||||||
|
keys.push("scope:provider-proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes("/identity-management/")) {
|
||||||
|
keys.push("scope:identity-management");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes("/workbench-proxy/")) {
|
||||||
|
keys.push("scope:workbench-proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes("/elastic/")) {
|
||||||
|
keys.push("scope:elastic");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerProxyMatch?.[1]) {
|
||||||
|
keys.push(`provider:${providerProxyMatch[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceMatch?.[1]) {
|
||||||
|
keys.push(`service:${serviceMatch[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.push("scope:api");
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeIntoBucket(bucket, headers) {
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
bucket.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthorizationLikeHeader(key) {
|
||||||
|
const normalizedKey = String(key || "").toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
normalizedKey === "authorization" ||
|
||||||
|
normalizedKey.endsWith("-authorization") ||
|
||||||
|
normalizedKey.includes("token")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberHeaders(url, input) {
|
||||||
|
const headers = normalizeHeaders(input);
|
||||||
|
|
||||||
|
if (headers.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
if (!isAuthorizationLikeHeader(key)) {
|
||||||
|
bridgeState.globalHeaders.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const scopeKey of getScopeKeys(url)) {
|
||||||
|
const bucket = bridgeState.scopedHeaders.get(scopeKey) || new Map();
|
||||||
|
mergeIntoBucket(bucket, headers);
|
||||||
|
bridgeState.scopedHeaders.set(scopeKey, bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHeaders(url, input, body) {
|
||||||
|
const merged = new Headers();
|
||||||
|
const activeScopeKeys = getScopeKeys(url);
|
||||||
|
|
||||||
|
bridgeState.globalHeaders.forEach((value, key) => {
|
||||||
|
merged.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const scopeKey of activeScopeKeys) {
|
||||||
|
const bucket = bridgeState.scopedHeaders.get(scopeKey);
|
||||||
|
|
||||||
|
if (!bucket) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.forEach((value, key) => {
|
||||||
|
merged.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeHeaders(input).forEach((value, key) => {
|
||||||
|
merged.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!merged.has("accept")) {
|
||||||
|
merged.set("accept", "application/json, text/plain, */*");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merged.has("x-requested-with")) {
|
||||||
|
merged.set("x-requested-with", "XMLHttpRequest");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeScopeKeys.includes("scope:provider-proxy")) {
|
||||||
|
const allowedProviderHeaders = new Set([
|
||||||
|
"accept",
|
||||||
|
"authorization",
|
||||||
|
"content-type",
|
||||||
|
"spa-ts-authorization",
|
||||||
|
"wwb-provider-authorization",
|
||||||
|
"x-id-token",
|
||||||
|
"x-requested-with",
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const key of Array.from(merged.keys())) {
|
||||||
|
if (key.endsWith("-authorization") && !allowedProviderHeaders.has(key)) {
|
||||||
|
merged.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (merged.has("wwb-provider-authorization")) {
|
||||||
|
merged.delete("authorization");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIntegrationOrigin(url)) {
|
||||||
|
const allowedIntegrationHeaders = new Set([
|
||||||
|
"accept",
|
||||||
|
"authorization",
|
||||||
|
"content-type",
|
||||||
|
"spa-ts-authorization",
|
||||||
|
"wwb-provider-authorization",
|
||||||
|
"x-id-token",
|
||||||
|
"x-requested-with",
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const key of Array.from(merged.keys())) {
|
||||||
|
if (!allowedIntegrationHeaders.has(key)) {
|
||||||
|
merged.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (merged.has("wwb-provider-authorization")) {
|
||||||
|
merged.delete("authorization");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
merged.delete("content-type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHeaderSnapshot(options = {}) {
|
||||||
|
const snapshot = new Headers();
|
||||||
|
const scopeUrl = options.url || window.location.href;
|
||||||
|
const merged = buildHeaders(scopeUrl, options.headers, options.body);
|
||||||
|
|
||||||
|
merged.forEach((value, key) => {
|
||||||
|
snapshot.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.includeAllAuth) {
|
||||||
|
bridgeState.scopedHeaders.forEach((bucket) => {
|
||||||
|
bucket.forEach((value, key) => {
|
||||||
|
if (isAuthorizationLikeHeader(key)) {
|
||||||
|
snapshot.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(snapshot.entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackedFetch = async function archPanelTrackedFetch(input, init) {
|
||||||
|
const url = normalizeUrl(typeof input === "string" ? input : input?.url);
|
||||||
|
|
||||||
|
if (shouldCapture(url)) {
|
||||||
|
rememberHeaders(url, init?.headers || input?.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nativeFetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.fetch = trackedFetch;
|
||||||
|
|
||||||
|
window.XMLHttpRequest.prototype.open = function archPanelTrackedOpen(method, url, ...rest) {
|
||||||
|
this.__archPanelTrackedUrl = normalizeUrl(url);
|
||||||
|
this.__archPanelTrackedHeaders = new Headers();
|
||||||
|
return originalOpen.call(this, method, url, ...rest);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.XMLHttpRequest.prototype.setRequestHeader = function archPanelTrackedSetRequestHeader(name, value) {
|
||||||
|
if (this.__archPanelTrackedHeaders) {
|
||||||
|
this.__archPanelTrackedHeaders.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalSetRequestHeader.call(this, name, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.XMLHttpRequest.prototype.send = function archPanelTrackedSend(body) {
|
||||||
|
if (shouldCapture(this.__archPanelTrackedUrl)) {
|
||||||
|
rememberHeaders(this.__archPanelTrackedUrl, this.__archPanelTrackedHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalSend.call(this, body);
|
||||||
|
};
|
||||||
|
|
||||||
|
function requestViaOriginalXhr(url, options, headers) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new window.XMLHttpRequest();
|
||||||
|
|
||||||
|
originalOpen.call(xhr, options?.method || "GET", url, true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
xhr.responseType = "text";
|
||||||
|
xhr.timeout = 60000;
|
||||||
|
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
originalSetRequestHeader.call(xhr, key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
resolve({
|
||||||
|
ok: xhr.status >= 200 && xhr.status < 300,
|
||||||
|
status: xhr.status,
|
||||||
|
text: xhr.responseText || "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
reject(new Error("Failed to fetch"));
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onabort = () => {
|
||||||
|
reject(new Error("Request aborted"));
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.ontimeout = () => {
|
||||||
|
reject(new Error(`Timed out while requesting ${url}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
originalSend.call(xhr, options?.body ?? null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("message", async (event) => {
|
||||||
|
if (event.source !== window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = event.data;
|
||||||
|
|
||||||
|
if (!message || message.source !== MESSAGE_SOURCE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "ARCH_PANEL_HEADERS") {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: MESSAGE_SOURCE,
|
||||||
|
type: "ARCH_PANEL_HEADERS_RESULT",
|
||||||
|
requestId: message.requestId,
|
||||||
|
headers: buildHeaderSnapshot(message.options),
|
||||||
|
},
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type !== "ARCH_PANEL_FETCH") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mergedHeaders = buildHeaders(
|
||||||
|
message.url,
|
||||||
|
message.options?.headers,
|
||||||
|
message.options?.body
|
||||||
|
);
|
||||||
|
const useXhr = message.options?.transport === "xhr";
|
||||||
|
const response = useXhr
|
||||||
|
? await requestViaOriginalXhr(message.url, message.options, mergedHeaders)
|
||||||
|
: await (async () => {
|
||||||
|
const activeFetch =
|
||||||
|
typeof window.fetch === "function"
|
||||||
|
? window.fetch.bind(window)
|
||||||
|
: nativeFetch;
|
||||||
|
const fetchResponse = await activeFetch(message.url, {
|
||||||
|
method: message.options?.method || "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: mergedHeaders,
|
||||||
|
body: message.options?.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: fetchResponse.ok,
|
||||||
|
status: fetchResponse.status,
|
||||||
|
text: await fetchResponse.text(),
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const rawText = response.text;
|
||||||
|
let payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = rawText ? JSON.parse(rawText) : null;
|
||||||
|
} catch {
|
||||||
|
payload = rawText;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: MESSAGE_SOURCE,
|
||||||
|
type: "ARCH_PANEL_FETCH_RESULT",
|
||||||
|
requestId: message.requestId,
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
debug: {
|
||||||
|
headerKeys: Array.from(mergedHeaders.keys()),
|
||||||
|
scopeKeys: getScopeKeys(message.url),
|
||||||
|
transport: useXhr ? "xhr" : "fetch",
|
||||||
|
},
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: MESSAGE_SOURCE,
|
||||||
|
type: "ARCH_PANEL_FETCH_RESULT",
|
||||||
|
requestId: message.requestId,
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
debug: {
|
||||||
|
scopeKeys: getScopeKeys(message.url),
|
||||||
|
transport: message.options?.transport || "fetch",
|
||||||
|
},
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: MESSAGE_SOURCE,
|
||||||
|
type: "ARCH_PANEL_BRIDGE_READY",
|
||||||
|
},
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user