The sessions required for Internal SR API calls are now loaded via an iframe

This commit is contained in:
2026-09-29 17:48:03 -03:00
parent 4d3f1f9760
commit 3fd89a307c
13 changed files with 366 additions and 35 deletions

View File

@@ -752,10 +752,14 @@ function createFusionSrDetails({ fetchJson }) {
const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId"; const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId";
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests" const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests"
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`; + `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
const query = async () => {
const result = await fetchJson(url, { method: "GET", credentials: "include", const result = await fetchJson(url, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" } headers: { Accept: "application/json", Authorization: "Session" }
}, "requestInternalServiceRequest", debugRequests); }, "requestInternalServiceRequest", debugRequests);
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response."); if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
return result;
};
const result = await query();
const items = result.items.flatMap(item => { const items = result.items.flatMap(item => {
const children = item.ServiceRequest_SRToInternalSR_Tgt; const children = item.ServiceRequest_SRToInternalSR_Tgt;
if (children == null) return []; if (children == null) return [];
@@ -1966,7 +1970,7 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) { if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup; const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
request(message).then(sendResponse).catch(error => sendResponse({ request(message, sender).then(sendResponse).catch(error => sendResponse({
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.", ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : [] debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
})); }));
@@ -2173,9 +2177,11 @@ function createBackgroundApplication(overrides = {}) {
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args); const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args); const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome; const runtimeApi = environment.browser || environment.chrome;
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) }); const fusionSrDetails = createFusionSrDetails({
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
});
function start() { function start() {
startBackgroundApplication({ messageTypes, requestInternalServiceRequest: message => fusionSrDetails.requestInternalServiceRequest(message), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi }); startBackgroundApplication({ messageTypes, requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
} }
return { return {
start, contracts, start, contracts,

View File

@@ -2620,6 +2620,62 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
return { bind, close }; return { bind, close };
} }
// Source: src/features/opportunities/internal-sr-session.js
function createInternalSrSession({ query, environment }) {
const { document, setTimeout } = environment;
let recovery = null;
const wait = duration => new Promise(resolve => setTimeout(resolve, duration));
const now = () => environment.performance?.now() ?? Date.now();
async function warmSession(id) {
const frame = document.createElement("iframe");
frame.src = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR0001421040&srId=302963804324543&view=showisr";
frame.title = "Internal SR session initialization";
frame.tabIndex = -1;
frame.setAttribute("aria-hidden", "true");
frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
frame.style.cssText = "position:fixed;width:1px;height:1px;left:-10000px;top:0;border:0;opacity:0;pointer-events:none;";
let blocked = false;
let loaded = false;
frame.addEventListener("error", () => { blocked = true; });
frame.addEventListener("load", () => { loaded = true; });
try {
if (!document.body) throw new Error("The current page cannot initialize the Internal SR session.");
document.body.append(frame);
const deadline = now() + 60000;
for (let attempt = 0; now() < deadline; attempt += 1) {
// Check immediately, then briefly poll faster before backing off.
if (attempt > 0) await wait(Math.min(attempt <= 4 ? 250 : 1000, deadline - now()));
if (now() >= deadline) break;
if (blocked) throw new Error("The Internal SR session iframe could not be loaded. Sign in manually and try again.");
const result = await query(id);
if (result?.ok) return { id, result };
if (result?.httpStatus !== 401) throw new Error(result?.error || "Unable to load Internal SR details.");
}
throw new Error(`Internal SR session timed out after 60 seconds: the API still returned HTTP 401. ${loaded ? "The iframe emitted a load event, but authentication was not confirmed." : "The iframe did not emit a load event."} Sign in manually and try again.`);
} finally {
// Stop remaining page resources without stopping the caller's page.
try { frame.contentWindow?.stop(); } catch { /* Redirects can make the frame cross-origin. */ }
frame.src = "about:blank";
frame.remove();
}
}
async function request(id) {
let result = await query(id);
if (!result?.ok && result?.httpStatus === 401) {
if (!recovery) {
recovery = warmSession(id);
const pending = recovery;
pending.finally(() => { if (recovery === pending) recovery = null; }).catch(() => {});
}
const recovered = await recovery;
result = recovered.id === id ? recovered.result : await query(id);
}
if (!result?.ok) throw new Error(result?.error || "Unable to load Internal SR details.");
return result.items;
}
return { request };
}
// Source: src/features/opportunities/workload-actions.js // Source: src/features/opportunities/workload-actions.js
function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage, function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage,
appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, reportProgress, activityCache, createActionButton, closeActionForm, setBadgeContent, environment }) { appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, reportProgress, activityCache, createActionButton, closeActionForm, setBadgeContent, environment }) {
@@ -8061,13 +8117,16 @@ function createContentApplication(overrides = {}) {
readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args), readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args),
maskToken: (...args) => debugContentLog.maskToken(...args), environment maskToken: (...args) => debugContentLog.maskToken(...args), environment
}); });
const crmSrPopover = createCrmSrPopover({ const internalSrSession = createInternalSrSession({
requestDetails: async srId => { environment,
query: async srId => {
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId }); const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId });
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
if (!result?.ok) throw new Error(result?.error || "Unable to load SR details."); return result;
return result.items; }
}, });
const crmSrPopover = createCrmSrPopover({
requestDetails: srId => internalSrSession.request(srId),
MODAL_ID: contracts.MODAL_ID, MODAL_ID: contracts.MODAL_ID,
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment

View File

@@ -752,10 +752,14 @@ function createFusionSrDetails({ fetchJson }) {
const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId"; const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId";
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests" const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests"
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`; + `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
const query = async () => {
const result = await fetchJson(url, { method: "GET", credentials: "include", const result = await fetchJson(url, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" } headers: { Accept: "application/json", Authorization: "Session" }
}, "requestInternalServiceRequest", debugRequests); }, "requestInternalServiceRequest", debugRequests);
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response."); if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
return result;
};
const result = await query();
const items = result.items.flatMap(item => { const items = result.items.flatMap(item => {
const children = item.ServiceRequest_SRToInternalSR_Tgt; const children = item.ServiceRequest_SRToInternalSR_Tgt;
if (children == null) return []; if (children == null) return [];
@@ -1966,7 +1970,7 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) { if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup; const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
request(message).then(sendResponse).catch(error => sendResponse({ request(message, sender).then(sendResponse).catch(error => sendResponse({
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.", ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : [] debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
})); }));
@@ -2173,9 +2177,11 @@ function createBackgroundApplication(overrides = {}) {
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args); const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args); const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome; const runtimeApi = environment.browser || environment.chrome;
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) }); const fusionSrDetails = createFusionSrDetails({
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
});
function start() { function start() {
startBackgroundApplication({ messageTypes, requestInternalServiceRequest: message => fusionSrDetails.requestInternalServiceRequest(message), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi }); startBackgroundApplication({ messageTypes, requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
} }
return { return {
start, contracts, start, contracts,

View File

@@ -2620,6 +2620,62 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
return { bind, close }; return { bind, close };
} }
// Source: src/features/opportunities/internal-sr-session.js
function createInternalSrSession({ query, environment }) {
const { document, setTimeout } = environment;
let recovery = null;
const wait = duration => new Promise(resolve => setTimeout(resolve, duration));
const now = () => environment.performance?.now() ?? Date.now();
async function warmSession(id) {
const frame = document.createElement("iframe");
frame.src = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR0001421040&srId=302963804324543&view=showisr";
frame.title = "Internal SR session initialization";
frame.tabIndex = -1;
frame.setAttribute("aria-hidden", "true");
frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
frame.style.cssText = "position:fixed;width:1px;height:1px;left:-10000px;top:0;border:0;opacity:0;pointer-events:none;";
let blocked = false;
let loaded = false;
frame.addEventListener("error", () => { blocked = true; });
frame.addEventListener("load", () => { loaded = true; });
try {
if (!document.body) throw new Error("The current page cannot initialize the Internal SR session.");
document.body.append(frame);
const deadline = now() + 60000;
for (let attempt = 0; now() < deadline; attempt += 1) {
// Check immediately, then briefly poll faster before backing off.
if (attempt > 0) await wait(Math.min(attempt <= 4 ? 250 : 1000, deadline - now()));
if (now() >= deadline) break;
if (blocked) throw new Error("The Internal SR session iframe could not be loaded. Sign in manually and try again.");
const result = await query(id);
if (result?.ok) return { id, result };
if (result?.httpStatus !== 401) throw new Error(result?.error || "Unable to load Internal SR details.");
}
throw new Error(`Internal SR session timed out after 60 seconds: the API still returned HTTP 401. ${loaded ? "The iframe emitted a load event, but authentication was not confirmed." : "The iframe did not emit a load event."} Sign in manually and try again.`);
} finally {
// Stop remaining page resources without stopping the caller's page.
try { frame.contentWindow?.stop(); } catch { /* Redirects can make the frame cross-origin. */ }
frame.src = "about:blank";
frame.remove();
}
}
async function request(id) {
let result = await query(id);
if (!result?.ok && result?.httpStatus === 401) {
if (!recovery) {
recovery = warmSession(id);
const pending = recovery;
pending.finally(() => { if (recovery === pending) recovery = null; }).catch(() => {});
}
const recovered = await recovery;
result = recovered.id === id ? recovered.result : await query(id);
}
if (!result?.ok) throw new Error(result?.error || "Unable to load Internal SR details.");
return result.items;
}
return { request };
}
// Source: src/features/opportunities/workload-actions.js // Source: src/features/opportunities/workload-actions.js
function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage, function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage,
appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, reportProgress, activityCache, createActionButton, closeActionForm, setBadgeContent, environment }) { appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, reportProgress, activityCache, createActionButton, closeActionForm, setBadgeContent, environment }) {
@@ -8061,13 +8117,16 @@ function createContentApplication(overrides = {}) {
readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args), readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args),
maskToken: (...args) => debugContentLog.maskToken(...args), environment maskToken: (...args) => debugContentLog.maskToken(...args), environment
}); });
const crmSrPopover = createCrmSrPopover({ const internalSrSession = createInternalSrSession({
requestDetails: async srId => { environment,
query: async srId => {
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId }); const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId });
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
if (!result?.ok) throw new Error(result?.error || "Unable to load SR details."); return result;
return result.items; }
}, });
const crmSrPopover = createCrmSrPopover({
requestDetails: srId => internalSrSession.request(srId),
MODAL_ID: contracts.MODAL_ID, MODAL_ID: contracts.MODAL_ID,
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment

View File

@@ -39,6 +39,7 @@ module.exports = {
"src/features/opportunities/activity-progress.js", "src/features/opportunities/activity-progress.js",
"src/features/opportunities/activity-filters.js", "src/features/opportunities/activity-filters.js",
"src/features/opportunities/crm-sr-popover.js", "src/features/opportunities/crm-sr-popover.js",
"src/features/opportunities/internal-sr-session.js",
"src/features/opportunities/workload-actions.js", "src/features/opportunities/workload-actions.js",
"src/features/opportunities/workload-action-form.js", "src/features/opportunities/workload-action-form.js",
"src/features/opportunities/workload-action-options.js", "src/features/opportunities/workload-action-options.js",

View File

@@ -125,9 +125,11 @@ function createBackgroundApplication(overrides = {}) {
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args); const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args); const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome; const runtimeApi = environment.browser || environment.chrome;
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) }); const fusionSrDetails = createFusionSrDetails({
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
});
function start() { function start() {
startBackgroundApplication({ messageTypes, requestInternalServiceRequest: message => fusionSrDetails.requestInternalServiceRequest(message), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi }); startBackgroundApplication({ messageTypes, requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
} }
return { return {
start, contracts, start, contracts,

View File

@@ -108,7 +108,7 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) { if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup; const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
request(message).then(sendResponse).catch(error => sendResponse({ request(message, sender).then(sendResponse).catch(error => sendResponse({
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.", ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : [] debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
})); }));

View File

@@ -216,13 +216,16 @@ function createContentApplication(overrides = {}) {
readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args), readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args),
maskToken: (...args) => debugContentLog.maskToken(...args), environment maskToken: (...args) => debugContentLog.maskToken(...args), environment
}); });
const crmSrPopover = createCrmSrPopover({ const internalSrSession = createInternalSrSession({
requestDetails: async srId => { environment,
query: async srId => {
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId }); const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId });
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
if (!result?.ok) throw new Error(result?.error || "Unable to load SR details."); return result;
return result.items; }
}, });
const crmSrPopover = createCrmSrPopover({
requestDetails: srId => internalSrSession.request(srId),
MODAL_ID: contracts.MODAL_ID, MODAL_ID: contracts.MODAL_ID,
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment

View File

@@ -0,0 +1,54 @@
function createInternalSrSession({ query, environment }) {
const { document, setTimeout } = environment;
let recovery = null;
const wait = duration => new Promise(resolve => setTimeout(resolve, duration));
const now = () => environment.performance?.now() ?? Date.now();
async function warmSession(id) {
const frame = document.createElement("iframe");
frame.src = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR0001421040&srId=302963804324543&view=showisr";
frame.title = "Internal SR session initialization";
frame.tabIndex = -1;
frame.setAttribute("aria-hidden", "true");
frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
frame.style.cssText = "position:fixed;width:1px;height:1px;left:-10000px;top:0;border:0;opacity:0;pointer-events:none;";
let blocked = false;
let loaded = false;
frame.addEventListener("error", () => { blocked = true; });
frame.addEventListener("load", () => { loaded = true; });
try {
if (!document.body) throw new Error("The current page cannot initialize the Internal SR session.");
document.body.append(frame);
const deadline = now() + 60000;
for (let attempt = 0; now() < deadline; attempt += 1) {
// Check immediately, then briefly poll faster before backing off.
if (attempt > 0) await wait(Math.min(attempt <= 4 ? 250 : 1000, deadline - now()));
if (now() >= deadline) break;
if (blocked) throw new Error("The Internal SR session iframe could not be loaded. Sign in manually and try again.");
const result = await query(id);
if (result?.ok) return { id, result };
if (result?.httpStatus !== 401) throw new Error(result?.error || "Unable to load Internal SR details.");
}
throw new Error(`Internal SR session timed out after 60 seconds: the API still returned HTTP 401. ${loaded ? "The iframe emitted a load event, but authentication was not confirmed." : "The iframe did not emit a load event."} Sign in manually and try again.`);
} finally {
// Stop remaining page resources without stopping the caller's page.
try { frame.contentWindow?.stop(); } catch { /* Redirects can make the frame cross-origin. */ }
frame.src = "about:blank";
frame.remove();
}
}
async function request(id) {
let result = await query(id);
if (!result?.ok && result?.httpStatus === 401) {
if (!recovery) {
recovery = warmSession(id);
const pending = recovery;
pending.finally(() => { if (recovery === pending) recovery = null; }).catch(() => {});
}
const recovered = await recovery;
result = recovered.id === id ? recovered.result : await query(id);
}
if (!result?.ok) throw new Error(result?.error || "Unable to load Internal SR details.");
return result.items;
}
return { request };
}

View File

@@ -7,10 +7,14 @@ function createFusionSrDetails({ fetchJson }) {
const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId"; const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId";
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests" const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests"
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`; + `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
const query = async () => {
const result = await fetchJson(url, { method: "GET", credentials: "include", const result = await fetchJson(url, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" } headers: { Accept: "application/json", Authorization: "Session" }
}, "requestInternalServiceRequest", debugRequests); }, "requestInternalServiceRequest", debugRequests);
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response."); if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
return result;
};
const result = await query();
const items = result.items.flatMap(item => { const items = result.items.flatMap(item => {
const children = item.ServiceRequest_SRToInternalSR_Tgt; const children = item.ServiceRequest_SRToInternalSR_Tgt;
if (children == null) return []; if (children == null) return [];

View File

@@ -2,6 +2,44 @@ const assert = require("node:assert/strict");
const { test } = require("node:test"); const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs"); const { loadBackground, response } = require("./support/characterization.cjs");
const type = "opportunitiesExtension.requestInternalServiceRequest"; const type = "opportunitiesExtension.requestInternalServiceRequest";
for (const family of ["chrome", "firefox"]) {
test(`${family}: 401 is returned to the caller without opening tabs`, async () => {
let count = 0;
const h = loadBackground({ family, fetch: async () => ++count < 3 ? response({}, 401) : response({ items: [] }) });
const result = await h.dispatch({ type, srId: "123" });
assert.equal(result.ok, false);
assert.equal(result.httpStatus, 401);
assert.equal(count, 1);
assert.equal(h.tabs.length, 0);
assert.deepEqual(h.removedTabs, []);
assert.deepEqual(result.debugRequests.map(entry => entry.httpStatus), [401]);
});
}
test("concurrent 401 responses do not cause background tab recovery", async () => {
let calls = 0;
const h = loadBackground({ fetch: async url => {
if (++calls <= 2) return response({}, 401);
const id = new URL(url).searchParams.get("q");
return response({ items: [{ ServiceRequest_SRToInternalSR_Tgt: [{ Title: id }] }] });
} });
const results = await Promise.all([h.dispatch({ type, srId: "1" }), h.dispatch({ type, srId: "2" })]);
assert.equal(h.tabs.length, 0);
assert.deepEqual(h.removedTabs, []);
assert.ok(results.every(result => result.httpStatus === 401));
});
test("background never creates recovery tabs after an error", async () => {
for (const status of [401, 500]) {
let count = 0;
const h = loadBackground({ fetch: async () => response({}, ++count === 1 ? 401 : status) });
const result = await h.dispatch({ type, srId: "1" });
assert.equal(result.ok, false);
assert.equal(count, 1);
assert.deepEqual(h.removedTabs, []);
}
const h = loadBackground({ fetch: async () => response({}, 403) });
assert.equal((await h.dispatch({ type, srId: "1" })).ok, false);
assert.equal(h.tabs.length, 0);
});
for (const family of ["chrome", "firefox"]) { for (const family of ["chrome", "firefox"]) {
test(`${family}: SR detail GET uses exact scope and Session authentication`, async () => { test(`${family}: SR detail GET uses exact scope and Session authentication`, async () => {
const calls = []; const calls = [];

View File

@@ -3,7 +3,7 @@ const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs"); const { loadApplication } = require("./support/application.cjs");
const flush = () => new Promise(resolve => setImmediate(resolve)); const flush = () => new Promise(resolve => setImmediate(resolve));
test("internal SR uses Session without TokenRelay or retries on 401", async () => { test("internal SR uses Session without TokenRelay and reports unavailable iframe host", async () => {
const h = setup(); const h = setup();
let renewals = 0; let renewals = 0;
const sent = []; const sent = [];
@@ -24,7 +24,7 @@ test("internal SR uses Session without TokenRelay or retries on 401", async () =
assert.equal(sent.length, 1); assert.equal(sent.length, 1);
assert.equal(sent[0].token, undefined); assert.equal(sent[0].token, undefined);
assert.ok(sent.every(item => item.type === "opportunitiesExtension.requestInternalServiceRequest")); assert.ok(sent.every(item => item.type === "opportunitiesExtension.requestInternalServiceRequest"));
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Unauthorized")); assert.ok(h.walk(h.panel()).some(node => node.attributes.role === "alert"));
}); });
function setup() { function setup() {

View File

@@ -0,0 +1,99 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const fs = require("node:fs");
const vm = require("node:vm");
const path = require("node:path");
const factory = vm.runInNewContext(`${fs.readFileSync(path.join(__dirname, "../src/features/opportunities/internal-sr-session.js"), "utf8")}\ncreateInternalSrSession`);
function setup(query) {
let elapsed = 0;
const frames = [], removed = [], delays = [], stopped = [];
const body = { append(frame) { frame.initialSrc = frame.src; frames.push(frame); } };
const document = { body, createElement(tag) {
assert.equal(tag, "iframe");
return { contentWindow: { stop() { stopped.push(true); } }, style: {}, attributes: {}, listeners: {}, setAttribute(k, v) { this.attributes[k] = v; },
addEventListener(k, fn) { this.listeners[k] = fn; }, remove() { removed.push(this); } };
} };
const api = factory({ query, environment: { document, performance: { now: () => elapsed }, setTimeout: (fn, ms) => { delays.push(ms); elapsed += ms; return setImmediate(fn); } } });
return { api, frames, removed, delays, stopped };
}
test("401 initializes a hidden iframe in the calling document and removes it on success", async () => {
let calls = 0;
const h = setup(async () => ++calls < 3 ? { ok: false, httpStatus: 401 } : { ok: true, items: [{ SrId: 1 }] });
const items = await h.api.request("1");
assert.equal(items[0].SrId, 1);
assert.equal(h.frames.length, 1);
const frame = h.frames[0];
assert.equal(frame.tabIndex, -1);
assert.equal(frame.attributes["aria-hidden"], "true");
assert.match(frame.style.cssText, /pointer-events:none/);
assert.ok(!frame.attributes.sandbox.includes("allow-top-navigation"));
assert.ok(frame.initialSrc.endsWith("&view=showisr"));
assert.equal(frame.src, "about:blank");
assert.equal(h.stopped.length, 1);
assert.deepEqual(h.delays, [250]);
assert.equal(h.removed[0], frame);
});
test("an immediately available session stops the iframe without waiting for load or a timer", async () => {
let calls = 0;
const h = setup(async () => ++calls === 1 ? { ok: false, httpStatus: 401 } : { ok: true, items: [] });
await h.api.request("1");
assert.equal(calls, 2);
assert.equal(h.delays.length, 0);
assert.equal(h.stopped.length, 1);
assert.equal(h.removed.length, 1);
});
test("cross-origin redirects still allow iframe removal when stop is blocked", async () => {
let calls = 0;
const h = setup(async () => {
if (++calls === 1) return { ok: false, httpStatus: 401 };
h.frames[0].contentWindow.stop = () => { throw new Error("Cross-origin"); };
return { ok: true, items: [] };
});
await h.api.request("1");
assert.equal(h.frames[0].src, "about:blank");
assert.equal(h.removed.length, 1);
});
test("concurrent queries share one iframe without mixing SR results", async () => {
let calls = 0;
const h = setup(async id => ++calls <= 2 ? { ok: false, httpStatus: 401 } : { ok: true, items: [id] });
const results = await Promise.all([h.api.request("1"), h.api.request("2")]);
assert.deepEqual(results, [["1"], ["2"]]);
assert.equal(h.frames.length, 1);
assert.equal(h.removed.length, 1);
});
test("normal success does not mount an iframe; errors and timeout always clean it up", async () => {
const success = setup(async () => ({ ok: true, items: [] }));
await success.api.request("1");
assert.equal(success.frames.length, 0);
for (const status of [401, 500]) {
let calls = 0;
const h = setup(async () => ({ ok: false, httpStatus: ++calls === 1 ? 401 : status }));
await assert.rejects(h.api.request("1"));
if (status === 401) {
assert.ok(calls > 31);
assert.equal(h.delays.reduce((sum, ms) => sum + ms, 0), 60000);
} else assert.equal(calls, 2);
assert.equal(h.removed.length, 1);
}
});
test("slow session initialization can succeed after the old thirty-attempt limit", async () => {
let calls = 0;
const h = setup(async () => ++calls < 45 ? { ok: false, httpStatus: 401 } : { ok: true, items: ["ready"] });
assert.deepEqual(await h.api.request("1"), ["ready"]);
assert.equal(calls, 45);
assert.equal(h.removed.length, 1);
assert.ok(h.delays.reduce((sum, ms) => sum + ms, 0) < 60000);
});
test("iframe load alone never confirms authentication", async () => {
let calls = 0;
const h = setup(async () => {
if (++calls > 1) h.frames[0].listeners.load();
return { ok: false, httpStatus: 401 };
});
await assert.rejects(h.api.request("1"), /HTTP 401.*load event, but authentication was not confirmed/);
assert.equal(h.removed.length, 1);
});