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: { condition: {
initiatorDomains: ["eeho.fa.us2.oraclecloud.com"],
requestDomains: ["spa.oracle.com"], requestDomains: ["spa.oracle.com"],
resourceTypes: ["sub_frame"], resourceTypes: ["sub_frame"],
urlFilter: "||spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html", urlFilter: "||spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html",

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Arch Panel", "name": "Arch Panel",
"version": "1.0.7", "version": "1.0.10",
"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": [
"declarativeNetRequestWithHostAccess", "declarativeNetRequestWithHostAccess",
@@ -10,6 +10,7 @@
], ],
"host_permissions": [ "host_permissions": [
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*", "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*",
"https://*.oraclecloud.com/*",
"https://eeho.fa.us2.oraclecloud.com/*", "https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*", "https://spa.oracle.com/*",
"https://worklist.oracle.com/*" "https://worklist.oracle.com/*"
@@ -31,8 +32,8 @@
}, },
{ {
"matches": [ "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": [ "css": [
"src/content/styles.css" "src/content/styles.css"
@@ -74,6 +75,7 @@
"page-bridge.js" "page-bridge.js"
], ],
"matches": [ "matches": [
"https://*.oraclecloud.com/*",
"https://eeho.fa.us2.oraclecloud.com/*", "https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*" "https://spa.oracle.com/*"
] ]

View File

@@ -81,7 +81,7 @@
} }
for (const origin of candidates) { for (const origin of candidates) {
if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) { if (isAllowedTopMessageOrigin(origin)) {
return origin; return origin;
} }
} }
@@ -89,6 +89,25 @@
return ""; 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) { function safePostToTop(payload) {
const targetOrigin = getAllowedTopMessageOrigin(); const targetOrigin = getAllowedTopMessageOrigin();

View File

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

View File

@@ -44,7 +44,8 @@
appState.errorMessage = ""; appState.errorMessage = "";
updateLoadingProgress("Requesting current user", 4); updateLoadingProgress("Requesting current user", 4);
const user = await fetchCurrentUser(); let user = await fetchCurrentUser();
user = await preserveCachedUserAvatar(user);
updateLoadingProgress("Loading current resource user", 8); 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) { function updateLoadingProgress(message, progress) {
appState.loadingMessage = message; appState.loadingMessage = message;
appState.loadingProgress = Math.max( appState.loadingProgress = Math.max(
@@ -312,21 +355,49 @@
const worklistToken = currentFrameToken || (await readCachedWorklistToken()); const worklistToken = currentFrameToken || (await readCachedWorklistToken());
if (!worklistToken) { appState.sessionAuthHeaders = {};
throw new Error(
"Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data." if (worklistToken) {
); try {
const user = await workbenchRepository.fetchWorklistCurrentUser(
worklistToken
);
appState.sessionAuthHeaders = {};
return user;
} catch (error) {
appState.sessionAuthHeaders = {};
if (!isWorklistAuthError(error)) {
throw error;
}
}
} }
appState.sessionAuthHeaders = {}; try {
const user = await workbenchRepository.fetchSpaCurrentUser();
const user = await workbenchRepository.fetchWorklistCurrentUser( appState.sessionAuthHeaders = {};
worklistToken
);
appState.sessionAuthHeaders = {}; return user;
} catch (error) {
appState.sessionAuthHeaders = {};
return user; 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) { function findWorkloadById(workloadId) {

View File

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

View File

@@ -8,6 +8,7 @@
cleanString, cleanString,
firstNonEmptyString, firstNonEmptyString,
getFieldValue, getFieldValue,
decodeJwtPayload,
toNumber, toNumber,
normalizeWorkload, normalizeWorkload,
isWonWorkload, isWonWorkload,
@@ -31,28 +32,45 @@
}).toString(); }).toString();
const payload = await fetchJson(url.toString(), { forceExtension: true }); 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( const firstName = cleanString(
getFieldValue(userPayload, ["FirstName", "firstName", "User.FirstName"])
);
const lastName = cleanString(
getFieldValue(userPayload, ["LastName", "lastName", "User.LastName"])
);
const userEmail = cleanString(
getFieldValue(userPayload, [ getFieldValue(userPayload, [
"Email", "FirstName",
"email", "firstName",
"EmailAddress", "GivenName",
"emailAddress", "givenName",
"User.Email", "User.FirstName",
"User.email",
"User.EmailAddress",
]) ])
); );
const lastName = cleanString(
getFieldValue(userPayload, [
"LastName",
"lastName",
"FamilyName",
"familyName",
"User.LastName",
])
);
const userEmail = resolveUserEmail(payload, userPayload);
const displayName = firstNonEmptyString([ const displayName = firstNonEmptyString([
`${firstName} ${lastName}`.trim(), `${firstName} ${lastName}`.trim(),
userPayload?.DisplayName, userPayload?.DisplayName,
userPayload?.displayName, userPayload?.displayName,
userPayload?.FullName,
userPayload?.fullName,
userPayload?.Name,
userPayload?.name,
userEmail, userEmail,
]); ]);
@@ -62,7 +80,7 @@
? Object.keys(userPayload).join(", ") ? Object.keys(userPayload).join(", ")
: ""; : "";
throw new Error( 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 || {} payload || {}
).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}` ).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
); );
@@ -71,9 +89,7 @@
return { return {
userEmail, userEmail,
name: displayName || userEmail, name: displayName || userEmail,
avatar: cleanString( avatar: resolveUserAvatar(payload, userPayload),
getFieldValue(userPayload, ["ImageURL", "imageUrl", "User.ImageURL"])
),
administrator: Boolean( administrator: Boolean(
getFieldValue(userPayload, [ getFieldValue(userPayload, [
"Administrator", "Administrator",
@@ -84,20 +100,178 @@
}; };
} }
function normalizeWorklistUserPayload(payload) { function normalizeCurrentUserCandidate(payload) {
const candidate = payload?.User ?? payload?.user ?? 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)) { if (Array.isArray(candidate)) {
return candidate[0] || {}; return candidate[0] || {};
} }
if (candidate?.User) { const nestedUser =
return normalizeWorklistUserPayload(candidate); getObjectCandidate(candidate?.User) ?? getObjectCandidate(candidate?.user);
if (nestedUser) {
return normalizeCurrentUserCandidate(nestedUser);
} }
return candidate || {}; 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) { async function fetchAllCustomers(userEmail) {
const limit = 49; const limit = 49;
let offset = 0; let offset = 0;
@@ -277,6 +451,7 @@
return { return {
fetchWorklistCurrentUser, fetchWorklistCurrentUser,
fetchSpaCurrentUser,
fetchAllCustomers, fetchAllCustomers,
fetchCustomerWorkloads, fetchCustomerWorkloads,
fetchWorkloadActions, fetchWorkloadActions,

View File

@@ -172,7 +172,9 @@
const response = await new Promise((resolve, reject) => { const response = await new Promise((resolve, reject) => {
const timeout = window.setTimeout(() => { const timeout = window.setTimeout(() => {
pendingBridgeRequests.delete(requestId); pendingBridgeRequests.delete(requestId);
reject(new Error(`Timed out while requesting ${requestUrl}`)); reject(
new Error(`Timed out while requesting ${redactUrlForError(requestUrl)}`)
);
}, 60000); }, 60000);
pendingBridgeRequests.set(requestId, { pendingBridgeRequests.set(requestId, {
@@ -229,7 +231,9 @@
`${ `${
payloadMessage || payloadMessage ||
response.error || response.error ||
`Request failed (${response.status}) for ${requestUrl}` `Request failed (${response.status}) for ${redactUrlForError(
requestUrl
)}`
}${debugSuffix}` }${debugSuffix}`
); );
} }
@@ -297,7 +301,9 @@
}); });
if (!response || typeof response !== "object") { if (!response || typeof response !== "object") {
throw new Error(`No response received for ${requestUrl}`); throw new Error(
`No response received for ${redactUrlForError(requestUrl)}`
);
} }
if ( if (
@@ -328,8 +334,8 @@
const debugFrameUrl = cleanString(response.debug?.frameUrl); const debugFrameUrl = cleanString(response.debug?.frameUrl);
const debugUrl = cleanString(response.debug?.url); const debugUrl = cleanString(response.debug?.url);
const debugParts = [ const debugParts = [
debugUrl ? `url: ${debugUrl}` : "", debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
debugFrameUrl ? `frame: ${debugFrameUrl}` : "", debugFrameUrl ? `frame: ${redactUrlForError(debugFrameUrl)}` : "",
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "", debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
].filter(Boolean); ].filter(Boolean);
const debugSuffix = const debugSuffix =
@@ -339,7 +345,9 @@
`${ `${
payloadMessage || payloadMessage ||
response.error || response.error ||
`Request failed (${response.status}) for ${requestUrl}` `Request failed (${response.status}) for ${redactUrlForError(
requestUrl
)}`
}${debugSuffix}` }${debugSuffix}`
); );
} }
@@ -493,7 +501,9 @@
}); });
if (!response || typeof response !== "object") { 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) { if (!response.ok) {
@@ -511,7 +521,7 @@
: ""; : "";
const debugUrl = cleanString(response.debug?.url); const debugUrl = cleanString(response.debug?.url);
const debugParts = [ const debugParts = [
debugUrl ? `url: ${debugUrl}` : "", debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "", debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
].filter(Boolean); ].filter(Boolean);
const debugSuffix = const debugSuffix =
@@ -521,7 +531,9 @@
`${ `${
payloadMessage || payloadMessage ||
response.error || response.error ||
`Request failed (${response.status}) for ${requestUrl}` `Request failed (${response.status}) for ${redactUrlForError(
requestUrl
)}`
}${debugSuffix}` }${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) { function handleBridgeMessage(event) {
const message = event.data; const message = event.data;

View File

@@ -7,7 +7,10 @@
const TARGET_APP_URL = const TARGET_APP_URL =
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench"; "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_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 HCM_MY_INFORMATION_APPS_GROUP_ID = "yourapps_groupNode_my_information";
const WORKLIST_ORIGIN = "https://worklist.oracle.com"; const WORKLIST_ORIGIN = "https://worklist.oracle.com";
const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html"; const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
@@ -167,7 +170,7 @@
TARGET_PARAM_VALUE, TARGET_PARAM_VALUE,
TARGET_APP_URL, TARGET_APP_URL,
HCM_ORIGIN, HCM_ORIGIN,
HCM_WELCOME_PATHNAME, HCM_WELCOME_PATHNAMES,
HCM_MY_INFORMATION_APPS_GROUP_ID, HCM_MY_INFORMATION_APPS_GROUP_ID,
WORKLIST_ORIGIN, WORKLIST_ORIGIN,
WORKLIST_SAASUI_PATHNAME, WORKLIST_SAASUI_PATHNAME,

View File

@@ -402,6 +402,54 @@
return new Date(value); 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) { function getErrorMessage(error) {
if (error instanceof Error) { if (error instanceof Error) {
return error.message; return error.message;
@@ -456,6 +504,9 @@
getOperationalWeekInfo, getOperationalWeekInfo,
getCalendarRowWeekDate, getCalendarRowWeekDate,
parseDatePreservingDateOnly, parseDatePreservingDateOnly,
decodeJwtPayload,
getJwtExpiresAt,
isJwtExpired,
getErrorMessage, getErrorMessage,
escapeHtml, escapeHtml,
cssEscape, cssEscape,