Implemented the Create Internal Service Request functionality

This commit is contained in:
2026-10-09 18:41:39 -03:00
parent c9de750261
commit 8b4d88b226
34 changed files with 1690 additions and 480 deletions

View File

@@ -20,9 +20,12 @@ function createMessageTypes() {
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest",
prepareSrSession: "opportunitiesExtension.prepareSrSession",
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform",
requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices",
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
@@ -33,45 +36,8 @@ function createMessageTypes() {
}
// Source: src/shared/debug-body.js
function createDebugBodyPreview(text, limit) {
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
try {
const normalized = text.replace(/^\uFEFF/, "").trimStart()
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
let value = JSON.parse(normalized);
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
try { value = JSON.parse(value); } catch { break; }
}
// Bound retained data without cutting JSON syntax or losing sensitive field names.
let remaining = Math.max(0, limit - 256);
const visit = (node, depth = 0) => {
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
if (node === null || typeof node !== "object") {
const serialized = JSON.stringify(node);
if (serialized.length > remaining) {
remaining = 0;
return "[omitted from debug preview]";
}
remaining -= serialized.length + 1;
return node;
}
const result = Array.isArray(node) ? [] : Object.create(null);
remaining -= 2;
for (const key of Object.keys(node)) {
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
if (remaining < cost + 64) break;
remaining -= cost;
result[key] = visit(node[key], depth + 1);
}
return result;
};
return {
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
truncated: true
};
} catch {
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
}
function createDebugBodyPreview(text) {
return { value: text || "(empty response body)", truncated: false };
}
// Source: src/background/contracts.js
@@ -435,6 +401,7 @@ function createAuthSpaCredentials({
function createSrRequestHeaders({ runtimeApi }) {
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
const appPath = "/ic/builder/rt/SeaaS_initiation/";
const internalPath = "/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
const maxAge = 60 * 60 * 1000;
let observed = null;
@@ -448,7 +415,7 @@ function createSrRequestHeaders({ runtimeApi }) {
function observeHeaders(rawUrl, headers) {
let url;
try { url = new URL(rawUrl); } catch { return; }
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/live(?:\/|;)/.test(url.pathname)) return;
// Read only the client ID, not cookies, bodies or authorization.
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
@@ -461,7 +428,7 @@ function createSrRequestHeaders({ runtimeApi }) {
function start() {
if (started || !runtimeApi?.webRequest) return;
started = true;
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
}, filter, ["requestHeaders"]);
@@ -474,6 +441,9 @@ function createSrRequestHeaders({ runtimeApi }) {
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
throw new Error("Unexpected Service Request token URL.");
}
return sessionHeaders(target.origin, "application/vnd.oracle.adf.action+json");
}
async function sessionHeaders(targetOrigin, contentType) {
if (!valid(observed)) {
try {
const stored = await runtimeApi.storage?.session?.get(storageKey);
@@ -481,8 +451,8 @@ function createSrRequestHeaders({ runtimeApi }) {
} catch { /* A missing capture must not prevent trying the other required headers. */ }
}
return {
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
Authorization: "Session", Origin: target.origin,
Accept: "application/json", "Content-Type": contentType,
Authorization: "Session", Origin: targetOrigin,
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
};
}
@@ -490,8 +460,8 @@ function createSrRequestHeaders({ runtimeApi }) {
const headers = await previewTokenHeaders(url);
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
const serviceTarget = new URL(serviceRequestUrl);
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
const isServiceRequest = serviceTarget.pathname.startsWith(appPath) && serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests");
if (serviceTarget.origin !== headers.Origin || !isServiceRequest || serviceTarget.search || serviceTarget.hash) {
throw new Error("Unexpected Service Request URL.");
}
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
@@ -501,10 +471,11 @@ function createSrRequestHeaders({ runtimeApi }) {
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const ruleIds = [7201, 7202];
await runtimeApi.declarativeNetRequest.updateSessionRules({
removeRuleIds: [7201, 7202],
removeRuleIds: ruleIds,
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
id: 7201 + index, priority: 1,
id: ruleIds[index], priority: 1,
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
condition: { regexFilter, initiatorDomains: [extensionDomain],
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
@@ -512,7 +483,29 @@ function createSrRequestHeaders({ runtimeApi }) {
});
return headers;
}
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
async function internalServiceRequestHeaders(url) {
const target = new URL(url);
if (target.origin !== origin || target.pathname !== internalPath || target.search || target.hash) {
throw new Error("Unexpected Internal Service Request URL.");
}
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
throw new Error("Reload the extension to enable the Internal Service Request header permissions.");
}
const headers = await sessionHeaders(target.origin, "application/json");
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
await runtimeApi.declarativeNetRequest.updateSessionRules({
removeRuleIds: [7203],
addRules: [{
id: 7203, priority: 1,
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [extensionDomain],
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
}]
});
return headers;
}
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
}
// Source: src/services/spa/accounts.js
@@ -1029,7 +1022,33 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
return { ok: true, items: result.items.map(({ Id, RecordName, Abstract_c }) => ({ Id: String(Id), RecordName, Abstract_c: Abstract_c == null ? "" : String(Abstract_c) })), debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
async function requestInternalSrOptions(message, services = false) {
const debugRequests = [];
const name = services ? "requestInternalSrServices" : "requestLookupTypePlatform";
try {
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/";
let path = "lookups?type=STANDARD&lookupType=SENGG_TECH_PLATFORM";
if (services) {
const id = String(message.parentServiceId ?? "");
if (!/^\d+$/.test(id)) throw new Error("A numeric parent SrId is required.");
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
path = `services?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&parentServiceId=${encodeURIComponent(id)}&serviceType=INTERNAL`;
}
const result = await fetchJson(base + path, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" }
}, name, debugRequests);
if (!Array.isArray(result?.items) || result.items.some(item => services
? !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string"
: typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${name} response.`);
const items = services
? result.items.map(item => ({ Id: String(item.Id), RecordName: item.RecordName, Description_c: String(item.Description_c ?? "") }))
: result.items.filter(item => item.enabledFlag !== "N").map(({ code, name }) => ({ code, name }));
return { ok: true, items, debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
requestLookupTypePlatform: () => requestInternalSrOptions({}),
requestInternalSrServices: message => requestInternalSrOptions(message, true),
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
@@ -1037,7 +1056,7 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
}
// Source: src/services/fusion/sr-create.js
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders }) {
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
@@ -1050,26 +1069,76 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
return value.trim();
}
function dateOnly(value, name) {
const date = text(value, name);
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error(`Invalid ${name}.`);
return date;
}
function payloadFor(input) {
const payload = {};
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
const date = payload.CustomerDeadline_c;
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
dateOnly(payload.CustomerDeadline_c, "Customer deadline");
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
}
async function requestResourceUser(email, debugRequests) {
const emailAddress = text(email, "current user email");
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestResourceUser", debugRequests);
return numericId(resource?.resourcePartyId, "resourcePartyId");
}
async function requestPostInternalServiceRequest(message) {
const debugRequests = [];
try {
const source = message.payload;
const parentSrId = numericId(source?.ServiceRequest_Id_SRToInternalSR, "parent SrId");
const payload = {
Title: text(source?.Title, "Title"),
ServiceNew_Id_c: numericId(source?.ServiceNew_Id_c, "ServiceNew_Id_c"),
ProblemDescription: text(source?.ProblemDescription, "Description"),
CustomerDeadline_c: dateOnly(source?.CustomerDeadline_c, "Customer deadline"),
TechPlatformLookUp_c: text(source?.TechPlatformLookUp_c, "Platform"),
InternalDeadline_c: `${dateOnly(source?.InternalDeadline_c, "Internal Deadline")}T00:00:00.000Z`,
SvcQuestionsCollection_c: []
};
const parent = await requestSRInfo(parentSrId, debugRequests);
payload.ServiceRequest_Id_SRToInternalSR = String(parentSrId);
for (const name of ["AccountPartyId", "OpportunityName_Id_c"]) payload[name] = numericId(parent[name], name);
for (const name of ["OpportunityName_c", "RegionText_c", "SvcHealth_c", "Language_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(parent[name], name);
payload.SecondaryPillarNew_c = parent.SecondaryPillarNew_c == null ? null : text(parent.SecondaryPillarNew_c, "SecondaryPillarNew_c");
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
const result = await fetchJson(url, { method: "POST", credentials: "include",
headers: await internalServiceRequestHeaders(url),
body: JSON.stringify(payload)
}, "requestPostInternalServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
async function requestSRInfo(parentSrId, debugRequests) {
const url = new 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");
url.searchParams.set("fields", "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
url.searchParams.set("onlyData", "true");
url.searchParams.set("orderBy", "LastUpdateDate:desc");
url.searchParams.set("q", `SrId=${parentSrId}`);
const result = await fetchJson(url.href, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestSRInfo", debugRequests);
if (!Array.isArray(result?.items) || result.items.length !== 1 || !result.items[0]
|| (result.items[0].SrId != null && String(result.items[0].SrId) !== String(parentSrId))) {
throw new Error("requestSRInfo did not return the selected parent Service Request.");
}
return result.items[0];
}
async function requestPostServiceRequest(message) {
const debugRequests = [];
try {
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
const payload = payloadFor(message.payload);
if (message.phase === "prepare") {
const emailAddress = text(message.emailAddress, "current user email");
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestResourceUser", debugRequests);
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
const sharedHeaders = await previewTokenHeaders(catalog);
return { ok: true, payload, debugRequests, headers: {
requestSrTokenCatalog: sharedHeaders,
@@ -1077,7 +1146,20 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
} };
}
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
let id = message.catalogId;
const headers = await requestCreationHeaders(message.catalogId, endpoint, debugRequests);
const result = await fetchJson(endpoint, {
method: "POST", credentials: "include", headers,
body: JSON.stringify(payload)
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
} finally {
annotateSigningRequest(debugRequests);
}
}
async function requestCreationHeaders(id, target, debugRequests) {
if (id == null) {
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
@@ -1086,7 +1168,7 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
}
id = numericId(id, "Catalogue Id");
const tokenUrl = `${catalog}/${id}`;
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
let catalogHeaders = await tokenHeaders(tokenUrl, target);
const requestToken = () => fetchJson(tokenUrl, {
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
}, "requestSrTokenCatalog", debugRequests);
@@ -1103,27 +1185,16 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
}
const result = await fetchJson(endpoint, {
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
body: JSON.stringify(payload)
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
} finally {
// Never forward the signing token to content scripts, even with Debug secrets enabled.
return { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() };
}
function annotateSigningRequest(debugRequests) {
for (const entry of debugRequests) {
if (entry.name === "requestSrTokenCatalog") {
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
entry.responseBody = JSON.stringify({ result: "(redacted)" });
entry.responseBodyTruncated = false;
}
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
}
}
}
return { requestPostServiceRequest };
return { requestPostServiceRequest, requestPostInternalServiceRequest };
}
// Source: src/services/spa/current-user.js
@@ -1144,12 +1215,6 @@ function createSpaCurrentUser({ fetchJson }) {
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
error.debugRequests = debugRequests;
throw error;
} finally {
// The identity token is not needed by content scripts or debug, including on failures.
for (const entry of debugRequests) {
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
entry.responseBodyTruncated = false;
}
}
}
return { requestCurrentUser };
@@ -1330,7 +1395,6 @@ function createSpaResponseShapes() {
// Source: src/services/http/background-json.js
function createHttpBackgroundJson({
previewDebugBody,
DEBUG_BODY_LIMIT,
observeResponseHeaders = () => {},
environment
}) {
@@ -1457,7 +1521,7 @@ function createHttpBackgroundJson({
}
function createDebugBody(text) {
return previewDebugBody(text, DEBUG_BODY_LIMIT);
return previewDebugBody(text);
}
function normalizeDebugHeaders(headers) {
@@ -2227,12 +2291,15 @@ function registerBackgroundMessages({
requestActionFindLookup,
requestMgmtAction,
requestPostServiceRequest,
requestPostInternalServiceRequest,
requestInternalServiceRequest,
requestSrLookupPrimaryPillar,
requestSrLookupWithSession,
prepareSrSession,
requestSrLookupLanguage,
requestSrLookupTechPlatform,
requestLookupTypePlatform,
requestInternalSrServices,
requestSrLookupOpportunityBusiness,
requestCurrentUser,
requestConsumerTokenService,
@@ -2246,6 +2313,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || !message.type) {
return false;
}
if (message.type === messageTypes.requestPostInternalServiceRequest) {
requestPostInternalServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));
return true;
}
if (message.type === messageTypes.requestPostServiceRequest) {
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));
@@ -2257,6 +2329,8 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
const srLookups = {
[messageTypes.requestLookupTypePlatform]: requestLookupTypePlatform,
[messageTypes.requestInternalSrServices]: requestInternalSrServices,
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
@@ -2491,7 +2565,6 @@ function createBackgroundApplication(overrides = {}) {
const httpBackgroundJson = createHttpBackgroundJson({
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
previewDebugBody: createDebugBodyPreview,
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
environment
});
const productsBackgroundJobs = createProductsBackgroundJobs({
@@ -2563,6 +2636,7 @@ function createBackgroundApplication(overrides = {}) {
const fusionSrCreate = createFusionSrCreate({
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
internalServiceRequestHeaders: url => srRequestHeaders.internalServiceRequestHeaders(url),
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
});
const fusionSrDetails = createFusionSrDetails({
@@ -2576,7 +2650,11 @@ function createBackgroundApplication(overrides = {}) {
});
function start() {
srRequestHeaders.start();
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), 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 });
startBackgroundApplication({
requestLookupTypePlatform: () => fusionSrDetails.requestLookupTypePlatform(),
requestPostInternalServiceRequest: message => fusionSrCreate.requestPostInternalServiceRequest(message),
requestInternalSrServices: message => fusionSrDetails.requestInternalSrServices(message),
requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), 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 {
start, contracts,

File diff suppressed because one or more lines are too long

View File

@@ -20,9 +20,12 @@ function createMessageTypes() {
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest",
prepareSrSession: "opportunitiesExtension.prepareSrSession",
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform",
requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices",
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
@@ -33,45 +36,8 @@ function createMessageTypes() {
}
// Source: src/shared/debug-body.js
function createDebugBodyPreview(text, limit) {
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
try {
const normalized = text.replace(/^\uFEFF/, "").trimStart()
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
let value = JSON.parse(normalized);
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
try { value = JSON.parse(value); } catch { break; }
}
// Bound retained data without cutting JSON syntax or losing sensitive field names.
let remaining = Math.max(0, limit - 256);
const visit = (node, depth = 0) => {
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
if (node === null || typeof node !== "object") {
const serialized = JSON.stringify(node);
if (serialized.length > remaining) {
remaining = 0;
return "[omitted from debug preview]";
}
remaining -= serialized.length + 1;
return node;
}
const result = Array.isArray(node) ? [] : Object.create(null);
remaining -= 2;
for (const key of Object.keys(node)) {
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
if (remaining < cost + 64) break;
remaining -= cost;
result[key] = visit(node[key], depth + 1);
}
return result;
};
return {
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
truncated: true
};
} catch {
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
}
function createDebugBodyPreview(text) {
return { value: text || "(empty response body)", truncated: false };
}
// Source: src/background/contracts.js
@@ -435,6 +401,7 @@ function createAuthSpaCredentials({
function createSrRequestHeaders({ runtimeApi }) {
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
const appPath = "/ic/builder/rt/SeaaS_initiation/";
const internalPath = "/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
const maxAge = 60 * 60 * 1000;
let observed = null;
@@ -448,7 +415,7 @@ function createSrRequestHeaders({ runtimeApi }) {
function observeHeaders(rawUrl, headers) {
let url;
try { url = new URL(rawUrl); } catch { return; }
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/live(?:\/|;)/.test(url.pathname)) return;
// Read only the client ID, not cookies, bodies or authorization.
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
@@ -461,7 +428,7 @@ function createSrRequestHeaders({ runtimeApi }) {
function start() {
if (started || !runtimeApi?.webRequest) return;
started = true;
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
}, filter, ["requestHeaders"]);
@@ -474,6 +441,9 @@ function createSrRequestHeaders({ runtimeApi }) {
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
throw new Error("Unexpected Service Request token URL.");
}
return sessionHeaders(target.origin, "application/vnd.oracle.adf.action+json");
}
async function sessionHeaders(targetOrigin, contentType) {
if (!valid(observed)) {
try {
const stored = await runtimeApi.storage?.session?.get(storageKey);
@@ -481,8 +451,8 @@ function createSrRequestHeaders({ runtimeApi }) {
} catch { /* A missing capture must not prevent trying the other required headers. */ }
}
return {
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
Authorization: "Session", Origin: target.origin,
Accept: "application/json", "Content-Type": contentType,
Authorization: "Session", Origin: targetOrigin,
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
};
}
@@ -490,8 +460,8 @@ function createSrRequestHeaders({ runtimeApi }) {
const headers = await previewTokenHeaders(url);
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
const serviceTarget = new URL(serviceRequestUrl);
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
const isServiceRequest = serviceTarget.pathname.startsWith(appPath) && serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests");
if (serviceTarget.origin !== headers.Origin || !isServiceRequest || serviceTarget.search || serviceTarget.hash) {
throw new Error("Unexpected Service Request URL.");
}
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
@@ -501,10 +471,11 @@ function createSrRequestHeaders({ runtimeApi }) {
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const ruleIds = [7201, 7202];
await runtimeApi.declarativeNetRequest.updateSessionRules({
removeRuleIds: [7201, 7202],
removeRuleIds: ruleIds,
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
id: 7201 + index, priority: 1,
id: ruleIds[index], priority: 1,
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
condition: { regexFilter, initiatorDomains: [extensionDomain],
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
@@ -512,7 +483,29 @@ function createSrRequestHeaders({ runtimeApi }) {
});
return headers;
}
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
async function internalServiceRequestHeaders(url) {
const target = new URL(url);
if (target.origin !== origin || target.pathname !== internalPath || target.search || target.hash) {
throw new Error("Unexpected Internal Service Request URL.");
}
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
throw new Error("Reload the extension to enable the Internal Service Request header permissions.");
}
const headers = await sessionHeaders(target.origin, "application/json");
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
await runtimeApi.declarativeNetRequest.updateSessionRules({
removeRuleIds: [7203],
addRules: [{
id: 7203, priority: 1,
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [extensionDomain],
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
}]
});
return headers;
}
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
}
// Source: src/services/spa/accounts.js
@@ -1029,7 +1022,33 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
return { ok: true, items: result.items.map(({ Id, RecordName, Abstract_c }) => ({ Id: String(Id), RecordName, Abstract_c: Abstract_c == null ? "" : String(Abstract_c) })), debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
async function requestInternalSrOptions(message, services = false) {
const debugRequests = [];
const name = services ? "requestInternalSrServices" : "requestLookupTypePlatform";
try {
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/";
let path = "lookups?type=STANDARD&lookupType=SENGG_TECH_PLATFORM";
if (services) {
const id = String(message.parentServiceId ?? "");
if (!/^\d+$/.test(id)) throw new Error("A numeric parent SrId is required.");
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
path = `services?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&parentServiceId=${encodeURIComponent(id)}&serviceType=INTERNAL`;
}
const result = await fetchJson(base + path, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" }
}, name, debugRequests);
if (!Array.isArray(result?.items) || result.items.some(item => services
? !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string"
: typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${name} response.`);
const items = services
? result.items.map(item => ({ Id: String(item.Id), RecordName: item.RecordName, Description_c: String(item.Description_c ?? "") }))
: result.items.filter(item => item.enabledFlag !== "N").map(({ code, name }) => ({ code, name }));
return { ok: true, items, debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
requestLookupTypePlatform: () => requestInternalSrOptions({}),
requestInternalSrServices: message => requestInternalSrOptions(message, true),
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
@@ -1037,7 +1056,7 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
}
// Source: src/services/fusion/sr-create.js
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders }) {
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
@@ -1050,26 +1069,76 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
return value.trim();
}
function dateOnly(value, name) {
const date = text(value, name);
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error(`Invalid ${name}.`);
return date;
}
function payloadFor(input) {
const payload = {};
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
const date = payload.CustomerDeadline_c;
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
dateOnly(payload.CustomerDeadline_c, "Customer deadline");
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
}
async function requestResourceUser(email, debugRequests) {
const emailAddress = text(email, "current user email");
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestResourceUser", debugRequests);
return numericId(resource?.resourcePartyId, "resourcePartyId");
}
async function requestPostInternalServiceRequest(message) {
const debugRequests = [];
try {
const source = message.payload;
const parentSrId = numericId(source?.ServiceRequest_Id_SRToInternalSR, "parent SrId");
const payload = {
Title: text(source?.Title, "Title"),
ServiceNew_Id_c: numericId(source?.ServiceNew_Id_c, "ServiceNew_Id_c"),
ProblemDescription: text(source?.ProblemDescription, "Description"),
CustomerDeadline_c: dateOnly(source?.CustomerDeadline_c, "Customer deadline"),
TechPlatformLookUp_c: text(source?.TechPlatformLookUp_c, "Platform"),
InternalDeadline_c: `${dateOnly(source?.InternalDeadline_c, "Internal Deadline")}T00:00:00.000Z`,
SvcQuestionsCollection_c: []
};
const parent = await requestSRInfo(parentSrId, debugRequests);
payload.ServiceRequest_Id_SRToInternalSR = String(parentSrId);
for (const name of ["AccountPartyId", "OpportunityName_Id_c"]) payload[name] = numericId(parent[name], name);
for (const name of ["OpportunityName_c", "RegionText_c", "SvcHealth_c", "Language_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(parent[name], name);
payload.SecondaryPillarNew_c = parent.SecondaryPillarNew_c == null ? null : text(parent.SecondaryPillarNew_c, "SecondaryPillarNew_c");
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
const result = await fetchJson(url, { method: "POST", credentials: "include",
headers: await internalServiceRequestHeaders(url),
body: JSON.stringify(payload)
}, "requestPostInternalServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
async function requestSRInfo(parentSrId, debugRequests) {
const url = new 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");
url.searchParams.set("fields", "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
url.searchParams.set("onlyData", "true");
url.searchParams.set("orderBy", "LastUpdateDate:desc");
url.searchParams.set("q", `SrId=${parentSrId}`);
const result = await fetchJson(url.href, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestSRInfo", debugRequests);
if (!Array.isArray(result?.items) || result.items.length !== 1 || !result.items[0]
|| (result.items[0].SrId != null && String(result.items[0].SrId) !== String(parentSrId))) {
throw new Error("requestSRInfo did not return the selected parent Service Request.");
}
return result.items[0];
}
async function requestPostServiceRequest(message) {
const debugRequests = [];
try {
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
const payload = payloadFor(message.payload);
if (message.phase === "prepare") {
const emailAddress = text(message.emailAddress, "current user email");
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestResourceUser", debugRequests);
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
const sharedHeaders = await previewTokenHeaders(catalog);
return { ok: true, payload, debugRequests, headers: {
requestSrTokenCatalog: sharedHeaders,
@@ -1077,7 +1146,20 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
} };
}
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
let id = message.catalogId;
const headers = await requestCreationHeaders(message.catalogId, endpoint, debugRequests);
const result = await fetchJson(endpoint, {
method: "POST", credentials: "include", headers,
body: JSON.stringify(payload)
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
} finally {
annotateSigningRequest(debugRequests);
}
}
async function requestCreationHeaders(id, target, debugRequests) {
if (id == null) {
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
@@ -1086,7 +1168,7 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
}
id = numericId(id, "Catalogue Id");
const tokenUrl = `${catalog}/${id}`;
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
let catalogHeaders = await tokenHeaders(tokenUrl, target);
const requestToken = () => fetchJson(tokenUrl, {
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
}, "requestSrTokenCatalog", debugRequests);
@@ -1103,27 +1185,16 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
}
const result = await fetchJson(endpoint, {
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
body: JSON.stringify(payload)
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
} finally {
// Never forward the signing token to content scripts, even with Debug secrets enabled.
return { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() };
}
function annotateSigningRequest(debugRequests) {
for (const entry of debugRequests) {
if (entry.name === "requestSrTokenCatalog") {
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
entry.responseBody = JSON.stringify({ result: "(redacted)" });
entry.responseBodyTruncated = false;
}
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
}
}
}
return { requestPostServiceRequest };
return { requestPostServiceRequest, requestPostInternalServiceRequest };
}
// Source: src/services/spa/current-user.js
@@ -1144,12 +1215,6 @@ function createSpaCurrentUser({ fetchJson }) {
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
error.debugRequests = debugRequests;
throw error;
} finally {
// The identity token is not needed by content scripts or debug, including on failures.
for (const entry of debugRequests) {
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
entry.responseBodyTruncated = false;
}
}
}
return { requestCurrentUser };
@@ -1330,7 +1395,6 @@ function createSpaResponseShapes() {
// Source: src/services/http/background-json.js
function createHttpBackgroundJson({
previewDebugBody,
DEBUG_BODY_LIMIT,
observeResponseHeaders = () => {},
environment
}) {
@@ -1457,7 +1521,7 @@ function createHttpBackgroundJson({
}
function createDebugBody(text) {
return previewDebugBody(text, DEBUG_BODY_LIMIT);
return previewDebugBody(text);
}
function normalizeDebugHeaders(headers) {
@@ -2227,12 +2291,15 @@ function registerBackgroundMessages({
requestActionFindLookup,
requestMgmtAction,
requestPostServiceRequest,
requestPostInternalServiceRequest,
requestInternalServiceRequest,
requestSrLookupPrimaryPillar,
requestSrLookupWithSession,
prepareSrSession,
requestSrLookupLanguage,
requestSrLookupTechPlatform,
requestLookupTypePlatform,
requestInternalSrServices,
requestSrLookupOpportunityBusiness,
requestCurrentUser,
requestConsumerTokenService,
@@ -2246,6 +2313,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || !message.type) {
return false;
}
if (message.type === messageTypes.requestPostInternalServiceRequest) {
requestPostInternalServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));
return true;
}
if (message.type === messageTypes.requestPostServiceRequest) {
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));
@@ -2257,6 +2329,8 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
const srLookups = {
[messageTypes.requestLookupTypePlatform]: requestLookupTypePlatform,
[messageTypes.requestInternalSrServices]: requestInternalSrServices,
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
@@ -2491,7 +2565,6 @@ function createBackgroundApplication(overrides = {}) {
const httpBackgroundJson = createHttpBackgroundJson({
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
previewDebugBody: createDebugBodyPreview,
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
environment
});
const productsBackgroundJobs = createProductsBackgroundJobs({
@@ -2563,6 +2636,7 @@ function createBackgroundApplication(overrides = {}) {
const fusionSrCreate = createFusionSrCreate({
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
internalServiceRequestHeaders: url => srRequestHeaders.internalServiceRequestHeaders(url),
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
});
const fusionSrDetails = createFusionSrDetails({
@@ -2576,7 +2650,11 @@ function createBackgroundApplication(overrides = {}) {
});
function start() {
srRequestHeaders.start();
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), 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 });
startBackgroundApplication({
requestLookupTypePlatform: () => fusionSrDetails.requestLookupTypePlatform(),
requestPostInternalServiceRequest: message => fusionSrCreate.requestPostInternalServiceRequest(message),
requestInternalSrServices: message => fusionSrDetails.requestInternalSrServices(message),
requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), 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 {
start, contracts,

File diff suppressed because one or more lines are too long

View File

@@ -40,6 +40,7 @@ module.exports = {
"src/features/opportunities/activity-filters.js",
"src/features/opportunities/crm-sr-popover.js",
"src/features/opportunities/crm-sr-form.js",
"src/features/opportunities/internal-sr-form.js",
"src/features/opportunities/searchable-select.js",
"src/features/opportunities/internal-sr-session.js",
"src/features/opportunities/sr-session-preparation.js",

View File

@@ -1,6 +1,7 @@
function createSrRequestHeaders({ runtimeApi }) {
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
const appPath = "/ic/builder/rt/SeaaS_initiation/";
const internalPath = "/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
const maxAge = 60 * 60 * 1000;
let observed = null;
@@ -14,7 +15,7 @@ function createSrRequestHeaders({ runtimeApi }) {
function observeHeaders(rawUrl, headers) {
let url;
try { url = new URL(rawUrl); } catch { return; }
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/live(?:\/|;)/.test(url.pathname)) return;
// Read only the client ID, not cookies, bodies or authorization.
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
@@ -27,7 +28,7 @@ function createSrRequestHeaders({ runtimeApi }) {
function start() {
if (started || !runtimeApi?.webRequest) return;
started = true;
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
}, filter, ["requestHeaders"]);
@@ -40,6 +41,9 @@ function createSrRequestHeaders({ runtimeApi }) {
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
throw new Error("Unexpected Service Request token URL.");
}
return sessionHeaders(target.origin, "application/vnd.oracle.adf.action+json");
}
async function sessionHeaders(targetOrigin, contentType) {
if (!valid(observed)) {
try {
const stored = await runtimeApi.storage?.session?.get(storageKey);
@@ -47,8 +51,8 @@ function createSrRequestHeaders({ runtimeApi }) {
} catch { /* A missing capture must not prevent trying the other required headers. */ }
}
return {
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
Authorization: "Session", Origin: target.origin,
Accept: "application/json", "Content-Type": contentType,
Authorization: "Session", Origin: targetOrigin,
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
};
}
@@ -56,8 +60,8 @@ function createSrRequestHeaders({ runtimeApi }) {
const headers = await previewTokenHeaders(url);
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
const serviceTarget = new URL(serviceRequestUrl);
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
const isServiceRequest = serviceTarget.pathname.startsWith(appPath) && serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests");
if (serviceTarget.origin !== headers.Origin || !isServiceRequest || serviceTarget.search || serviceTarget.hash) {
throw new Error("Unexpected Service Request URL.");
}
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
@@ -67,10 +71,11 @@ function createSrRequestHeaders({ runtimeApi }) {
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const ruleIds = [7201, 7202];
await runtimeApi.declarativeNetRequest.updateSessionRules({
removeRuleIds: [7201, 7202],
removeRuleIds: ruleIds,
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
id: 7201 + index, priority: 1,
id: ruleIds[index], priority: 1,
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
condition: { regexFilter, initiatorDomains: [extensionDomain],
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
@@ -78,5 +83,27 @@ function createSrRequestHeaders({ runtimeApi }) {
});
return headers;
}
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
async function internalServiceRequestHeaders(url) {
const target = new URL(url);
if (target.origin !== origin || target.pathname !== internalPath || target.search || target.hash) {
throw new Error("Unexpected Internal Service Request URL.");
}
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
throw new Error("Reload the extension to enable the Internal Service Request header permissions.");
}
const headers = await sessionHeaders(target.origin, "application/json");
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
await runtimeApi.declarativeNetRequest.updateSessionRules({
removeRuleIds: [7203],
addRules: [{
id: 7203, priority: 1,
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [extensionDomain],
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
}]
});
return headers;
}
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
}

View File

@@ -92,10 +92,7 @@ function createAuthTokenRelay({
throw new Error("Token relay response does not contain access_token.");
}
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
...responseData,
access_token: maskToken(responseData.access_token)
}, null, 2)));
logRequestSuccess(requestEntry, response, responseBody);
if (url === TOKEN_RELAY_URL) {
authSessionState.principal = typeof responseData.principal === "string" ? responseData.principal.trim() : "";
@@ -122,7 +119,7 @@ function createAuthTokenRelay({
method: "GET",
url,
headers: {
"x-xsrf-token": xsrfTokenSource.token ? maskToken(xsrfTokenSource.token) : "(missing)"
"x-xsrf-token": xsrfTokenSource.token || "(missing)"
},
metadata: {
xsrfCookieName: xsrfTokenSource.cookieName || "(missing)",

View File

@@ -58,7 +58,6 @@ function createBackgroundApplication(overrides = {}) {
const httpBackgroundJson = createHttpBackgroundJson({
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
previewDebugBody: createDebugBodyPreview,
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
environment
});
const productsBackgroundJobs = createProductsBackgroundJobs({
@@ -130,6 +129,7 @@ function createBackgroundApplication(overrides = {}) {
const fusionSrCreate = createFusionSrCreate({
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
internalServiceRequestHeaders: url => srRequestHeaders.internalServiceRequestHeaders(url),
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
});
const fusionSrDetails = createFusionSrDetails({
@@ -143,7 +143,11 @@ function createBackgroundApplication(overrides = {}) {
});
function start() {
srRequestHeaders.start();
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), 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 });
startBackgroundApplication({
requestLookupTypePlatform: () => fusionSrDetails.requestLookupTypePlatform(),
requestPostInternalServiceRequest: message => fusionSrCreate.requestPostInternalServiceRequest(message),
requestInternalSrServices: message => fusionSrDetails.requestInternalSrServices(message),
requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), 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 {
start, contracts,

View File

@@ -12,12 +12,15 @@ function registerBackgroundMessages({
requestActionFindLookup,
requestMgmtAction,
requestPostServiceRequest,
requestPostInternalServiceRequest,
requestInternalServiceRequest,
requestSrLookupPrimaryPillar,
requestSrLookupWithSession,
prepareSrSession,
requestSrLookupLanguage,
requestSrLookupTechPlatform,
requestLookupTypePlatform,
requestInternalSrServices,
requestSrLookupOpportunityBusiness,
requestCurrentUser,
requestConsumerTokenService,
@@ -31,6 +34,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || !message.type) {
return false;
}
if (message.type === messageTypes.requestPostInternalServiceRequest) {
requestPostInternalServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));
return true;
}
if (message.type === messageTypes.requestPostServiceRequest) {
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));
@@ -42,6 +50,8 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
const srLookups = {
[messageTypes.requestLookupTypePlatform]: requestLookupTypePlatform,
[messageTypes.requestInternalSrServices]: requestInternalSrServices,
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,

View File

@@ -57,7 +57,7 @@ function createContentApplication(overrides = {}) {
});
const contentModal = createContentModal({
prepareSrSession: () => srSessionPreparation.ensure(),
closeActionForm: (...args) => opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args),
closeActionForm: (...args) => internalSrForm.close(...args) || opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args),
mountOpportunityViews: (...args) => opportunityCalendar.mount(...args),
createActivityFilterControls: (...args) => activityFilters.createControls(...args),
closeCrmSrPopover: () => crmSrPopover.close(),
@@ -228,6 +228,8 @@ function createContentApplication(overrides = {}) {
}
});
const crmSrPopover = createCrmSrPopover({
openInternalSrForm: (parent, trigger) => internalSrForm.open(parent, trigger),
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
requestDetails: srId => internalSrSession.request(srId),
MODAL_ID: contracts.MODAL_ID,
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
@@ -240,6 +242,20 @@ function createContentApplication(overrides = {}) {
if (!result?.ok) throw new Error(result?.error || "Unable to load Service Request options.");
return result.items;
}
const internalSrForm = createInternalSrForm({
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
requestSave: async payload => {
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostInternalServiceRequest, payload, emailAddress: currentUser.getUserId() });
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
if (!result?.ok) throw new Error(result?.error || "Unable to create Internal Service Request.");
return result.result;
},
getOpportunity: number => number == null ? null : opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)),
MODAL_ID: contracts.MODAL_ID, environment, searchableSelect,
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
requestPlatform: () => requestSrFormLookup(messageTypes.requestLookupTypePlatform),
requestServices: parentServiceId => requestSrFormLookup(messageTypes.requestInternalSrServices, { parentServiceId, emailAddress: currentUser.getUserId() })
});
const crmSrForm = createCrmSrForm({
requestSave: async fields => {
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostServiceRequest, ...fields, emailAddress: currentUser.getUserId() });
@@ -261,8 +277,9 @@ function createContentApplication(overrides = {}) {
requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar)
});
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
createSrButton: number => crmSrForm.createButton(number),
closeSrForm: () => crmSrForm.close(false),
closeSrForm: () => { internalSrForm.close(false); crmSrForm.close(false); },
setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args),
activityCache,
reportProgress: (...args) => activityProgress.update("sr", ...args),
@@ -639,7 +656,6 @@ function createContentApplication(overrides = {}) {
});
const debugContentLog = createDebugContentLog({
previewDebugBody: createDebugBodyPreview,
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
debugState,
renderDebugPanel: (...args) => debugContentView.renderDebugPanel(...args)
});

View File

@@ -1,6 +1,5 @@
function createDebugContentLog({
previewDebugBody,
DEBUG_BODY_LIMIT,
debugState,
renderDebugPanel
}) {
@@ -99,7 +98,7 @@ function createDebugContentLog({
}
function createDebugBody(text) {
return previewDebugBody(text, DEBUG_BODY_LIMIT);
return previewDebugBody(text);
}
function headersToObject(headers) {
@@ -126,7 +125,7 @@ function createDebugContentLog({
.join("\n");
}
function formatDebugBody(body) {
function formatDebugBody(body, requestName = "") {
const text = body || "(none)";
try {
const normalized = String(text).replace(/^\uFEFF/, "").trimStart()
@@ -137,8 +136,14 @@ function createDebugContentLog({
for (let depth = 0; depth < 2 && typeof value === "string" && /^[\s]*[\[{]/.test(value); depth += 1) {
try { value = JSON.parse(value); } catch { break; }
}
if (!debugState.debugSecretsVisible && requestName === "requestSrTokenCatalog") {
value = value && typeof value === "object" && !Array.isArray(value)
? { ...value, ...(Object.hasOwn(value, "result") ? { result: formatHiddenDebugValue(value.result) } : {}) }
: formatHiddenDebugValue(value);
}
return JSON.stringify(debugState.debugSecretsVisible ? value : maskSensitiveDebugValue(value), null, 2);
} catch (error) {
if (!debugState.debugSecretsVisible && /^(requestSrTokenCatalog|requestCurrentUser|requestTokenRelay.*)$/.test(requestName)) return formatHiddenDebugValue(text);
return text;
}
}
@@ -162,7 +167,7 @@ function createDebugContentLog({
}
function isSensitiveDebugKey(key) {
return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$/i.test(key);
return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$|x-appbuilder-client-id/i.test(key);
}
function formatHiddenDebugValue(value) {

View File

@@ -26,6 +26,7 @@ function createDebugContentView({
if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions";
if (entry.name === "requestGenerateAiSummary") return "ai";
if (entry.name === "requestInstallBase") return "installBase";
if (["requestSRInfo", "requestLookupTypePlatform", "requestInternalSrServices", "requestPostInternalServiceRequest"].includes(entry.name)) return "serviceRequests";
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestResourceUser", "requestSrCatalog", "requestSrTokenCatalog", "requestPostServiceRequest"].includes(entry.name)) return "serviceRequests";
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
return "other";
@@ -249,7 +250,7 @@ function createDebugContentView({
appendDebugBlock(details, "Request Headers", formatHeaderBlock(entry.requestHeaders));
appendDebugBlock(details, "Request Body", formatDebugBody(entry.requestBody));
appendDebugBlock(details, "Response Headers", formatHeaderBlock(entry.responseHeaders));
appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody));
appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody, entry.name));
item.append(heading, details);
list.append(item);

View File

@@ -1,4 +1,4 @@
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, environment }) {
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment }) {
let status = "idle";
let byOpportunity = new Map();
let pending = null;
@@ -79,6 +79,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
`Title: ${display(item.Title)}`,
`Opportunity: ${display(item.OpportunityName_Id_c?.OptyNumber)}`,
`Reported Hours: ${display(item.ReportedHours_c)}`,
`Customer Deadline: ${display(formatDate(item.CustomerDeadline_c))}`,
`Owner: ${[item.Owner?.PersonFirstName, item.Owner?.PersonLastName].filter(Boolean).join(" ") || "N/D"}`,
`Email: ${display(item.Owner?.PrimaryEmail?.EmailAddress)}`,
`Do Not Call: ${display(item.DoNotCallFlag)}`, `Do Not Email: ${display(item.DoNotEmailFlag)}`

View File

@@ -1,4 +1,4 @@
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, environment }) {
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, formatDate, openInternalSrForm, environment }) {
const { document, window, setTimeout, clearTimeout } = environment;
let active = null;
let sequence = 0;
@@ -91,6 +91,29 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
label.textContent = "SR Number:";
heading.append(label, link);
if (number) heading.append(createCopyButton(String(number), "SR number", "SR Number"));
const add = document.createElement("button");
add.type = "button";
add.className = "opportunities-extension-copy-button opportunities-extension-internal-sr-add";
add.title = "Create Internal Service Request";
add.setAttribute("aria-label", add.title);
add.setAttribute("aria-haspopup", "dialog");
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
icon.setAttribute("viewBox", "0 0 24 24");
icon.setAttribute("aria-hidden", "true");
icon.setAttribute("focusable", "false");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M12 5v14M5 12h14");
path.setAttribute("stroke", "currentColor");
path.setAttribute("stroke-width", "2");
path.setAttribute("stroke-linecap", "round");
icon.append(path);
add.append(icon);
add.addEventListener("click", event => {
event.preventDefault(); event.stopPropagation();
close();
openInternalSrForm(item, button);
});
heading.append(add);
const details = document.createElement("div");
details.className = "opportunities-extension-sr-details";
details.textContent = descriptions[index];
@@ -126,7 +149,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
body.className = "opportunities-extension-sr-details";
body.textContent = ["Title", "OpportunityName_c", "StatusCd", "ServiceNew_c", "AccountPartyName",
"LastUpdateDate", "SvcHealth_c", "AssigneePersonName", "OpenDate"].map(key =>
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(record[key])}`).join("\n");
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(key === "LastUpdateDate" || key === "OpenDate" ? formatDate(record[key]) : record[key])}`).join("\n");
child.append(row, body);
children.append(child);
}

View File

@@ -0,0 +1,197 @@
function createInternalSrForm({ MODAL_ID, searchableSelect, formatDate, getOpportunity, requestPlatform, requestServices, requestSave, notify, environment }) {
const { document } = environment;
let active = null;
function node(tag, className, text) {
const el = document.createElement(tag);
if (className) el.className = className;
if (text !== undefined) el.textContent = text;
return el;
}
function close(restoreFocus = true) {
if (!active) return false;
if (active.saving) return true;
const previous = active;
active = null;
previous.modal.remove();
if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus();
return true;
}
function open(parent, trigger) {
if (active?.saving) return;
const host = document.getElementById(MODAL_ID);
if (!host || host.hidden) return;
close(false);
const modal = node("section", "opportunities-extension-action-form-modal");
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.setAttribute("aria-labelledby", "internal-sr-form-title");
const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-internal-sr-dialog");
const header = node("header", "opportunities-extension-account-plans-header");
const heading = node("div", "opportunities-extension-action-form-heading");
const title = node("h2", "", "Create Internal Service Request");
title.id = "internal-sr-form-title";
const context = node("dl", "opportunities-extension-action-form-context");
const opportunity = getOpportunity(parent.OpportunityName_Id_c?.OptyNumber);
for (const [label, value] of [["SR Number", parent.SrNumber], ["Opportunity", opportunity?.Name], ["Title", parent.Title]]) {
context.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "N/D" : String(value)));
}
heading.append(title, context);
header.append(heading);
const form = node("form", "opportunities-extension-action-form");
form.noValidate = true;
const fields = new Map();
const combos = new Map();
const ready = new Set();
function field(name, label, type, wide = false) {
const wrapper = node("label", "opportunities-extension-action-form-field" + (wide ? " opportunities-extension-action-form-wide" : ""));
const input = node(type === "textarea" || type === "select" ? type : "input");
if (!["textarea", "select"].includes(type)) input.type = type;
if (type === "textarea") input.rows = 4;
input.id = `internal-sr-${name}`;
input.name = name;
fields.set(name, input);
input.required = name !== "customerDeadline";
input.setAttribute("aria-required", String(input.required));
wrapper.htmlFor = input.id;
wrapper.append(node("span", "", input.required ? `${label} (required)` : label), input);
form.append(wrapper);
return { input, wrapper };
}
const titleInput = field("title", "Title", "text", true).input;
const deadline = field("customerDeadline", "Customer Deadline", "text").input;
deadline.readOnly = true;
deadline.value = formatDate(parent.CustomerDeadline_c) || "N/D";
field("internalDeadline", "Internal Deadline", "date");
const platform = field("platform", "Platform", "select", true);
field("description", "Description", "textarea", true);
const services = field("services", "Services", "select", true);
const footer = node("footer", "opportunities-extension-action-form-footer");
const cancel = node("button", "", "Cancel");
const save = node("button", "opportunities-extension-action-form-save", "Save");
save.type = "submit";
save.disabled = true;
const feedback = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide");
feedback.setAttribute("role", "status");
feedback.hidden = true;
cancel.type = "button";
cancel.addEventListener("click", () => close());
footer.append(save, cancel);
form.append(feedback, footer);
dialog.append(header, form);
modal.append(dialog);
host.append(modal);
const entry = { modal, trigger, saving: false };
active = entry;
const validDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`))
&& new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
form.addEventListener("submit", async event => {
event.preventDefault();
if (active !== entry || entry.saving || ready.size !== 2) return;
let firstInvalid = null;
for (const [name, input] of fields) {
if (!input.required) continue;
const value = String(input.value || "").trim();
const valid = !!value && (name !== "internalDeadline" || validDate(value))
&& (!combos.has(name) || Array.from(input.children).some(option => option.value === value));
input.setAttribute("aria-invalid", String(!valid));
const visible = combos.get(name)?.input || input;
visible.setAttribute("aria-invalid", String(!valid));
if (!valid && !firstInvalid) firstInvalid = visible;
}
feedback.hidden = false;
if (firstInvalid) {
feedback.setAttribute("role", "alert");
feedback.textContent = "Complete all required fields with valid values.";
firstInvalid.focus();
return;
}
const value = name => String(fields.get(name).value || "").trim();
const dateParts = value("customerDeadline").match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
const payload = {
Title: value("title"),
ServiceNew_Id_c: value("services"),
ProblemDescription: value("description"),
CustomerDeadline_c: dateParts ? `${dateParts[3]}-${dateParts[2]}-${dateParts[1]}` : "",
TechPlatformLookUp_c: value("platform"),
ServiceRequest_Id_SRToInternalSR: String(parent.SrId ?? ""),
SvcQuestionsCollection_c: [], InternalDeadline_c: value("internalDeadline")
};
entry.saving = true;
form.setAttribute("aria-busy", "true");
save.disabled = cancel.disabled = true;
save.textContent = "Saving...";
for (const [name, input] of fields) { input.disabled = true; combos.get(name)?.sync(); }
feedback.setAttribute("role", "status");
feedback.textContent = "Creating Internal Service Request...";
try {
await requestSave(payload);
entry.saving = false;
close();
notify("Internal Service Request created successfully.");
} catch (error) {
if (active !== entry) return;
feedback.setAttribute("role", "alert");
feedback.textContent = error.message || "Unable to create Internal Service Request.";
notify(feedback.textContent, true);
} finally {
entry.saving = false;
if (active === entry) {
form.setAttribute("aria-busy", "false");
save.disabled = cancel.disabled = false;
save.textContent = "Save";
for (const [name, input] of fields) { input.disabled = false; combos.get(name)?.sync(); }
}
}
});
function load(target, request, service) {
const { input, wrapper } = target;
input.disabled = true;
const combo = searchableSelect(input, wrapper);
combos.set(input.name, combo);
const status = node("p", "opportunities-extension-action-form-status", "Loading...");
status.setAttribute("role", "status");
wrapper.append(status);
wrapper.setAttribute("aria-busy", "true");
Promise.resolve().then(request).then(items => {
if (active !== entry) return;
const placeholder = node("option", "", items.length ? "Select an option" : "No options available");
placeholder.value = "";
input.append(placeholder);
const seen = new Set();
for (const item of items) {
const value = String(service ? item.Id : item.code);
if (seen.has(value)) continue;
seen.add(value);
const option = node("option", "", service ? item.RecordName : item.name);
option.value = value;
if (service && item.Description_c) option.setAttribute("data-description", item.Description_c);
input.append(option);
}
input.value = "";
input.disabled = !items.length;
if (items.length) ready.add(input.name);
save.disabled = ready.size !== 2;
status.textContent = items.length ? "" : "No options available.";
status.hidden = !!items.length;
combo.sync();
}).catch(error => {
if (active !== entry) return;
status.setAttribute("role", "alert");
status.textContent = error.message || "Unable to load options. Close and reopen the form to retry.";
}).finally(() => { if (active === entry) wrapper.setAttribute("aria-busy", "false"); });
}
load(platform, requestPlatform, false);
load(services, () => requestServices(parent.SrId), true);
modal.addEventListener("keydown", event => {
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
if (event.key === "Tab") {
const focusable = Array.from(form.querySelectorAll("input, textarea, button")).filter(el => !el.disabled && !el.hidden);
const first = focusable[0], last = focusable.at(-1);
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
}
});
titleInput.focus();
}
return { open, close };
}

View File

@@ -66,11 +66,13 @@ function createSearchableSelect({ environment }) {
}
function render(query = "") {
if (input.disabled) return;
matches = options().filter(item => normalize(`${item.textContent} ${item.value}`).includes(normalize(query)));
matches = options().filter(item => normalize(`${item.textContent} ${item.getAttribute?.("data-description") || ""} ${item.value}`).includes(normalize(query)));
index = -1;
list.textContent = "";
matches.forEach((item, i) => {
const row = element("div", "opportunities-extension-action-option", item.textContent);
const description = item.getAttribute?.("data-description");
if (description) row.append(element("small", "opportunities-extension-option-description", description));
row.id = `${list.id}-${i}`;
row.setAttribute("role", "option");
row.setAttribute("aria-selected", "false");

View File

@@ -626,3 +626,5 @@
@media (prefers-reduced-motion: reduce) {
.opportunities-extension-refresh-all[aria-busy="true"] svg { animation: none; }
}
.opportunities-extension-option-description { display: block; font-size: 12px; line-height: 1.4; margin-top: 4px; white-space: normal; overflow-wrap: anywhere; opacity: .85; }
.opportunities-extension-action-form-dialog.opportunities-extension-internal-sr-dialog { width: min(760px, 100%); }

View File

@@ -11,7 +11,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
{ op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" }
] },
keywords: null,
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title",
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "CustomerDeadline_c", "PrimaryPillarNew_c", "_actions", "Title",
"Owner.PersonFirstName", "Owner.PersonLastName", "Owner.PrimaryEmail.EmailAddress", "DoNotCallFlag", "DoNotEmailFlag"],
sort: [{ attribute: "_score", direction: "descending" }], language: "en",
keywordsFields: ["SrNumber", "ServiceNew_Id_c.RecordName", "CustomerAccount", "OpportunityName_Id_c",
@@ -29,7 +29,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient"
};
const entry = logRequestStart({ name: "resquestCrmServiceRequest", method: "POST", url,
headers: { ...headers, Authorization: `Bearer ${maskToken(token)}` }, body: JSON.stringify(payload, null, 2), metadata: { offset, attempt } });
headers: { ...headers }, body: JSON.stringify(payload, null, 2), metadata: { offset, attempt } });
try {
const response = await fetch(url, {
method: "POST", credentials: "include", headers, body: JSON.stringify(payload)

View File

@@ -24,7 +24,7 @@ function createFusionOpportunities({
url: OPPORTUNITIES_QUERY_URL,
headers: {
Accept: "application/json",
Authorization: `Bearer ${maskToken(token)}`,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Origin: "https://eeho.fa.us2.oraclecloud.com",
Preference: "transient"
@@ -151,7 +151,8 @@ function createFusionOpportunities({
"CustomerAccount.PartyUniqueName",
"CustomerAccount.PartyId",
"CustomerAccount.PartyNumber",
"CustomerAccount.Region_c"
"CustomerAccount.Region_c",
"CustomerAccount.PrimaryAddress.CountryCode"
],
sort: [],
language: "en",

View File

@@ -1,4 +1,4 @@
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders }) {
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
@@ -11,26 +11,76 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
return value.trim();
}
function dateOnly(value, name) {
const date = text(value, name);
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error(`Invalid ${name}.`);
return date;
}
function payloadFor(input) {
const payload = {};
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
const date = payload.CustomerDeadline_c;
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
dateOnly(payload.CustomerDeadline_c, "Customer deadline");
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
}
async function requestResourceUser(email, debugRequests) {
const emailAddress = text(email, "current user email");
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestResourceUser", debugRequests);
return numericId(resource?.resourcePartyId, "resourcePartyId");
}
async function requestPostInternalServiceRequest(message) {
const debugRequests = [];
try {
const source = message.payload;
const parentSrId = numericId(source?.ServiceRequest_Id_SRToInternalSR, "parent SrId");
const payload = {
Title: text(source?.Title, "Title"),
ServiceNew_Id_c: numericId(source?.ServiceNew_Id_c, "ServiceNew_Id_c"),
ProblemDescription: text(source?.ProblemDescription, "Description"),
CustomerDeadline_c: dateOnly(source?.CustomerDeadline_c, "Customer deadline"),
TechPlatformLookUp_c: text(source?.TechPlatformLookUp_c, "Platform"),
InternalDeadline_c: `${dateOnly(source?.InternalDeadline_c, "Internal Deadline")}T00:00:00.000Z`,
SvcQuestionsCollection_c: []
};
const parent = await requestSRInfo(parentSrId, debugRequests);
payload.ServiceRequest_Id_SRToInternalSR = String(parentSrId);
for (const name of ["AccountPartyId", "OpportunityName_Id_c"]) payload[name] = numericId(parent[name], name);
for (const name of ["OpportunityName_c", "RegionText_c", "SvcHealth_c", "Language_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(parent[name], name);
payload.SecondaryPillarNew_c = parent.SecondaryPillarNew_c == null ? null : text(parent.SecondaryPillarNew_c, "SecondaryPillarNew_c");
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
const result = await fetchJson(url, { method: "POST", credentials: "include",
headers: await internalServiceRequestHeaders(url),
body: JSON.stringify(payload)
}, "requestPostInternalServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
async function requestSRInfo(parentSrId, debugRequests) {
const url = new 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");
url.searchParams.set("fields", "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
url.searchParams.set("onlyData", "true");
url.searchParams.set("orderBy", "LastUpdateDate:desc");
url.searchParams.set("q", `SrId=${parentSrId}`);
const result = await fetchJson(url.href, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestSRInfo", debugRequests);
if (!Array.isArray(result?.items) || result.items.length !== 1 || !result.items[0]
|| (result.items[0].SrId != null && String(result.items[0].SrId) !== String(parentSrId))) {
throw new Error("requestSRInfo did not return the selected parent Service Request.");
}
return result.items[0];
}
async function requestPostServiceRequest(message) {
const debugRequests = [];
try {
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
const payload = payloadFor(message.payload);
if (message.phase === "prepare") {
const emailAddress = text(message.emailAddress, "current user email");
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestResourceUser", debugRequests);
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
const sharedHeaders = await previewTokenHeaders(catalog);
return { ok: true, payload, debugRequests, headers: {
requestSrTokenCatalog: sharedHeaders,
@@ -38,7 +88,20 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
} };
}
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
let id = message.catalogId;
const headers = await requestCreationHeaders(message.catalogId, endpoint, debugRequests);
const result = await fetchJson(endpoint, {
method: "POST", credentials: "include", headers,
body: JSON.stringify(payload)
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
} finally {
annotateSigningRequest(debugRequests);
}
}
async function requestCreationHeaders(id, target, debugRequests) {
if (id == null) {
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
@@ -47,7 +110,7 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
}
id = numericId(id, "Catalogue Id");
const tokenUrl = `${catalog}/${id}`;
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
let catalogHeaders = await tokenHeaders(tokenUrl, target);
const requestToken = () => fetchJson(tokenUrl, {
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
}, "requestSrTokenCatalog", debugRequests);
@@ -64,25 +127,14 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
}
const result = await fetchJson(endpoint, {
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
body: JSON.stringify(payload)
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
} finally {
// Never forward the signing token to content scripts, even with Debug secrets enabled.
return { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() };
}
function annotateSigningRequest(debugRequests) {
for (const entry of debugRequests) {
if (entry.name === "requestSrTokenCatalog") {
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
entry.responseBody = JSON.stringify({ result: "(redacted)" });
entry.responseBodyTruncated = false;
}
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
}
}
}
return { requestPostServiceRequest };
return { requestPostServiceRequest, requestPostInternalServiceRequest };
}

View File

@@ -194,7 +194,33 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
return { ok: true, items: result.items.map(({ Id, RecordName, Abstract_c }) => ({ Id: String(Id), RecordName, Abstract_c: Abstract_c == null ? "" : String(Abstract_c) })), debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
async function requestInternalSrOptions(message, services = false) {
const debugRequests = [];
const name = services ? "requestInternalSrServices" : "requestLookupTypePlatform";
try {
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/";
let path = "lookups?type=STANDARD&lookupType=SENGG_TECH_PLATFORM";
if (services) {
const id = String(message.parentServiceId ?? "");
if (!/^\d+$/.test(id)) throw new Error("A numeric parent SrId is required.");
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
path = `services?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&parentServiceId=${encodeURIComponent(id)}&serviceType=INTERNAL`;
}
const result = await fetchJson(base + path, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" }
}, name, debugRequests);
if (!Array.isArray(result?.items) || result.items.some(item => services
? !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string"
: typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${name} response.`);
const items = services
? result.items.map(item => ({ Id: String(item.Id), RecordName: item.RecordName, Description_c: String(item.Description_c ?? "") }))
: result.items.filter(item => item.enabledFlag !== "N").map(({ code, name }) => ({ code, name }));
return { ok: true, items, debugRequests };
} catch (error) { error.debugRequests = debugRequests; throw error; }
}
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
requestLookupTypePlatform: () => requestInternalSrOptions({}),
requestInternalSrServices: message => requestInternalSrOptions(message, true),
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),

View File

@@ -1,6 +1,5 @@
function createHttpBackgroundJson({
previewDebugBody,
DEBUG_BODY_LIMIT,
observeResponseHeaders = () => {},
environment
}) {
@@ -127,7 +126,7 @@ function createHttpBackgroundJson({
}
function createDebugBody(text) {
return previewDebugBody(text, DEBUG_BODY_LIMIT);
return previewDebugBody(text);
}
function normalizeDebugHeaders(headers) {

View File

@@ -15,12 +15,6 @@ function createSpaCurrentUser({ fetchJson }) {
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
error.debugRequests = debugRequests;
throw error;
} finally {
// The identity token is not needed by content scripts or debug, including on failures.
for (const entry of debugRequests) {
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
entry.responseBodyTruncated = false;
}
}
}
return { requestCurrentUser };

View File

@@ -1,40 +1,3 @@
function createDebugBodyPreview(text, limit) {
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
try {
const normalized = text.replace(/^\uFEFF/, "").trimStart()
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
let value = JSON.parse(normalized);
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
try { value = JSON.parse(value); } catch { break; }
}
// Bound retained data without cutting JSON syntax or losing sensitive field names.
let remaining = Math.max(0, limit - 256);
const visit = (node, depth = 0) => {
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
if (node === null || typeof node !== "object") {
const serialized = JSON.stringify(node);
if (serialized.length > remaining) {
remaining = 0;
return "[omitted from debug preview]";
}
remaining -= serialized.length + 1;
return node;
}
const result = Array.isArray(node) ? [] : Object.create(null);
remaining -= 2;
for (const key of Object.keys(node)) {
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
if (remaining < cost + 64) break;
remaining -= cost;
result[key] = visit(node[key], depth + 1);
}
return result;
};
return {
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
truncated: true
};
} catch {
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
}
function createDebugBodyPreview(text) {
return { value: text || "(empty response body)", truncated: false };
}

View File

@@ -17,9 +17,12 @@ function createMessageTypes() {
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest",
prepareSrSession: "opportunitiesExtension.prepareSrSession",
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform",
requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices",
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",

View File

@@ -93,7 +93,7 @@ test("opportunity query preserves fields, record-set OR, date AND, and paginatio
{ op: "$or", criteria: types.map(value => ({ op: "$eq", attribute: "RecordSet", value })) },
{ op: "$wi", attribute: "EffectiveDate", value1: range.startCloseDate, value2: range.endCloseDate, dynamicDate: false }
] }, keywords: null, keywordsFields: fields,
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c"],
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c", "CustomerAccount.PrimaryAddress.CountryCode"],
sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true,
copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: 15, offset: 15
});
@@ -183,6 +183,8 @@ test("tokenrelay missing-cookie recovery and HTTP failure do not change the requ
assert.equal(h.refreshes.length, 1);
assert.equal(calls.length, 1);
assert.equal(calls[0].options.headers["x-xsrf-token"], "synthetic-xsrf");
assert.equal(h.requestLog.at(-1).requestHeaders["x-xsrf-token"], "synthetic-xsrf");
assert.equal(JSON.parse(h.requestLog.at(-1).responseBody).access_token, "synthetic-access");
assert.equal(calls[0].options.method, "GET");
for (const status of [400, 401, 403]) {
let attempts = 0;

View File

@@ -28,6 +28,14 @@ test("SR badge shows reported hours, sums linked SRs and preserves missing value
}
});
test("SR tooltip formats customer deadlines without timezone day shifts", async () => {
for (const [value, expected] of [["2026-05-14T00:00:00.000+0000", "14/05/2026"], [null, "N/D"], [undefined, "N/D"], ["", "N/D"]]) {
const app = setup(async () => response({ items: [{ ...sr("A"), CustomerDeadline_c: value }], hasMore: false }));
await app.opportunityCrmServiceRequests.load("synthetic", 1);
assert.ok(app.opportunityCrmServiceRequests.createBadge("A").attributes["aria-label"].includes(`Customer Deadline: ${expected}`));
}
});
test("CRM fetch is called without the environment object as its receiver", async () => {
let calls = 0;
const app = setup(async function (url, options) {
@@ -76,7 +84,7 @@ test("CRM query preserves payload and pagination after opportunities, uses token
{ op: "$in", attribute: "StatusCd", values: ["SVC_PENDING_DELIVERY"] },
{ op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" }
] }, keywords: null,
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title", "Owner.PersonFirstName",
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "CustomerDeadline_c", "PrimaryPillarNew_c", "_actions", "Title", "Owner.PersonFirstName",
"Owner.PersonLastName", "Owner.PrimaryEmail.EmailAddress", "DoNotCallFlag", "DoNotEmailFlag"],
sort: [{ attribute: "_score", direction: "descending" }], language: "en",
keywordsFields: ["SrNumber", "ServiceNew_Id_c.RecordName", "CustomerAccount", "OpportunityName_Id_c", "OpportunityName_Id_c.OptyNumber", "Owner"],
@@ -90,7 +98,11 @@ test("CRM query preserves payload and pagination after opportunities, uses token
assert.equal(log.status, "success");
assert.ok(log.responseBody);
assert.ok(log.durationMs >= 0);
assert.doesNotMatch(log.requestHeaders.Authorization, /synthetic-token/);
assert.equal(log.requestHeaders.Authorization, "Bearer synthetic-token");
assert.doesNotMatch(app.debugContentLog.formatHeaderBlock(log.requestHeaders), /synthetic-token/);
app.debugState.debugSecretsVisible = true;
assert.match(app.debugContentLog.formatHeaderBlock(log.requestHeaders), /Bearer synthetic-token/);
app.debugState.debugSecretsVisible = false;
assert.equal(app.debugContentView.getRequestCategory(log), "serviceRequests");
}
const found = app.opportunityCrmServiceRequests.createBadge("TEST-1");

View File

@@ -3,6 +3,33 @@ const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const type = "opportunitiesExtension.requestInternalServiceRequest";
test("internal SR form lookups use management proxy, Session and parent SrId", async () => {
const calls = [];
const h = loadBackground({ fetch: async (url, options) => {
calls.push({ url: new URL(url), options });
return response({ items: String(url).includes("/lookups?")
? [{ code: "OCI", name: "Oracle Cloud" }]
: [{ Id: 123, RecordName: "Review", Description_c: "Review the architecture" }] });
} });
const platform = await h.dispatch({ type: "opportunitiesExtension.requestLookupTypePlatform" });
assert.equal(platform.ok, true);
assert.equal(platform.items[0].code, "OCI");
const service = await h.dispatch({ type: "opportunitiesExtension.requestInternalSrServices", parentServiceId: "456", emailAddress: "user+test@example.invalid" });
assert.equal(service.ok, true);
assert.equal(service.items[0].Id, "123");
assert.equal(service.items[0].Description_c, "Review the architecture");
assert.equal(calls[1].url.searchParams.get("parentServiceId"), "456");
assert.equal(calls[1].url.searchParams.get("emailAddress"), "user+test@example.invalid");
assert.equal(calls[1].url.searchParams.get("serviceType"), "INTERNAL");
for (const call of calls) {
assert.match(call.url.pathname, /SeaaS_SR_management.*oalsetSeaaSOKECustomRestAPI/);
assert.equal(call.options.headers.Authorization, "Session");
assert.equal(call.options.credentials, "include");
}
assert.equal((await h.dispatch({ type: "opportunitiesExtension.requestInternalSrServices", parentServiceId: "bad", emailAddress: "user@example.invalid" })).ok, false);
assert.equal(calls.length, 2);
});
test("preparation reuses an existing unexpired cookie pair without navigation or API calls", async () => {
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
const h = loadBackground({

View File

@@ -3,6 +3,87 @@ const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const flush = () => new Promise(resolve => setImmediate(resolve));
test("internal SR form validates Save, maps parent data, prevents duplicate posts and preserves failures", async () => {
const h = setup();
h.app.currentUser.getUserId = () => "current@example.invalid";
h.app.opportunitiesTableState.opportunitiesTableState.items = [
{ OptyNumber: "OTHER", Name: "Other opportunity", CustomerAccount: { PrimaryAddress: { CountryCode: "US" } } },
{ OptyNumber: "OP1", Name: "<Opportunity name>", OptyId: 456, CountryCode: "CA",
CustomerAccount: { PartyId: 789, Region_c_localizedValue: "LAD", CountryCode: "AR", PrimaryAddress: { CountryCode: "BR" } } }
];
h.app.debugContentView.renderDebugPanel = () => {};
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{ SrId: "123", SrNumber: "SR123",
CustomerDeadline_c: "2026-05-14T00:00:00.000+0000", PrimaryPillarNew_c: "Tech_Cloud_Infrastructure", Title: "<Parent SR title>", OpportunityName_Id_c: { OptyNumber: "OP1" } }];
const calls = [];
h.app.platformContentMessages.sendRuntimeMessage = async message => {
calls.push(message);
return { ok: true, items: message.type.endsWith("requestLookupTypePlatform") ? [{ code: "OCI", name: "Oracle Cloud" }]
: message.type.endsWith("requestInternalSrServices") ? [{ Id: "45", RecordName: "Review", Description_c: "Architecture review" }] : [] };
};
await h.app.opportunityCrmServiceRequests.load("synthetic", 1);
const badge = h.app.opportunityCrmServiceRequests.createBadge("OP1");
h.modal.append(badge);
badge.listeners.pointerenter();
await flush();
const add = h.walk(h.panel()).find(el => el.title === "Create Internal Service Request");
assert.ok(add);
assert.equal(add.parent.children[2].title, "Copy SR number");
add.listeners.click({ preventDefault() {}, stopPropagation() {} });
await flush();
const nodes = h.walk(h.modal);
const context = nodes.find(el => el.className === "opportunities-extension-action-form-context");
assert.deepEqual(context.children.map(el => el.textContent), ["SR Number", "SR123", "Opportunity", "<Opportunity name>", "Title", "<Parent SR title>"]);
assert.equal(nodes.find(el => el.name === "customerDeadline").value, "14/05/2026");
assert.equal(nodes.find(el => el.name === "customerDeadline").readOnly, true);
assert.equal(nodes.find(el => el.name === "customerDeadline").required, false);
for (const name of ["title", "internalDeadline", "platform", "description", "services"]) {
const input = nodes.find(el => el.name === name);
assert.equal(input.required, true, name);
assert.equal(input.attributes["aria-required"], "true", name);
assert.match(input.parent.children[0].textContent, /\(required\)$/);
}
for (const name of ["platform", "services"]) {
const input = nodes.find(el => el.id === `internal-sr-${name}-search`);
assert.equal(input.required, true);
assert.equal(input.attributes["aria-required"], "true");
}
assert.equal(nodes.find(el => el.name === "internalDeadline").type, "date");
assert.equal(nodes.find(el => el.name === "platform").children[1].value, "OCI");
assert.equal(nodes.find(el => el.name === "services").children[1].attributes["data-description"], "Architecture review");
assert.equal(calls.at(-1).parentServiceId, "123");
assert.equal(calls.at(-1).emailAddress, "current@example.invalid");
const form = nodes.find(el => el.tag === "form");
assert.deepEqual(h.walk(form).filter(el => el.tag === "button").map(el => el.textContent), ["Save", "Cancel"]);
const submit = () => form.listeners.submit({ preventDefault() {} });
const before = calls.length;
await submit();
assert.equal(calls.length, before);
assert.equal(nodes.find(el => el.name === "title").attributes["aria-invalid"], "true");
for (const [name, value] of Object.entries({ title: "New internal SR", internalDeadline: "2026-05-01", platform: "OCI", description: "Detailed request", services: "45" })) nodes.find(el => el.name === name).value = value;
const posts = [], toasts = [];
h.app.opportunitiesLinks.showCopyConfirmation = (message, isError = false) => toasts.push({ message, isError });
h.app.platformContentMessages.sendRuntimeMessage = message => new Promise(resolve => posts.push({ message, resolve }));
const saving = submit();
await submit();
assert.equal(posts.length, 1);
assert.equal(posts[0].message.type, "opportunitiesExtension.requestPostInternalServiceRequest");
assert.equal(posts[0].message.emailAddress, "current@example.invalid");
assert.deepEqual(JSON.parse(JSON.stringify(posts[0].message.payload)), {
Title: "New internal SR", ServiceNew_Id_c: "45",
ProblemDescription: "Detailed request", CustomerDeadline_c: "2026-05-14", TechPlatformLookUp_c: "OCI",
ServiceRequest_Id_SRToInternalSR: "123", SvcQuestionsCollection_c: [], InternalDeadline_c: "2026-05-01"
});
posts[0].resolve({ ok: false, httpStatus: 403, error: "Synthetic save failure" });
await saving;
assert.equal(nodes.find(el => el.name === "title").value, "New internal SR");
assert.deepEqual(toasts, [{ message: "Synthetic save failure", isError: true }]);
const retry = submit();
posts[1].resolve({ ok: true, result: { SrNumber: "ISR1" } });
await retry;
assert.deepEqual(toasts.at(-1), { message: "Internal Service Request created successfully.", isError: false });
assert.ok(!h.walk(h.modal).includes(form));
});
test("internal SR uses Session without TokenRelay and reports unavailable iframe host", async () => {
const h = setup();
let renewals = 0;
@@ -146,7 +227,8 @@ test("SR tooltip loads nested details with copy/link and empty text, discarding
assert.equal(calls[0].message.token, undefined);
assert.equal(calls[0].message.srId, "NOT-THE-URL-ID");
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Loading SR details..."));
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue" }] });
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue",
LastUpdateDate: "2026-05-14T00:00:00.000+0000", OpenDate: "2026-04-01T23:30:00.000+0000" }] });
calls[1].resolve({ ok: true, items: [] });
await flush();
const nodes = h.walk(h.panel());
@@ -155,6 +237,8 @@ test("SR tooltip loads nested details with copy/link and empty text, discarding
assert.ok(link.href.endsWith("srNumber=SR%20%26child&srId=99"));
assert.equal(link.parent.children[2].title, "Copy SR number");
assert.ok(nodes.some(node => node.textContent.includes("Service Queue: Queue")));
assert.ok(nodes.some(node => node.textContent.includes("LastUpdateDate: 14/05/2026")));
assert.ok(nodes.some(node => node.textContent.includes("OpenDate: 01/04/2026")));
assert.ok(nodes.some(node => node.textContent === "No further details."));
h.app.crmSrPopover?.close();
h.app.opportunityCrmServiceRequests.reset();

View File

@@ -5,7 +5,7 @@ const { loadApplication } = require("./support/application.cjs");
const type = "opportunitiesExtension.requestCurrentUser";
for (const family of ["chrome", "firefox"]) {
test(`${family}: current user GET uses SPA cookies and discards identity token before messaging/debug`, async () => {
test(`${family}: current user GET preserves identity token only in debug data for explicit reveal`, async () => {
const calls = [];
const app = loadBackground({ family, fetch: async (url, options) => {
calls.push({ url, options });
@@ -20,12 +20,16 @@ for (const family of ["chrome", "firefox"]) {
assert.equal(calls[0].options.cache, "no-store");
assert.equal(result.debugRequests[0].name, "requestCurrentUser");
assert.equal(result.debugRequests[0].httpStatus, 200);
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
assert.equal(JSON.parse(result.debugRequests[0].responseBody).token, "synthetic-secret");
const content = loadApplication("content");
assert.doesNotMatch(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
content.debugState.debugSecretsVisible = true;
assert.match(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
assert.equal(result.token, undefined);
});
}
test("identity failures and malformed roles remain explicit and never expose response tokens", async () => {
test("identity failures retain diagnostics but hide response tokens by default", async () => {
for (const fetch of [
async () => new Response("synthetic-secret<html>fallback</html>"),
async () => response({ token: "synthetic-secret", user: {} }),
@@ -37,7 +41,8 @@ test("identity failures and malformed roles remain explicit and never expose res
const result = await app.dispatch({ type });
assert.equal(result.ok, false);
assert.equal(result.debugRequests[0].status, "failed");
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
const content = loadApplication("content");
assert.doesNotMatch(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
}
});

View File

@@ -20,6 +20,26 @@ function fixture() {
return { ...dom, app, add, nodes, tab, ids, select };
}
test("catalog and identity secrets are revealed exactly by the eye button and hidden again", () => {
const h = fixture();
const body = JSON.stringify({ result: "catalog-exact-secret", detail: "Preserved" });
h.app.debugContentLog.appendBackgroundRequestLog([{
name: "requestSrTokenCatalog", method: "POST", startedAt: new Date().toISOString(), status: "success",
responseBody: body, requestHeaders: { "vb-proxy-header-x-oracle-ectoken": "header-exact-secret", "x-appbuilder-client-id": "client-exact-secret" }
}, {
name: "requestCurrentUser", method: "GET", startedAt: new Date().toISOString(), status: "success",
responseBody: JSON.stringify({ user: { roles: ["SIMPLE_USER"] }, token: "identity-exact-secret" })
}]);
const text = node => node.text + node.children.map(text).join("");
const toggle = () => h.nodes().find(node => node.getAttribute("data-debug-focus") === "visibility").listeners.click();
assert.doesNotMatch(text(h.panel), /exact-secret/);
toggle();
for (const secret of ["catalog-exact-secret", "header-exact-secret", "client-exact-secret", "identity-exact-secret"]) assert.ok(text(h.panel).includes(secret));
toggle();
assert.doesNotMatch(text(h.panel), /exact-secret/);
assert.equal(h.app.debugState.requestLog[0].responseBody, body, "display masking never overwrites originals");
});
test("request summaries show duration after status, including zero, failure and pending", () => {
const h = fixture();
h.add("duration", "requestOpportunities", 1);
@@ -33,23 +53,23 @@ test("request summaries show duration after status, including zero, failure and
}
});
test("large response previews stay valid, formatted, masked and colored in both runtimes", () => {
test("complete large responses stay formatted, masked and colored in both runtimes", () => {
const h = fixture();
const background = loadApplication("background");
const body = JSON.stringify({ items: Array.from({ length: 2000 }, (_, index) => ({
id: index, name: "Synthetic opportunity", access_token: "synthetic-secret",
uiaasHeader: { value: "synthetic-header-secret" }, owner: { name: "Synthetic owner" }
})) });
for (const [logger, limit] of [[h.app.debugContentLog, 12000], [background.httpBackgroundJson, 100000]]) {
for (const logger of [h.app.debugContentLog, background.httpBackgroundJson]) {
const preview = logger.createDebugBody(body);
assert.equal(preview.truncated, true);
assert.ok(preview.value.length <= limit);
assert.equal(preview.truncated, false);
assert.equal(preview.value, body);
const parsed = JSON.parse(preview.value);
assert.match(parsed.debugNotice, /Partial response/);
assert.ok(parsed.preview.items.length > 0);
assert.ok(parsed.preview.items.length < 2000);
assert.equal(parsed.items.length, 2000);
assert.equal(parsed.items.at(-1).id, 1999);
const formatted = h.app.debugContentLog.formatDebugBody(preview.value);
assert.match(formatted, /\n "debugNotice"/);
assert.match(formatted, /\n "items"/);
assert.match(formatted, /"id": 1999/);
assert.doesNotMatch(formatted, /synthetic-secret|synthetic-header-secret/);
const container = h.document.createElement("div");
h.app.debugContentView.appendDebugBlock(container, "Response Body", formatted);
@@ -58,7 +78,9 @@ test("large response previews stay valid, formatted, masked and colored in both
assert.match(h.app.debugContentLog.formatDebugBody(preview.value), /synthetic-secret/);
h.app.debugState.debugSecretsVisible = false;
assert.equal(logger.createDebugBody('{"ok":true}').value, '{"ok":true}');
assert.match(logger.createDebugBody("<html>" + "x".repeat(limit + 1000)).value, /truncated/);
const html = "<html>" + "x".repeat(150000) + "</html>";
assert.equal(logger.createDebugBody(html).value, html);
assert.equal(logger.createDebugBody("").value, "(empty response body)");
}
});
@@ -69,7 +91,7 @@ test("request categories cover every current family and unknown requests stay vi
requestConsumerTokenService: "authentication", requestCustomerTokenService: "authentication",
requestOpportunities: "opportunities", requestOpptyProducts: "products", requestGenerateAiSummary: "ai",
requestAccountSummary: "consumption", requestAccountPlans: "consumption",
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests",
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests", requestSRInfo: "serviceRequests",
requestConsumptionPlanDetail: "consumption",
requestRatecardByPlan: "consumption",
requestConsumptionServiceQuarterly: "consumption", requestConsumptionServiceMonthly: "consumption",

View File

@@ -1,10 +1,12 @@
(() => {
const listeners = [];
const storage = new Map();
const internalSrPreview = new URLSearchParams(location.search).has("internal-sr");
const opportunities = Array.from({ length: 25 }, (_, index) => ({
OptyNumber: `TEST${index + 1}`,
OptyId: 1000 + index,
Name: `Synthetic opportunity ${index + 1} - Database migration`,
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: "test-party", PartyNumber: "test-account" },
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: internalSrPreview ? 789 : "test-party", PartyNumber: "test-account", Region_c_localizedValue: "LAD", PrimaryAddress: { CountryCode: "BR" } },
Owner: { PersonFirstName: "Example", PersonLastName: "Owner" },
PrimaryRevenue: { RevnAmountCurcyCode: "USD", RevnAmount: "25000", WinProb: 60 },
EffectiveDate: index === 24 ? null : `2026-11-${String(index < 8 ? 15 : 1 + index).padStart(2, "0")}T00:00:00Z`,
@@ -28,7 +30,11 @@
onMessage: { addListener: listener => listeners.push(listener) },
async sendMessage(message) {
const operation = message.type.split(".").at(-1);
if (operation === "requestCurrentUser") return { ok: true, user: { id: "example@example.invalid", name: "Example Owner", roles: ["SALES_REP"] }, debugRequests: [] };
if (operation === "requestPostInternalServiceRequest") return { ok: true, result: { SrNumber: "ISR123" }, debugRequests: [] };
if (operation === "requestCurrentUser") return { ok: true, user: { id: "example@example.invalid", name: "Example Owner", roles: internalSrPreview ? [] : ["SALES_REP"] }, debugRequests: [] };
if (["prepareSrSession", "requestInternalServiceRequest", "requestWlActions"].includes(operation)) return { ok: true, items: [], actions: [], debugRequests: [] };
if (operation === "requestLookupTypePlatform") return { ok: true, items: [{ code: "OCI", name: "Oracle Cloud Infrastructure" }, { code: "MULTI", name: "Multicloud" }], debugRequests: [] };
if (operation === "requestInternalSrServices") return { ok: true, items: [{ Id: "123", RecordName: "Architecture review", Description_c: "Review the customer architecture, workloads and technical requirements." }, { Id: "124", RecordName: "Technical workshop", Description_c: "Prepare and deliver a technical session for the customer team." }], debugRequests: [] };
if (operation === "getXsrfToken") return { ok: true, token: "synthetic-xsrf", cookieName: "XSRF-TOKEN-TEST" };
if (operation === "requestConsumerTokenService") return { ok: true, authorizations, debugRequests: [] };
if (operation === "requestOpptyProducts") {
@@ -65,7 +71,8 @@
let body;
if (url.pathname === "/fscmRestApi/tokenrelay") body = { access_token: "synthetic-fusion-token", expires_in: 3600 };
else if (url.pathname.endsWith("custom-actions/queries")) {
const { offset, limit } = JSON.parse(options.body);
const { offset, limit, entity } = JSON.parse(options.body);
if (entity === "CRMServiceRequest") return new Response(JSON.stringify({ items: [{ SrId: "123456", SrNumber: "SR123456", Title: "Example parent SR", PrimaryPillarNew_c: "Tech_Cloud_Infrastructure", CustomerDeadline_c: "2026-11-30T00:00:00.000+0000", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "TEST1" } }], hasMore: false }), { status: 200 });
body = { items: opportunities.slice(offset, offset + limit), totalResults: opportunities.length, count: Math.min(limit, opportunities.length - offset), hasMore: offset + limit < opportunities.length, offset, limit };
} else if (url.pathname.endsWith("summarizations/generate")) body = { summaryText: "Synthetic AI summary.\nText is displayed safely: <script>never executed</script>." };
else throw new Error(`Unexpected synthetic fetch: ${url.pathname}`);

View File

@@ -1,8 +1,141 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const { loadApplication } = require("./support/application.cjs");
function assertDebugVisibility(result, secret) {
const app = loadApplication("content");
const entries = result.debugRequests || [];
const render = () => entries.map(entry => app.debugContentLog.formatHeaderBlock(entry.requestHeaders)
+ app.debugContentLog.formatDebugBody(entry.responseBody, entry.name)).join("\n");
assert.ok(!render().includes(secret));
app.debugState.debugSecretsVisible = true;
assert.equal(render().includes(secret), JSON.stringify(entries).includes(secret));
}
const type = "opportunitiesExtension.requestPostServiceRequest";
const emailAddress = "signed.in+sr@example.invalid";
const internalType = "opportunitiesExtension.requestPostInternalServiceRequest";
const internalInput = () => ({ Title: "Internal request title", ServiceNew_Id_c: "404", ProblemDescription: "Form description",
CustomerDeadline_c: "2026-11-29", TechPlatformLookUp_c: "DATABASE", InternalDeadline_c: "2027-01-31",
ServiceRequest_Id_SRToInternalSR: "505" });
const parentInfo = () => ({ SrId: 505, SrNumber: "SR505", AccountPartyId: 100000693906507, OpportunityName_c: "A8YPN8",
OpportunityName_Id_c: 302959906052470, RegionText_c: "LAD", SvcHealth_c: "SVC_HEALTH_YELLOW", Language_c: "PORTUGUESE",
CountryText_c: "BR", PrimaryPillarNew_c: "Tech_Cloud_Platform", SecondaryPillarNew_c: "Tech_Cloud_Infrastructure", ServiceNew_Id_c: 300010753841648 });
for (const family of ["chrome", "firefox"]) {
test(`${family}: internal SR reads parent SRInfo, resolves user and posts form values with Session and observed client ID`, async () => {
const calls = [];
const h = loadBackground({ family, fetch: async (url, options) => {
calls.push({ url, options });
if (options.method === "GET" && url.includes("/serviceRequests?")) return withClientId({ items: [parentInfo()] }, 200, "internal-client");
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 });
assert.ok(!url.includes("Catalogue_c"), "Internal SR must not use catalog signing");
return response({ SrNumber: "ISR1" }, 201);
} });
const input = { ...internalInput(), AccountPartyId: 1, OpportunityName_c: "STALE", OpportunityName_Id_c: 2, RegionText_c: "APAC",
SvcHealth_c: "SVC_HEALTH_GREEN", Language_c: "ENGLISH", CountryText_c: "US", PrimaryPillarNew_c: "STALE", SecondaryPillarNew_c: null, ReportedByPartyId: 999 };
const result = await h.dispatch({ type: internalType, emailAddress, payload: input });
assert.equal(result.ok, true);
assert.equal(calls.length, 3);
const info = new URL(calls[0].url);
assert.match(info.pathname, /SeaaS_SR_management.*\/oalsetCRMRestAPI\/.*resources\/latest\/serviceRequests$/);
assert.equal(info.searchParams.get("q"), "SrId=505");
assert.equal(info.searchParams.get("onlyData"), "true");
assert.equal(info.searchParams.get("orderBy"), "LastUpdateDate:desc");
assert.equal(info.searchParams.get("fields"), "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
const post = calls[2];
assert.equal(new URL(calls[1].url).searchParams.get("emailAddress"), emailAddress);
assert.match(post.url, /SeaaS_SR_management.*oalsetCRMRestAPICloudAccount.*resources\/latest\/serviceRequests$/);
assert.equal(post.options.method, "POST");
const { SrId, SrNumber, ...parent } = parentInfo();
assert.deepEqual(JSON.parse(post.options.body), { ...parent, ...internalInput(),
ServiceRequest_Id_SRToInternalSR: "505", ServiceNew_Id_c: 404,
InternalDeadline_c: "2027-01-31T00:00:00.000Z", ReportedByPartyId: 303, SvcQuestionsCollection_c: [] });
assert.ok(calls.every(call => call.options.headers.Authorization === "Session" && call.options.credentials === "include"));
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
assert.deepEqual({ ...post.options.headers }, { Accept: "application/json", "Content-Type": "application/json",
Authorization: "Session", Origin: origin, "x-appbuilder-client-id": "internal-client" });
assertDebugVisibility(result, "internal-client");
assert.equal(calls[0].options.headers.Origin, undefined);
assert.equal(result.debugRequests[2].requestHeaders.Origin, origin);
assert.equal(h.headerRules.length, 1);
const { removeRuleIds, addRules } = h.headerRules[0];
assert.deepEqual(removeRuleIds, [7203]);
assert.equal(addRules.length, 1);
const rule = addRules[0];
assert.deepEqual(rule.action, { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: origin }] });
assert.deepEqual(rule.condition.initiatorDomains, ["synthetic-extension"]);
assert.deepEqual(rule.condition.requestMethods, ["post"]);
assert.deepEqual(rule.condition.resourceTypes, ["xmlhttprequest"]);
const pattern = new RegExp(rule.condition.regexFilter);
assert.ok(pattern.test(post.url));
assert.ok(!pattern.test(calls[0].url));
assert.ok(!pattern.test(post.url + "/other"));
assert.ok(!pattern.test(post.url + "?other=1"));
assert.deepEqual(result.debugRequests.map(entry => entry.name), ["requestSRInfo", "requestResourceUser", "requestPostInternalServiceRequest"]);
});
}
test("internal SR queries the current parent on every Save and reuses captured client ID after worker restart", async () => {
const sessionStorage = {};
const queries = [], posts = [];
const fetch = async (url, options) => {
if (url.includes("/serviceRequests?")) {
const id = Number(new URL(url).searchParams.get("q").slice("SrId=".length));
queries.push(id);
const parent = { ...parentInfo(), SrId: id, SecondaryPillarNew_c: null, ServiceNew_Id_c: id + 1000 };
if (id === 606) delete parent.ServiceNew_Id_c;
return id === 505 ? withClientId({ items: [parent] }, 200, "saved-internal-client") : response({ items: [parent] });
}
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 });
posts.push(options);
return response({ SrNumber: "ISR1" }, 201);
};
const h = loadBackground({ fetch, sessionStorage });
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: internalInput() })).ok, true);
const restarted = loadBackground({ fetch, sessionStorage });
assert.equal((await restarted.dispatch({ type: internalType, emailAddress,
payload: { ...internalInput(), ServiceRequest_Id_SRToInternalSR: "606" } })).ok, true);
assert.deepEqual(queries, [505, 606]);
assert.equal(posts[1].headers["x-appbuilder-client-id"], "saved-internal-client");
assert.equal(JSON.parse(posts[1].body).ServiceRequest_Id_SRToInternalSR, "606", "links to parent SrId, independently of the parent's service ID");
assert.equal(JSON.parse(posts[1].body).SecondaryPillarNew_c, null);
});
test("internal SR save validates form fields and parent ID before requesting SRInfo", async () => {
let calls = 0;
const h = loadBackground({ fetch: async () => { calls++; return response({}, 403); } });
for (const change of [{ Title: "" }, { ProblemDescription: "" }, { TechPlatformLookUp_c: "" }, { InternalDeadline_c: "2026-02-30" },
{ CustomerDeadline_c: "2026-02-30" }, { ServiceNew_Id_c: "bad" }, { ServiceRequest_Id_SRToInternalSR: "bad" }]) {
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: { ...internalInput(), ...change } })).ok, false);
}
assert.equal(calls, 0);
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: internalInput() })).ok, false);
assert.equal(calls, 1);
});
test("internal SR stops after missing or rejected parent data, failed user lookup or POST and never retries creation", async () => {
for (const mode of ["info-401", "info-500", "empty", "multiple", "wrong-parent", "missing-country", "user", "post-403", "post-500", "network"]) {
const calls = [];
const h = loadBackground({ fetch: async (url, options) => {
calls.push({ url, options });
if (url.includes("/serviceRequests?")) {
const parent = parentInfo();
if (mode === "wrong-parent") parent.SrId = 999;
if (mode === "missing-country") delete parent.CountryText_c;
return response({ items: mode === "empty" ? [] : mode === "multiple" ? [parent, parent] : [parent] }, mode === "info-401" ? 401 : mode === "info-500" ? 500 : 200);
}
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 }, mode === "user" ? 403 : 200);
if (mode === "network") throw new Error("Synthetic network failure");
return response({}, mode === "post-403" ? 403 : 500);
} });
const result = await h.dispatch({ type: internalType, emailAddress, payload: internalInput() });
assert.equal(result.ok, false, mode);
const hasPost = ["post-403", "post-500", "network"].includes(mode);
assert.equal(calls.length, hasPost ? 3 : mode === "user" ? 2 : 1, mode);
assert.equal(calls.filter(call => call.options.method === "POST").length, hasPost ? 1 : 0);
assert.equal(result.debugRequests.at(-1).status, ["empty", "multiple", "wrong-parent", "missing-country"].includes(mode) ? "success" : "failed");
}
});
const payload = () => ({ AccountPartyId: 100000000000101, OpportunityName_Id_c: 202,
ServiceNew_Id_c: 404, Title: "Service-OP1-Customer", RegionText_c: "LAD", CountryText_c: "BR",
ProblemDescription: "Test description", Language_c: "ENGLISH", CustomerDeadline_c: "2027-01-31",
@@ -80,7 +213,7 @@ test("token 403 retries once only with a newly returned client ID; SR POST is ne
if (["recovered", "save-failure"].includes(mode)) {
assert.deepEqual({ ...calls.at(-1).options.headers }, { ...tokenCalls.at(-1).options.headers, "vb-proxy-header-x-oracle-ectoken": "retry-token" });
}
assert.ok(!JSON.stringify(result).includes("retry-token"));
assertDebugVisibility(result, "retry-token");
}
});
@@ -137,7 +270,7 @@ for (const family of ["chrome", "firefox"]) {
assert.deepEqual(JSON.parse(calls[3].options.body), { ...payload(), ReportedByPartyId: 303, SvcQuestionsCollection_c: [],
SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" });
assert.equal(calls[3].options.body, JSON.stringify(prepared.payload));
assert.ok(!JSON.stringify(result).includes("synthetic-secret-token"));
assertDebugVisibility(result, "synthetic-secret-token");
assert.equal(h.tabs.length, 0);
});
}
@@ -209,6 +342,20 @@ test("Origin rule failure blocks token acquisition and SR creation", async () =>
assert.match(result.error, /Rule permission denied/);
});
test("Origin rule failure prevents the Internal SR POST", async () => {
const calls = [];
const h = loadBackground({ ruleError: true, fetch: async url => {
calls.push(url);
if (url.includes("/serviceRequests?")) return response({ items: [parentInfo()] });
assert.match(url, /\/resourceUsers\?/);
return response({ resourcePartyId: 303 });
} });
const result = await h.dispatch({ type: internalType, emailAddress, payload: internalInput() });
assert.equal(result.ok, false);
assert.match(result.error, /Rule permission denied/);
assert.equal(calls.length, 2);
});
test("resource lookup failures or invalid resourcePartyId never obtain a token or create SR", async () => {
for (const body of [{ resourcePartyId: null }, { resourceProfileId: 999 }, { resourcePartyId: "9007199254740993" }, { resourcePartyId: 0 }, "unauthorized"]) {
let calls = 0;
@@ -238,7 +385,7 @@ test("token rejection never creates SR; creation failures are never automaticall
const result = await h.dispatch({ type, phase: "confirm", catalogId: 505, payload: prepared.payload });
assert.equal(result.ok, false);
assert.equal(calls, failure === "token" || failure === "empty-token" ? 2 : 3);
assert.ok(!JSON.stringify(result).includes("secret-token"));
assertDebugVisibility(result, "secret-token");
assert.equal(h.tabs.length, 0);
}
});

View File

@@ -125,9 +125,9 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
h.app.debugContentView.renderDebugPanel = () => {};
await h.app.opportunityCrmServiceRequests.load("test", 1);
h.app.currentUser.getUserId = () => "current@example.invalid";
h.app.opportunitiesTableState.opportunitiesTableState.items = [{ OptyNumber: "B", CustomerAccount: { PartyId: 999, Region_c: "EMEA" } }, {
OptyNumber: "A", OptyId: 202, PartyId: 888, AccountPartyId: 777, Owner: { PartyId: 666 }, Region_c: "EMEA", RegionText_c: "LAD", Name: "Opportunity",
CustomerAccount: { PartyId: 100000000000101, PartyUniqueName: "Customer", Region_c: "APAC", PrimaryAddress: { CountryCode: "BR" } }
h.app.opportunitiesTableState.opportunitiesTableState.items = [{ OptyNumber: "B", CustomerAccount: { PartyId: 999, Region_c: "EMEA", PrimaryAddress: { CountryCode: "US" } } }, {
OptyNumber: "A", OptyId: 202, PartyId: 888, AccountPartyId: 777, Owner: { PartyId: 666 }, Region_c: "EMEA", RegionText_c: "LAD", Name: "Opportunity", CountryCode: "CA",
CustomerAccount: { PartyId: 100000000000101, PartyUniqueName: "Customer", Region_c: "APAC", CountryCode: "AR", PrimaryAddress: { CountryCode: "BR" } }
}];
const posts = [];
const notices = [];
@@ -167,7 +167,7 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
assert.equal(payload.PrimaryPillarNew_c, "CODE");
assert.equal(payload.Language_c, "CODE");
assert.equal(payload.TechPlatformLookUp_c, "CODE");
assert.equal(payload.CountryText_c, "BR");
assert.equal(payload.CountryText_c, "BR", "uses the selected opportunity's CustomerAccount.PrimaryAddress.CountryCode, not CustomerAccount, root or another opportunity");
assert.equal(payload.RegionText_c, "APAC", "uses CustomerAccount.Region_c from the selected opportunity, not a root-level region or another opportunity");
assert.equal(payload.ProblemDescription, "Dynamic description");
assert.equal(payload.CustomerDeadline_c, "2027-01-31");
@@ -230,7 +230,9 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
posts[3].resolve({ ok: false, error: "Synthetic failure" });
await failed;
assert.equal(notices.at(-1)[1], true);
delete h.app.opportunitiesTableState.opportunitiesTableState.items[1].CustomerAccount.CountryCode;
const retry = submit();
assert.equal(posts[4].message.payload.CountryText_c, "BR", "uses PrimaryAddress.CountryCode even without CustomerAccount.CountryCode");
posts[4].resolve({ ok: true, payload: preparedPayload });
await flush();
confirm().click();