Release version 1.0.1
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Opportunities Extension 1.0.0
|
# Opportunities Extension 1.0.1
|
||||||
|
|
||||||
Browser extension that adds the "Opportunities Extension" shortcut to the permitted Oracle Fusion pages, retrieves opportunities, and displays them in a filterable table.
|
Browser extension that adds the "Opportunities Extension" shortcut to the permitted Oracle Fusion pages, retrieves opportunities, and displays them in a filterable table.
|
||||||
|
|
||||||
@@ -29,6 +29,12 @@ The build recreates `dist/chromium` and `dist/firefox` from the same source.
|
|||||||
- Light and dark themes based on Oracle Redwood.
|
- Light and dark themes based on Oracle Redwood.
|
||||||
- Separate outputs for Chromium/Edge and Firefox.
|
- Separate outputs for Chromium/Edge and Firefox.
|
||||||
|
|
||||||
|
## Release 1.0.1
|
||||||
|
|
||||||
|
- Retries the consumer token service when it returns HTTP 200 with an HTML fallback.
|
||||||
|
- Opens the SPA session page in an inactive temporary tab and closes it as soon as valid JSON is received.
|
||||||
|
- Limits the recovery flow to five attempts and records the session bootstrap details in debug mode.
|
||||||
|
|
||||||
## Loading in Development Mode
|
## Loading in Development Mode
|
||||||
|
|
||||||
### Chrome ou Microsoft Edge
|
### Chrome ou Microsoft Edge
|
||||||
|
|||||||
152
dist/chromium/background.js
vendored
152
dist/chromium/background.js
vendored
@@ -18,6 +18,10 @@
|
|||||||
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
||||||
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
||||||
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
||||||
|
const CUSTOMER_TOKEN_SESSION_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html";
|
||||||
|
const CUSTOMER_TOKEN_MAX_ATTEMPTS = 5;
|
||||||
|
const CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS = 750;
|
||||||
|
const CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS = 4000;
|
||||||
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
||||||
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
||||||
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
|
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
|
||||||
@@ -129,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
||||||
requestConsumerTokenService()
|
requestConsumerTokenService(sender)
|
||||||
.then(sendResponse)
|
.then(sendResponse)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
sendResponse({
|
sendResponse({
|
||||||
@@ -173,20 +177,11 @@
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function requestConsumerTokenService() {
|
async function requestConsumerTokenService(sender) {
|
||||||
const debugRequests = [];
|
const debugRequests = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests);
|
||||||
method: "GET",
|
|
||||||
credentials: "include",
|
|
||||||
cache: "no-store",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
Pragma: "no-cache"
|
|
||||||
}
|
|
||||||
}, "requestConsumerTokenService", debugRequests);
|
|
||||||
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
"ACCP-TS",
|
"ACCP-TS",
|
||||||
@@ -239,6 +234,126 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchConsumerTokenServiceUntilJson(sender, debugRequests) {
|
||||||
|
let attempt = 0;
|
||||||
|
let sessionTab = null;
|
||||||
|
let receivedValidJson = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (attempt < CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||||
|
attempt += 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Cache-Control": "no-cache, no-store, max-age=0",
|
||||||
|
Pragma: "no-cache"
|
||||||
|
}
|
||||||
|
}, "requestConsumerTokenService", debugRequests);
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedValidJson = true;
|
||||||
|
return tokenResponse;
|
||||||
|
} catch (error) {
|
||||||
|
const isHtmlFallback = error
|
||||||
|
&& error.code === "INVALID_JSON_RESPONSE"
|
||||||
|
&& error.httpStatus === 200;
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (!isHtmlFallback || attempt >= CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
if (isHtmlFallback) {
|
||||||
|
debugEntry.metadata.retryLimitReached = "true";
|
||||||
|
debugEntry.error = `requestConsumerTokenService returned invalid JSON after ${CUSTOMER_TOKEN_MAX_ATTEMPTS} attempts.`;
|
||||||
|
error.message = debugEntry.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionTab) {
|
||||||
|
try {
|
||||||
|
sessionTab = await openConsumerTokenSessionTab(sender);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||||
|
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
|
||||||
|
}
|
||||||
|
} catch (sessionError) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||||
|
debugEntry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = "waiting-for-valid-session";
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryDelayMs = getConsumerTokenRetryDelay(attempt);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
debugEntry.metadata.retryReason = "HTTP 200 response did not contain valid JSON";
|
||||||
|
debugEntry.metadata.nextRetryDelayMs = String(retryDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(retryDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("requestConsumerTokenService retry loop ended unexpectedly.");
|
||||||
|
} finally {
|
||||||
|
if (sessionTab && Number.isInteger(sessionTab.id)) {
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tabsRemove(sessionTab.id);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = receivedValidJson
|
||||||
|
? "closed-after-valid-json"
|
||||||
|
: "closed-after-retry-finished";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConsumerTokenRetryDelay(attempt) {
|
||||||
|
return Math.min(
|
||||||
|
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS * (2 ** Math.max(attempt - 1, 0)),
|
||||||
|
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConsumerTokenSessionTab(sender) {
|
||||||
|
const details = {
|
||||||
|
url: CUSTOMER_TOKEN_SESSION_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
details.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
details.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabsCreate(details);
|
||||||
|
}
|
||||||
|
|
||||||
async function requestAccountPlans(message) {
|
async function requestAccountPlans(message) {
|
||||||
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
||||||
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
||||||
@@ -416,7 +531,14 @@
|
|||||||
throw new Error(`${requestName} response body could not be read.`);
|
throw new Error(`${requestName} response body could not be read.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseJsonResponse(responsePayload.text, requestName);
|
try {
|
||||||
|
return parseJsonResponse(responsePayload.text, requestName);
|
||||||
|
} catch (error) {
|
||||||
|
error.httpStatus = response.status;
|
||||||
|
debugEntry.status = "failed";
|
||||||
|
debugEntry.error = error.message;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
||||||
@@ -491,7 +613,9 @@
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(normalizedText);
|
return JSON.parse(normalizedText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`${requestName} returned an invalid JSON response.`);
|
const invalidJsonError = new Error(`${requestName} returned an invalid JSON response.`);
|
||||||
|
invalidJsonError.code = "INVALID_JSON_RESPONSE";
|
||||||
|
throw invalidJsonError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
dist/chromium/manifest.json
vendored
2
dist/chromium/manifest.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Opportunities Extension",
|
"name": "Opportunities Extension",
|
||||||
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cookies",
|
"cookies",
|
||||||
"storage"
|
"storage"
|
||||||
|
|||||||
152
dist/firefox/background.js
vendored
152
dist/firefox/background.js
vendored
@@ -18,6 +18,10 @@
|
|||||||
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
||||||
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
||||||
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
||||||
|
const CUSTOMER_TOKEN_SESSION_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html";
|
||||||
|
const CUSTOMER_TOKEN_MAX_ATTEMPTS = 5;
|
||||||
|
const CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS = 750;
|
||||||
|
const CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS = 4000;
|
||||||
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
||||||
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
||||||
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
|
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
|
||||||
@@ -129,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
||||||
requestConsumerTokenService()
|
requestConsumerTokenService(sender)
|
||||||
.then(sendResponse)
|
.then(sendResponse)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
sendResponse({
|
sendResponse({
|
||||||
@@ -173,20 +177,11 @@
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function requestConsumerTokenService() {
|
async function requestConsumerTokenService(sender) {
|
||||||
const debugRequests = [];
|
const debugRequests = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests);
|
||||||
method: "GET",
|
|
||||||
credentials: "include",
|
|
||||||
cache: "no-store",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
Pragma: "no-cache"
|
|
||||||
}
|
|
||||||
}, "requestConsumerTokenService", debugRequests);
|
|
||||||
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
"ACCP-TS",
|
"ACCP-TS",
|
||||||
@@ -239,6 +234,126 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchConsumerTokenServiceUntilJson(sender, debugRequests) {
|
||||||
|
let attempt = 0;
|
||||||
|
let sessionTab = null;
|
||||||
|
let receivedValidJson = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (attempt < CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||||
|
attempt += 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Cache-Control": "no-cache, no-store, max-age=0",
|
||||||
|
Pragma: "no-cache"
|
||||||
|
}
|
||||||
|
}, "requestConsumerTokenService", debugRequests);
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedValidJson = true;
|
||||||
|
return tokenResponse;
|
||||||
|
} catch (error) {
|
||||||
|
const isHtmlFallback = error
|
||||||
|
&& error.code === "INVALID_JSON_RESPONSE"
|
||||||
|
&& error.httpStatus === 200;
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (!isHtmlFallback || attempt >= CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
if (isHtmlFallback) {
|
||||||
|
debugEntry.metadata.retryLimitReached = "true";
|
||||||
|
debugEntry.error = `requestConsumerTokenService returned invalid JSON after ${CUSTOMER_TOKEN_MAX_ATTEMPTS} attempts.`;
|
||||||
|
error.message = debugEntry.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionTab) {
|
||||||
|
try {
|
||||||
|
sessionTab = await openConsumerTokenSessionTab(sender);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||||
|
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
|
||||||
|
}
|
||||||
|
} catch (sessionError) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||||
|
debugEntry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = "waiting-for-valid-session";
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryDelayMs = getConsumerTokenRetryDelay(attempt);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
debugEntry.metadata.retryReason = "HTTP 200 response did not contain valid JSON";
|
||||||
|
debugEntry.metadata.nextRetryDelayMs = String(retryDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(retryDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("requestConsumerTokenService retry loop ended unexpectedly.");
|
||||||
|
} finally {
|
||||||
|
if (sessionTab && Number.isInteger(sessionTab.id)) {
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tabsRemove(sessionTab.id);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = receivedValidJson
|
||||||
|
? "closed-after-valid-json"
|
||||||
|
: "closed-after-retry-finished";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConsumerTokenRetryDelay(attempt) {
|
||||||
|
return Math.min(
|
||||||
|
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS * (2 ** Math.max(attempt - 1, 0)),
|
||||||
|
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConsumerTokenSessionTab(sender) {
|
||||||
|
const details = {
|
||||||
|
url: CUSTOMER_TOKEN_SESSION_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
details.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
details.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabsCreate(details);
|
||||||
|
}
|
||||||
|
|
||||||
async function requestAccountPlans(message) {
|
async function requestAccountPlans(message) {
|
||||||
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
||||||
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
||||||
@@ -416,7 +531,14 @@
|
|||||||
throw new Error(`${requestName} response body could not be read.`);
|
throw new Error(`${requestName} response body could not be read.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseJsonResponse(responsePayload.text, requestName);
|
try {
|
||||||
|
return parseJsonResponse(responsePayload.text, requestName);
|
||||||
|
} catch (error) {
|
||||||
|
error.httpStatus = response.status;
|
||||||
|
debugEntry.status = "failed";
|
||||||
|
debugEntry.error = error.message;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
||||||
@@ -491,7 +613,9 @@
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(normalizedText);
|
return JSON.parse(normalizedText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`${requestName} returned an invalid JSON response.`);
|
const invalidJsonError = new Error(`${requestName} returned an invalid JSON response.`);
|
||||||
|
invalidJsonError.code = "INVALID_JSON_RESPONSE";
|
||||||
|
throw invalidJsonError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
dist/firefox/manifest.json
vendored
2
dist/firefox/manifest.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Opportunities Extension",
|
"name": "Opportunities Extension",
|
||||||
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cookies",
|
"cookies",
|
||||||
"storage"
|
"storage"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "opportunities-extension",
|
"name": "opportunities-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
"check": "node --check src/content.js && node --check src/background.js && node tests/consumptionTrend.test.cjs && node scripts/build.mjs --check",
|
"check": "node --check src/content.js && node --check src/background.js && node tests/consumptionTrend.test.cjs && node tests/consumerTokenSessionRetry.test.cjs && node scripts/build.mjs --check",
|
||||||
"test": "node tests/consumptionTrend.test.cjs"
|
"test": "node tests/consumptionTrend.test.cjs && node tests/consumerTokenSessionRetry.test.cjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const baseManifest = {
|
|||||||
manifest_version: 3,
|
manifest_version: 3,
|
||||||
name: "Opportunities Extension",
|
name: "Opportunities Extension",
|
||||||
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||||
version: "1.0.0",
|
version: "1.0.1",
|
||||||
permissions: [
|
permissions: [
|
||||||
"cookies",
|
"cookies",
|
||||||
"storage"
|
"storage"
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
||||||
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
||||||
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
||||||
|
const CUSTOMER_TOKEN_SESSION_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html";
|
||||||
|
const CUSTOMER_TOKEN_MAX_ATTEMPTS = 5;
|
||||||
|
const CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS = 750;
|
||||||
|
const CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS = 4000;
|
||||||
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
||||||
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
||||||
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
|
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
|
||||||
@@ -129,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
||||||
requestConsumerTokenService()
|
requestConsumerTokenService(sender)
|
||||||
.then(sendResponse)
|
.then(sendResponse)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
sendResponse({
|
sendResponse({
|
||||||
@@ -173,20 +177,11 @@
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function requestConsumerTokenService() {
|
async function requestConsumerTokenService(sender) {
|
||||||
const debugRequests = [];
|
const debugRequests = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests);
|
||||||
method: "GET",
|
|
||||||
credentials: "include",
|
|
||||||
cache: "no-store",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
Pragma: "no-cache"
|
|
||||||
}
|
|
||||||
}, "requestConsumerTokenService", debugRequests);
|
|
||||||
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
"ACCP-TS",
|
"ACCP-TS",
|
||||||
@@ -239,6 +234,126 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchConsumerTokenServiceUntilJson(sender, debugRequests) {
|
||||||
|
let attempt = 0;
|
||||||
|
let sessionTab = null;
|
||||||
|
let receivedValidJson = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (attempt < CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||||
|
attempt += 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Cache-Control": "no-cache, no-store, max-age=0",
|
||||||
|
Pragma: "no-cache"
|
||||||
|
}
|
||||||
|
}, "requestConsumerTokenService", debugRequests);
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedValidJson = true;
|
||||||
|
return tokenResponse;
|
||||||
|
} catch (error) {
|
||||||
|
const isHtmlFallback = error
|
||||||
|
&& error.code === "INVALID_JSON_RESPONSE"
|
||||||
|
&& error.httpStatus === 200;
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (!isHtmlFallback || attempt >= CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
if (isHtmlFallback) {
|
||||||
|
debugEntry.metadata.retryLimitReached = "true";
|
||||||
|
debugEntry.error = `requestConsumerTokenService returned invalid JSON after ${CUSTOMER_TOKEN_MAX_ATTEMPTS} attempts.`;
|
||||||
|
error.message = debugEntry.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionTab) {
|
||||||
|
try {
|
||||||
|
sessionTab = await openConsumerTokenSessionTab(sender);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||||
|
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
|
||||||
|
}
|
||||||
|
} catch (sessionError) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||||
|
debugEntry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = "waiting-for-valid-session";
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryDelayMs = getConsumerTokenRetryDelay(attempt);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.attempt = String(attempt);
|
||||||
|
debugEntry.metadata.retryReason = "HTTP 200 response did not contain valid JSON";
|
||||||
|
debugEntry.metadata.nextRetryDelayMs = String(retryDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(retryDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("requestConsumerTokenService retry loop ended unexpectedly.");
|
||||||
|
} finally {
|
||||||
|
if (sessionTab && Number.isInteger(sessionTab.id)) {
|
||||||
|
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tabsRemove(sessionTab.id);
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = receivedValidJson
|
||||||
|
? "closed-after-valid-json"
|
||||||
|
: "closed-after-retry-finished";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (debugEntry) {
|
||||||
|
debugEntry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConsumerTokenRetryDelay(attempt) {
|
||||||
|
return Math.min(
|
||||||
|
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS * (2 ** Math.max(attempt - 1, 0)),
|
||||||
|
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConsumerTokenSessionTab(sender) {
|
||||||
|
const details = {
|
||||||
|
url: CUSTOMER_TOKEN_SESSION_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
details.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
details.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabsCreate(details);
|
||||||
|
}
|
||||||
|
|
||||||
async function requestAccountPlans(message) {
|
async function requestAccountPlans(message) {
|
||||||
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
||||||
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
||||||
@@ -416,7 +531,14 @@
|
|||||||
throw new Error(`${requestName} response body could not be read.`);
|
throw new Error(`${requestName} response body could not be read.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseJsonResponse(responsePayload.text, requestName);
|
try {
|
||||||
|
return parseJsonResponse(responsePayload.text, requestName);
|
||||||
|
} catch (error) {
|
||||||
|
error.httpStatus = response.status;
|
||||||
|
debugEntry.status = "failed";
|
||||||
|
debugEntry.error = error.message;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
||||||
@@ -491,7 +613,9 @@
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(normalizedText);
|
return JSON.parse(normalizedText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`${requestName} returned an invalid JSON response.`);
|
const invalidJsonError = new Error(`${requestName} returned an invalid JSON response.`);
|
||||||
|
invalidJsonError.code = "INVALID_JSON_RESPONSE";
|
||||||
|
throw invalidJsonError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
tests/consumerTokenSessionRetry.test.cjs
Normal file
122
tests/consumerTokenSessionRetry.test.cjs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const vm = require("node:vm");
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "..");
|
||||||
|
|
||||||
|
function createResponse(body, contentType) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
get(name) {
|
||||||
|
return name.toLowerCase() === "content-type" ? contentType : null;
|
||||||
|
},
|
||||||
|
forEach(callback) {
|
||||||
|
callback(contentType, "content-type");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: async () => body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBackground(responses, createdTabs, removedTabs) {
|
||||||
|
let listener;
|
||||||
|
const source = fs.readFileSync(path.join(root, "src", "background.js"), "utf8");
|
||||||
|
const context = {
|
||||||
|
chrome: {
|
||||||
|
runtime: {
|
||||||
|
onMessage: {
|
||||||
|
addListener(callback) {
|
||||||
|
listener = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
async create(details) {
|
||||||
|
createdTabs.push(details);
|
||||||
|
return { id: 27 };
|
||||||
|
},
|
||||||
|
async remove(tabId) {
|
||||||
|
removedTabs.push(tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetch: async () => {
|
||||||
|
const response = responses.shift();
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("Unexpected fetch call.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
Date,
|
||||||
|
Error,
|
||||||
|
JSON,
|
||||||
|
Map,
|
||||||
|
Set,
|
||||||
|
Promise,
|
||||||
|
String,
|
||||||
|
Array,
|
||||||
|
Object,
|
||||||
|
Number,
|
||||||
|
Math,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.runInNewContext(source, context);
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatch(listener) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
assert.equal(listener({ type: "opportunitiesExtension.requestConsumerTokenService" }, {
|
||||||
|
tab: { windowId: 1 }
|
||||||
|
}, resolve), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const createdTabs = [];
|
||||||
|
const removedTabs = [];
|
||||||
|
const listener = loadBackground([
|
||||||
|
createResponse("<html><body>Fallback</body></html>", "text/html"),
|
||||||
|
createResponse(JSON.stringify([
|
||||||
|
{
|
||||||
|
tokenService: "ACCP-TS",
|
||||||
|
uiaasHeader: { label: "Account-Ti-Authorization", value: "account-token" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tokenService: "OCICONS-TS",
|
||||||
|
uiaasHeader: { label: "G2m-Authorization", value: "consumption-token" }
|
||||||
|
}
|
||||||
|
]), "application/json")
|
||||||
|
], createdTabs, removedTabs);
|
||||||
|
const response = await dispatch(listener);
|
||||||
|
|
||||||
|
assert.equal(response.ok, true);
|
||||||
|
assert.equal(response.authorizations.accountSummary.value, "account-token");
|
||||||
|
assert.equal(response.authorizations.accountPlans.value, "consumption-token");
|
||||||
|
assert.equal(createdTabs.length, 1);
|
||||||
|
assert.equal(createdTabs[0].active, false);
|
||||||
|
assert.equal(
|
||||||
|
createdTabs[0].url,
|
||||||
|
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html"
|
||||||
|
);
|
||||||
|
assert.deepEqual(removedTabs, [27]);
|
||||||
|
assert.equal(response.debugRequests.length, 2);
|
||||||
|
assert.equal(response.debugRequests[0].status, "failed");
|
||||||
|
assert.equal(response.debugRequests[0].metadata.sessionBootstrapState, "opened-inactive-tab");
|
||||||
|
assert.equal(response.debugRequests[1].metadata.sessionBootstrapState, "closed-after-valid-json");
|
||||||
|
|
||||||
|
console.log("Consumer token session retry tests passed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user