Lançamento da versão 1.0.10

This commit is contained in:
2026-08-28 10:39:51 -03:00
parent 904e210ac8
commit 086cad88f5
11 changed files with 476 additions and 65 deletions

View File

@@ -59,7 +59,6 @@ async function installSpaFrameHeaderRules() {
],
},
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",

View File

@@ -6,7 +6,7 @@
TARGET_PARAM_VALUE,
TARGET_APP_URL,
HCM_ORIGIN,
HCM_WELCOME_PATHNAME,
HCM_WELCOME_PATHNAMES,
HCM_MY_INFORMATION_APPS_GROUP_ID,
WORKLIST_ORIGIN,
WORKLIST_SAASUI_PATHNAME,
@@ -57,6 +57,8 @@
getOperationalWeekInfo,
getCalendarRowWeekDate,
parseDatePreservingDateOnly,
decodeJwtPayload,
isJwtExpired,
getErrorMessage,
escapeHtml,
cssEscape,
@@ -134,6 +136,7 @@
storageService,
normalizeCachedExportPayload,
cleanString,
isJwtExpired,
});
const hcmPage = globalThis.ArchPanelHcmPage.create({
HCM_MY_INFORMATION_APPS_GROUP_ID,
@@ -141,6 +144,7 @@
WORKLIST_SAASUI_PATHNAME,
IDS,
cleanString,
isJwtExpired,
hcmTileTemplate: globalThis.ArchPanelTemplates.hcmTile,
onOpenArchPanel: openModal,
writeCachedWorklistToken,
@@ -247,11 +251,15 @@
const currentUrl = new URL(window.location.href);
return (
currentUrl.origin === HCM_ORIGIN &&
currentUrl.pathname === HCM_WELCOME_PATHNAME
isAllowedHcmOrigin(currentUrl) &&
HCM_WELCOME_PATHNAMES.includes(currentUrl.pathname)
);
}
function isAllowedHcmOrigin(url) {
return url?.origin === HCM_ORIGIN;
}
function scheduleSync() {
if (syncFrame) {
return;
@@ -642,6 +650,7 @@
cleanString,
firstNonEmptyString,
getFieldValue,
decodeJwtPayload,
toNumber,
normalizeWorkload,
isWonWorkload,

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Arch Panel",
"version": "1.0.7",
"version": "1.0.10",
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
"permissions": [
"declarativeNetRequestWithHostAccess",
@@ -10,6 +10,7 @@
],
"host_permissions": [
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*",
"https://*.oraclecloud.com/*",
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*",
"https://worklist.oracle.com/*"
@@ -31,8 +32,8 @@
},
{
"matches": [
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html*",
"https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*"
"https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*",
"https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*"
],
"css": [
"src/content/styles.css"
@@ -74,6 +75,7 @@
"page-bridge.js"
],
"matches": [
"https://*.oraclecloud.com/*",
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
]

View File

@@ -81,7 +81,7 @@
}
for (const origin of candidates) {
if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) {
if (isAllowedTopMessageOrigin(origin)) {
return origin;
}
}
@@ -89,6 +89,25 @@
return "";
}
function isAllowedTopMessageOrigin(origin) {
if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) {
return true;
}
try {
const url = new URL(origin);
const hostname = url.hostname.toLowerCase();
return (
url.protocol === "https:" &&
hostname.endsWith(".oraclecloud.com") &&
hostname.includes(".fa.")
);
} catch {
return false;
}
}
function safePostToTop(payload) {
const targetOrigin = getAllowedTopMessageOrigin();

View File

@@ -5,6 +5,7 @@
WORKLIST_SAASUI_PATHNAME,
IDS,
cleanString,
isJwtExpired,
hcmTileTemplate,
onOpenArchPanel,
writeCachedWorklistToken,
@@ -12,12 +13,9 @@
let lastPersistedWorklistIframeUrl = "";
function syncHcmTile() {
const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
const addTile = appsGroup?.querySelector(
".flat-grid-cell.flat-grid-cell-addicon"
);
const insertion = resolveHcmTileInsertion();
if (!addTile?.parentElement) {
if (!insertion?.container) {
document.getElementById(IDS.hcmTile)?.remove();
return;
}
@@ -25,22 +23,48 @@
const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
if (
tile.parentElement !== addTile.parentElement ||
tile.nextElementSibling !== addTile
tile.parentElement !== insertion.container ||
tile.nextElementSibling !== insertion.before
) {
addTile.parentElement.insertBefore(tile, addTile);
insertion.container.insertBefore(tile, insertion.before || null);
}
}
function resolveHcmTileInsertion() {
const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
if (!appsGroup) {
return null;
}
const addTile = appsGroup.querySelector(
".flat-grid-cell.flat-grid-cell-addicon"
);
if (addTile?.parentElement && appsGroup.contains(addTile.parentElement)) {
return {
container: addTile.parentElement,
before: addTile,
};
}
return {
container: appsGroup,
before: null,
};
}
function syncWorklistIframeToken(options = {}) {
for (const frame of document.querySelectorAll("iframe")) {
const tokenPayload = extractWorklistIframeToken(frame);
if (tokenPayload?.token) {
if (!tokenPayload?.token || isWorklistTokenExpired(tokenPayload.token)) {
continue;
}
persistWorklistIframeToken(tokenPayload, options);
return tokenPayload;
}
}
return null;
}
@@ -73,6 +97,10 @@
}
}
function isWorklistTokenExpired(token) {
return typeof isJwtExpired === "function" && isJwtExpired(token, 120);
}
function persistWorklistIframeToken(tokenPayload, options = {}) {
if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
return;

View File

@@ -44,7 +44,8 @@
appState.errorMessage = "";
updateLoadingProgress("Requesting current user", 4);
const user = await fetchCurrentUser();
let user = await fetchCurrentUser();
user = await preserveCachedUserAvatar(user);
updateLoadingProgress("Loading current resource user", 8);
@@ -206,6 +207,48 @@
}
}
async function preserveCachedUserAvatar(user) {
if (cleanString(user?.avatar)) {
return user;
}
const currentDatasetAvatar =
getCachedUserAvatar(user, appState.dataset?.user) ||
getCachedUserAvatar(user, appState.dataset?.exportPayload?.user);
if (currentDatasetAvatar) {
return {
...user,
avatar: currentDatasetAvatar,
};
}
const cachedPayload = await readCachedDataset().catch(() => null);
const persistedAvatar = getCachedUserAvatar(user, cachedPayload?.user);
return persistedAvatar
? {
...user,
avatar: persistedAvatar,
}
: user;
}
function getCachedUserAvatar(user, cachedUser) {
if (!cachedUser) {
return "";
}
const userEmail = normalizeString(user?.userEmail);
const cachedUserEmail = normalizeString(cachedUser?.userEmail);
if (!userEmail || userEmail !== cachedUserEmail) {
return "";
}
return cleanString(cachedUser?.avatar || cachedUser?.imageUrl);
}
function updateLoadingProgress(message, progress) {
appState.loadingMessage = message;
appState.loadingProgress = Math.max(
@@ -312,14 +355,10 @@
const worklistToken = currentFrameToken || (await readCachedWorklistToken());
if (!worklistToken) {
throw new Error(
"Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data."
);
}
appState.sessionAuthHeaders = {};
if (worklistToken) {
try {
const user = await workbenchRepository.fetchWorklistCurrentUser(
worklistToken
);
@@ -327,6 +366,38 @@
appState.sessionAuthHeaders = {};
return user;
} catch (error) {
appState.sessionAuthHeaders = {};
if (!isWorklistAuthError(error)) {
throw error;
}
}
}
try {
const user = await workbenchRepository.fetchSpaCurrentUser();
appState.sessionAuthHeaders = {};
return user;
} catch (error) {
appState.sessionAuthHeaders = {};
throw new Error(
worklistToken
? `Unable to refresh current user. The Worklist token was rejected and the Workload Workbench current-user fallback also failed: ${getErrorMessage(
error
)}. Reload the FuseWelcome page and try Refresh data again.`
: `Unable to resolve the current user. Reload the FuseWelcome page and try Refresh data again. ${getErrorMessage(
error
)}`
);
}
}
function isWorklistAuthError(error) {
return /\b(401|403)\b/.test(getErrorMessage(error));
}
function findWorkloadById(workloadId) {

View File

@@ -4,6 +4,7 @@
storageService,
normalizeCachedExportPayload,
cleanString,
isJwtExpired,
}) {
async function readCachedDataset() {
const record = await storageService.readRecord(
@@ -60,7 +61,7 @@
async function writeCachedWorklistToken(tokenPayload) {
const token = cleanString(tokenPayload?.token);
if (!token) {
if (!token || isCachedWorklistTokenExpired(token)) {
return;
}
@@ -82,8 +83,13 @@
CACHE.worklistTokenKey,
"Unable to read the cached Worklist token."
);
const token = cleanString(record?.token);
return cleanString(record?.token);
return isCachedWorklistTokenExpired(token) ? "" : token;
}
function isCachedWorklistTokenExpired(token) {
return typeof isJwtExpired === "function" && isJwtExpired(token, 120);
}
return Object.freeze({

View File

@@ -8,6 +8,7 @@
cleanString,
firstNonEmptyString,
getFieldValue,
decodeJwtPayload,
toNumber,
normalizeWorkload,
isWonWorkload,
@@ -31,28 +32,45 @@
}).toString();
const payload = await fetchJson(url.toString(), { forceExtension: true });
const userPayload = normalizeWorklistUserPayload(payload);
return normalizeCurrentUserPayload(payload, "Worklist user");
}
async function fetchSpaCurrentUser() {
const payload = await fetchJson(API_ENDPOINTS.currentUser);
return normalizeCurrentUserPayload(payload, "SPA current user");
}
function normalizeCurrentUserPayload(payload, sourceLabel) {
const userPayload = normalizeCurrentUserCandidate(payload);
const firstName = cleanString(
getFieldValue(userPayload, ["FirstName", "firstName", "User.FirstName"])
);
const lastName = cleanString(
getFieldValue(userPayload, ["LastName", "lastName", "User.LastName"])
);
const userEmail = cleanString(
getFieldValue(userPayload, [
"Email",
"email",
"EmailAddress",
"emailAddress",
"User.Email",
"User.email",
"User.EmailAddress",
"FirstName",
"firstName",
"GivenName",
"givenName",
"User.FirstName",
])
);
const lastName = cleanString(
getFieldValue(userPayload, [
"LastName",
"lastName",
"FamilyName",
"familyName",
"User.LastName",
])
);
const userEmail = resolveUserEmail(payload, userPayload);
const displayName = firstNonEmptyString([
`${firstName} ${lastName}`.trim(),
userPayload?.DisplayName,
userPayload?.displayName,
userPayload?.FullName,
userPayload?.fullName,
userPayload?.Name,
userPayload?.name,
userEmail,
]);
@@ -62,7 +80,7 @@
? Object.keys(userPayload).join(", ")
: "";
throw new Error(
`Unable to resolve userEmail from Worklist user response. Available top-level keys: ${Object.keys(
`Unable to resolve userEmail from ${sourceLabel} response. Available top-level keys: ${Object.keys(
payload || {}
).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
);
@@ -71,9 +89,7 @@
return {
userEmail,
name: displayName || userEmail,
avatar: cleanString(
getFieldValue(userPayload, ["ImageURL", "imageUrl", "User.ImageURL"])
),
avatar: resolveUserAvatar(payload, userPayload),
administrator: Boolean(
getFieldValue(userPayload, [
"Administrator",
@@ -84,20 +100,178 @@
};
}
function normalizeWorklistUserPayload(payload) {
const candidate = payload?.User ?? payload?.user ?? payload;
function normalizeCurrentUserCandidate(payload) {
const candidate =
getObjectCandidate(payload?.User) ??
getObjectCandidate(payload?.user) ??
getObjectCandidate(payload?.currentUser) ??
getObjectCandidate(payload?.CurrentUser) ??
getObjectCandidate(payload?.data) ??
getObjectCandidate(payload?.result) ??
getObjectCandidate(payload?.items?.[0]) ??
payload;
if (Array.isArray(candidate)) {
return candidate[0] || {};
}
if (candidate?.User) {
return normalizeWorklistUserPayload(candidate);
const nestedUser =
getObjectCandidate(candidate?.User) ?? getObjectCandidate(candidate?.user);
if (nestedUser) {
return normalizeCurrentUserCandidate(nestedUser);
}
return candidate || {};
}
function getObjectCandidate(value) {
return value && typeof value === "object" ? value : null;
}
function resolveUserEmail(payload, userPayload) {
return firstEmailLikeString([
getFieldValue(userPayload, [
"Email",
"email",
"EmailAddress",
"emailAddress",
"User.Email",
"User.email",
"User.EmailAddress",
"UserName",
"userName",
"username",
"mail",
"Mail",
"userEmail",
"UserEmail",
"PrimaryEmail.EmailAddress",
"primaryEmail.emailAddress",
"ResourceEmailAddress",
"resourceEmailAddress",
"id",
"Id",
]),
...getJwtEmailCandidates(payload?.token),
...getJwtEmailCandidates(payload?.accessToken),
...getJwtEmailCandidates(payload?.idToken),
...getJwtEmailCandidates(userPayload?.token),
]);
}
function resolveUserAvatar(payload, userPayload) {
return firstNonEmptyString([
getFieldValue(userPayload, [
"ImageURL",
"imageUrl",
"imageURL",
"User.ImageURL",
"Picture",
"picture",
"avatar",
"Avatar",
"avatarUrl",
"avatarURL",
"photo",
"photoUrl",
"photoURL",
"profileImageUrl",
"profileImageURL",
"thumbnailUrl",
"thumbnailURL",
"image",
"userImage",
"userImageUrl",
"profile.picture",
"profile.avatar",
"profile.photoUrl",
]),
...getJwtAvatarCandidates(payload?.token),
...getJwtAvatarCandidates(payload?.accessToken),
...getJwtAvatarCandidates(payload?.idToken),
...getJwtAvatarCandidates(userPayload?.token),
]);
}
function getJwtAvatarCandidates(token) {
const tokenPayload = decodeTokenPayload(token);
if (!tokenPayload) {
return [];
}
return [
getFieldValue(tokenPayload, [
"picture",
"avatar",
"avatar_url",
"image",
"photo",
"photo_url",
"thumbnail",
"thumbnail_url",
]),
];
}
function getJwtEmailCandidates(token) {
const tokenPayload = decodeTokenPayload(token);
if (!tokenPayload) {
return [];
}
return [
getFieldValue(tokenPayload, [
"email",
"Email",
"sub",
"prn",
"upn",
"user_name",
"username",
"preferred_username",
"unique_name",
"mail",
"userEmail",
"UserEmail",
]),
];
}
function decodeTokenPayload(token) {
const normalizedToken = cleanTokenValue(token);
if (!normalizedToken || typeof decodeJwtPayload !== "function") {
return null;
}
return decodeJwtPayload(normalizedToken);
}
function cleanTokenValue(value) {
const normalized = cleanString(value);
if (!normalized) {
return "";
}
return normalized.replace(/^Bearer\s+/i, "");
}
function firstEmailLikeString(values) {
for (const value of values) {
const normalized = cleanString(value);
if (/@/.test(normalized)) {
return normalized;
}
}
return "";
}
async function fetchAllCustomers(userEmail) {
const limit = 49;
let offset = 0;
@@ -277,6 +451,7 @@
return {
fetchWorklistCurrentUser,
fetchSpaCurrentUser,
fetchAllCustomers,
fetchCustomerWorkloads,
fetchWorkloadActions,

View File

@@ -172,7 +172,9 @@
const response = await new Promise((resolve, reject) => {
const timeout = window.setTimeout(() => {
pendingBridgeRequests.delete(requestId);
reject(new Error(`Timed out while requesting ${requestUrl}`));
reject(
new Error(`Timed out while requesting ${redactUrlForError(requestUrl)}`)
);
}, 60000);
pendingBridgeRequests.set(requestId, {
@@ -229,7 +231,9 @@
`${
payloadMessage ||
response.error ||
`Request failed (${response.status}) for ${requestUrl}`
`Request failed (${response.status}) for ${redactUrlForError(
requestUrl
)}`
}${debugSuffix}`
);
}
@@ -297,7 +301,9 @@
});
if (!response || typeof response !== "object") {
throw new Error(`No response received for ${requestUrl}`);
throw new Error(
`No response received for ${redactUrlForError(requestUrl)}`
);
}
if (
@@ -328,8 +334,8 @@
const debugFrameUrl = cleanString(response.debug?.frameUrl);
const debugUrl = cleanString(response.debug?.url);
const debugParts = [
debugUrl ? `url: ${debugUrl}` : "",
debugFrameUrl ? `frame: ${debugFrameUrl}` : "",
debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
debugFrameUrl ? `frame: ${redactUrlForError(debugFrameUrl)}` : "",
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
].filter(Boolean);
const debugSuffix =
@@ -339,7 +345,9 @@
`${
payloadMessage ||
response.error ||
`Request failed (${response.status}) for ${requestUrl}`
`Request failed (${response.status}) for ${redactUrlForError(
requestUrl
)}`
}${debugSuffix}`
);
}
@@ -493,7 +501,9 @@
});
if (!response || typeof response !== "object") {
throw new Error(`No response received for ${requestUrl}`);
throw new Error(
`No response received for ${redactUrlForError(requestUrl)}`
);
}
if (!response.ok) {
@@ -511,7 +521,7 @@
: "";
const debugUrl = cleanString(response.debug?.url);
const debugParts = [
debugUrl ? `url: ${debugUrl}` : "",
debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
].filter(Boolean);
const debugSuffix =
@@ -521,7 +531,9 @@
`${
payloadMessage ||
response.error ||
`Request failed (${response.status}) for ${requestUrl}`
`Request failed (${response.status}) for ${redactUrlForError(
requestUrl
)}`
}${debugSuffix}`
);
}
@@ -631,6 +643,42 @@
}
}
function redactUrlForError(value) {
const normalized = cleanString(value);
if (!normalized) {
return "";
}
try {
const parsedUrl = new URL(normalized, window.location.origin);
const sensitiveQueryPattern =
/(token|authorization|auth|jwt|id_token|access_token)/i;
for (const key of Array.from(parsedUrl.searchParams.keys())) {
if (sensitiveQueryPattern.test(key)) {
parsedUrl.searchParams.set(key, "[redacted]");
}
}
const redactedUrl = parsedUrl.toString();
if (
parsedUrl.origin === window.location.origin &&
!/^[a-z][a-z0-9+.-]*:/i.test(normalized)
) {
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
}
return redactedUrl;
} catch {
return normalized.replace(
/([?&][^=\s|]*(?:token|authorization|auth|jwt|id_token|access_token)[^=\s|]*=)[^&\s|]+/gi,
"$1[redacted]"
);
}
}
function handleBridgeMessage(event) {
const message = event.data;

View File

@@ -7,7 +7,10 @@
const TARGET_APP_URL =
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
const HCM_ORIGIN = "https://eeho.fa.us2.oraclecloud.com";
const HCM_WELCOME_PATHNAME = "/hcmUI/faces/FuseWelcome";
const HCM_WELCOME_PATHNAMES = Object.freeze([
"/hcmUI/faces/FuseWelcome",
"/fscmUI/faces/FuseWelcome",
]);
const HCM_MY_INFORMATION_APPS_GROUP_ID = "yourapps_groupNode_my_information";
const WORKLIST_ORIGIN = "https://worklist.oracle.com";
const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
@@ -167,7 +170,7 @@
TARGET_PARAM_VALUE,
TARGET_APP_URL,
HCM_ORIGIN,
HCM_WELCOME_PATHNAME,
HCM_WELCOME_PATHNAMES,
HCM_MY_INFORMATION_APPS_GROUP_ID,
WORKLIST_ORIGIN,
WORKLIST_SAASUI_PATHNAME,

View File

@@ -402,6 +402,54 @@
return new Date(value);
}
function decodeJwtPayload(token) {
const normalizedToken = cleanString(token);
if (
!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(
normalizedToken
)
) {
return null;
}
try {
const payloadPart = normalizedToken.split(".")[1];
const base64 = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
const paddedBase64 = base64.padEnd(Math.ceil(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;
}
}
function getJwtExpiresAt(token) {
const payload = decodeJwtPayload(token);
const expiresAtSeconds = Number(payload?.exp);
return Number.isFinite(expiresAtSeconds) && expiresAtSeconds > 0
? expiresAtSeconds * 1000
: 0;
}
function isJwtExpired(token, skewSeconds = 60) {
const expiresAt = getJwtExpiresAt(token);
if (!expiresAt) {
return false;
}
const skewMilliseconds = Math.max(Number(skewSeconds) || 0, 0) * 1000;
return expiresAt <= Date.now() + skewMilliseconds;
}
function getErrorMessage(error) {
if (error instanceof Error) {
return error.message;
@@ -456,6 +504,9 @@
getOperationalWeekInfo,
getCalendarRowWeekDate,
parseDatePreservingDateOnly,
decodeJwtPayload,
getJwtExpiresAt,
isJwtExpired,
getErrorMessage,
escapeHtml,
cssEscape,