Adjustment to API authentication with fallback in case the cookie is invalid or missing
This commit is contained in:
186
dist/chromium/background.js
vendored
186
dist/chromium/background.js
vendored
@@ -10,37 +10,181 @@
|
|||||||
];
|
];
|
||||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||||
|
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||||
|
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||||
|
const COOKIE_REFRESH_POLL_MS = 250;
|
||||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXsrfTokenCookie()
|
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||||
.then((result) => {
|
getXsrfTokenCookie()
|
||||||
sendResponse({
|
.then((result) => {
|
||||||
ok: true,
|
sendResponse({
|
||||||
cookieName: result.cookie ? result.cookie.name : "",
|
ok: true,
|
||||||
token: result.cookie ? result.cookie.value : "",
|
cookieName: result.cookie ? result.cookie.name : "",
|
||||||
matchedCookieNames: result.matchedCookieNames,
|
token: result.cookie ? result.cookie.value : "",
|
||||||
lookupDetails: result.lookupDetails
|
matchedCookieNames: result.matchedCookieNames,
|
||||||
|
lookupDetails: result.lookupDetails
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
cookieName: "",
|
||||||
|
token: "",
|
||||||
|
matchedCookieNames: [],
|
||||||
|
lookupDetails: [],
|
||||||
|
error: error.message || "Unable to read cookies."
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
sendResponse({
|
|
||||||
ok: false,
|
|
||||||
cookieName: "",
|
|
||||||
token: "",
|
|
||||||
matchedCookieNames: [],
|
|
||||||
lookupDetails: [],
|
|
||||||
error: error.message || "Unable to read cookies."
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||||
|
refreshXsrfCookie(sender)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: error.message || "Unable to refresh XSRF cookie."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refreshXsrfCookie(sender) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const previous = (await getXsrfTokenCookie()).cookie;
|
||||||
|
const createProperties = {
|
||||||
|
url: OPPORTUNITIES_LIST_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
createProperties.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
createProperties.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshTab;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshTab = await tabsCreate(createProperties);
|
||||||
|
const current = await waitForXsrfCookieChange(previous);
|
||||||
|
const refreshed = hasCookieChanged(previous, current);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: Boolean(current),
|
||||||
|
refreshed,
|
||||||
|
cookieName: current ? current.name : "",
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
error: refreshed ? "" : "XSRF cookie was not created or changed before timeout."
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (refreshTab && Number.isInteger(refreshTab.id)) {
|
||||||
|
try {
|
||||||
|
await tabsRemove(refreshTab.id);
|
||||||
|
} catch (error) {
|
||||||
|
// The temporary tab may already have been closed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForXsrfCookieChange(previousCookie) {
|
||||||
|
const deadline = Date.now() + COOKIE_REFRESH_TIMEOUT_MS;
|
||||||
|
let currentCookie = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
currentCookie = (await getXsrfTokenCookie()).cookie;
|
||||||
|
|
||||||
|
if (hasCookieChanged(previousCookie, currentCookie)) {
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(COOKIE_REFRESH_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCookieChanged(previousCookie, currentCookie) {
|
||||||
|
if (!currentCookie || !currentCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousCookie) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousCookie.name !== currentCookie.name
|
||||||
|
|| previousCookie.value !== currentCookie.value
|
||||||
|
|| previousCookie.expirationDate !== currentCookie.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsFirefoxCookieStoreTabs() {
|
||||||
|
return Boolean(
|
||||||
|
runtimeApi.runtime
|
||||||
|
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsCreate(details) {
|
||||||
|
if (runtimeApi.tabs.create.length <= 1) {
|
||||||
|
return runtimeApi.tabs.create(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.create(details, (tab) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsRemove(tabId) {
|
||||||
|
if (runtimeApi.tabs.remove.length <= 1) {
|
||||||
|
return runtimeApi.tabs.remove(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.remove(tabId, () => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(durationMs) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
async function getXsrfTokenCookie() {
|
async function getXsrfTokenCookie() {
|
||||||
const lookupDetails = [];
|
const lookupDetails = [];
|
||||||
const allCookies = [];
|
const allCookies = [];
|
||||||
|
|||||||
199
dist/chromium/content.js
vendored
199
dist/chromium/content.js
vendored
@@ -502,8 +502,98 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestTokenRelay() {
|
async function requestTokenRelay() {
|
||||||
const xsrfTokenSource = await getXsrfToken();
|
let refreshResult = null;
|
||||||
const requestEntry = logRequestStart({
|
let refreshAttempted = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const xsrfTokenSource = await getXsrfToken();
|
||||||
|
|
||||||
|
if (!xsrfTokenSource.token) {
|
||||||
|
if (refreshAttempted) {
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh.");
|
||||||
|
throw new Error("XSRF token cookie not found after automatic refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshResult = await refreshXsrfCookie("missing-cookie");
|
||||||
|
refreshAttempted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(TOKEN_RELAY_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"x-xsrf-token": xsrfTokenSource.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await readResponseBodyForDebug(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted && isInvalidXsrfStatus(response.status)) {
|
||||||
|
refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`);
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
|
||||||
|
return responseData.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||||
|
return logRequestStart({
|
||||||
name: "requestTokenRelay",
|
name: "requestTokenRelay",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: TOKEN_RELAY_URL,
|
url: TOKEN_RELAY_URL,
|
||||||
@@ -519,56 +609,25 @@
|
|||||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||||
: "(none)",
|
: "(none)",
|
||||||
xsrfTokenError: xsrfTokenSource.error || ""
|
xsrfTokenError: xsrfTokenSource.error || "",
|
||||||
|
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!xsrfTokenSource.token) {
|
function isInvalidXsrfStatus(status) {
|
||||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
return status === 400 || status === 401 || status === 403;
|
||||||
throw new Error("XSRF token cookie not found.");
|
}
|
||||||
|
|
||||||
|
function formatXsrfRefreshResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
return "not-requested";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const outcome = result.refreshed ? "refreshed" : "not-refreshed";
|
||||||
const response = await fetch(TOKEN_RELAY_URL, {
|
const cookieName = result.cookieName || "(none)";
|
||||||
method: "GET",
|
const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
|
||||||
credentials: "include",
|
return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
|
||||||
headers: {
|
|
||||||
"x-xsrf-token": xsrfTokenSource.token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseBody = await readResponseBodyForDebug(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseData = await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
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)));
|
|
||||||
|
|
||||||
return responseData.access_token;
|
|
||||||
} catch (error) {
|
|
||||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
@@ -2106,6 +2165,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshXsrfCookie(reason) {
|
||||||
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
|
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: "Runtime messaging API unavailable."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
runtimeApi.runtime.sendMessage({
|
||||||
|
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||||
|
}, (response) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: lastError.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
...(response || {
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: "Empty refresh response."
|
||||||
|
}),
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthBadge(status) {
|
function updateAuthBadge(status) {
|
||||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||||
|
|
||||||
|
|||||||
186
dist/firefox/background.js
vendored
186
dist/firefox/background.js
vendored
@@ -10,37 +10,181 @@
|
|||||||
];
|
];
|
||||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||||
|
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||||
|
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||||
|
const COOKIE_REFRESH_POLL_MS = 250;
|
||||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXsrfTokenCookie()
|
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||||
.then((result) => {
|
getXsrfTokenCookie()
|
||||||
sendResponse({
|
.then((result) => {
|
||||||
ok: true,
|
sendResponse({
|
||||||
cookieName: result.cookie ? result.cookie.name : "",
|
ok: true,
|
||||||
token: result.cookie ? result.cookie.value : "",
|
cookieName: result.cookie ? result.cookie.name : "",
|
||||||
matchedCookieNames: result.matchedCookieNames,
|
token: result.cookie ? result.cookie.value : "",
|
||||||
lookupDetails: result.lookupDetails
|
matchedCookieNames: result.matchedCookieNames,
|
||||||
|
lookupDetails: result.lookupDetails
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
cookieName: "",
|
||||||
|
token: "",
|
||||||
|
matchedCookieNames: [],
|
||||||
|
lookupDetails: [],
|
||||||
|
error: error.message || "Unable to read cookies."
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
sendResponse({
|
|
||||||
ok: false,
|
|
||||||
cookieName: "",
|
|
||||||
token: "",
|
|
||||||
matchedCookieNames: [],
|
|
||||||
lookupDetails: [],
|
|
||||||
error: error.message || "Unable to read cookies."
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||||
|
refreshXsrfCookie(sender)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: error.message || "Unable to refresh XSRF cookie."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refreshXsrfCookie(sender) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const previous = (await getXsrfTokenCookie()).cookie;
|
||||||
|
const createProperties = {
|
||||||
|
url: OPPORTUNITIES_LIST_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
createProperties.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
createProperties.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshTab;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshTab = await tabsCreate(createProperties);
|
||||||
|
const current = await waitForXsrfCookieChange(previous);
|
||||||
|
const refreshed = hasCookieChanged(previous, current);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: Boolean(current),
|
||||||
|
refreshed,
|
||||||
|
cookieName: current ? current.name : "",
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
error: refreshed ? "" : "XSRF cookie was not created or changed before timeout."
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (refreshTab && Number.isInteger(refreshTab.id)) {
|
||||||
|
try {
|
||||||
|
await tabsRemove(refreshTab.id);
|
||||||
|
} catch (error) {
|
||||||
|
// The temporary tab may already have been closed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForXsrfCookieChange(previousCookie) {
|
||||||
|
const deadline = Date.now() + COOKIE_REFRESH_TIMEOUT_MS;
|
||||||
|
let currentCookie = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
currentCookie = (await getXsrfTokenCookie()).cookie;
|
||||||
|
|
||||||
|
if (hasCookieChanged(previousCookie, currentCookie)) {
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(COOKIE_REFRESH_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCookieChanged(previousCookie, currentCookie) {
|
||||||
|
if (!currentCookie || !currentCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousCookie) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousCookie.name !== currentCookie.name
|
||||||
|
|| previousCookie.value !== currentCookie.value
|
||||||
|
|| previousCookie.expirationDate !== currentCookie.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsFirefoxCookieStoreTabs() {
|
||||||
|
return Boolean(
|
||||||
|
runtimeApi.runtime
|
||||||
|
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsCreate(details) {
|
||||||
|
if (runtimeApi.tabs.create.length <= 1) {
|
||||||
|
return runtimeApi.tabs.create(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.create(details, (tab) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsRemove(tabId) {
|
||||||
|
if (runtimeApi.tabs.remove.length <= 1) {
|
||||||
|
return runtimeApi.tabs.remove(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.remove(tabId, () => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(durationMs) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
async function getXsrfTokenCookie() {
|
async function getXsrfTokenCookie() {
|
||||||
const lookupDetails = [];
|
const lookupDetails = [];
|
||||||
const allCookies = [];
|
const allCookies = [];
|
||||||
|
|||||||
199
dist/firefox/content.js
vendored
199
dist/firefox/content.js
vendored
@@ -502,8 +502,98 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestTokenRelay() {
|
async function requestTokenRelay() {
|
||||||
const xsrfTokenSource = await getXsrfToken();
|
let refreshResult = null;
|
||||||
const requestEntry = logRequestStart({
|
let refreshAttempted = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const xsrfTokenSource = await getXsrfToken();
|
||||||
|
|
||||||
|
if (!xsrfTokenSource.token) {
|
||||||
|
if (refreshAttempted) {
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh.");
|
||||||
|
throw new Error("XSRF token cookie not found after automatic refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshResult = await refreshXsrfCookie("missing-cookie");
|
||||||
|
refreshAttempted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(TOKEN_RELAY_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"x-xsrf-token": xsrfTokenSource.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await readResponseBodyForDebug(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted && isInvalidXsrfStatus(response.status)) {
|
||||||
|
refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`);
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
|
||||||
|
return responseData.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||||
|
return logRequestStart({
|
||||||
name: "requestTokenRelay",
|
name: "requestTokenRelay",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: TOKEN_RELAY_URL,
|
url: TOKEN_RELAY_URL,
|
||||||
@@ -519,56 +609,25 @@
|
|||||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||||
: "(none)",
|
: "(none)",
|
||||||
xsrfTokenError: xsrfTokenSource.error || ""
|
xsrfTokenError: xsrfTokenSource.error || "",
|
||||||
|
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!xsrfTokenSource.token) {
|
function isInvalidXsrfStatus(status) {
|
||||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
return status === 400 || status === 401 || status === 403;
|
||||||
throw new Error("XSRF token cookie not found.");
|
}
|
||||||
|
|
||||||
|
function formatXsrfRefreshResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
return "not-requested";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const outcome = result.refreshed ? "refreshed" : "not-refreshed";
|
||||||
const response = await fetch(TOKEN_RELAY_URL, {
|
const cookieName = result.cookieName || "(none)";
|
||||||
method: "GET",
|
const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
|
||||||
credentials: "include",
|
return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
|
||||||
headers: {
|
|
||||||
"x-xsrf-token": xsrfTokenSource.token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseBody = await readResponseBodyForDebug(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseData = await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
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)));
|
|
||||||
|
|
||||||
return responseData.access_token;
|
|
||||||
} catch (error) {
|
|
||||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
@@ -2106,6 +2165,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshXsrfCookie(reason) {
|
||||||
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
|
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: "Runtime messaging API unavailable."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
runtimeApi.runtime.sendMessage({
|
||||||
|
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||||
|
}, (response) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: lastError.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
...(response || {
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: "Empty refresh response."
|
||||||
|
}),
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthBadge(status) {
|
function updateAuthBadge(status) {
|
||||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||||
|
|
||||||
|
|||||||
@@ -10,37 +10,181 @@
|
|||||||
];
|
];
|
||||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||||
|
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||||
|
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||||
|
const COOKIE_REFRESH_POLL_MS = 250;
|
||||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXsrfTokenCookie()
|
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||||
.then((result) => {
|
getXsrfTokenCookie()
|
||||||
sendResponse({
|
.then((result) => {
|
||||||
ok: true,
|
sendResponse({
|
||||||
cookieName: result.cookie ? result.cookie.name : "",
|
ok: true,
|
||||||
token: result.cookie ? result.cookie.value : "",
|
cookieName: result.cookie ? result.cookie.name : "",
|
||||||
matchedCookieNames: result.matchedCookieNames,
|
token: result.cookie ? result.cookie.value : "",
|
||||||
lookupDetails: result.lookupDetails
|
matchedCookieNames: result.matchedCookieNames,
|
||||||
|
lookupDetails: result.lookupDetails
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
cookieName: "",
|
||||||
|
token: "",
|
||||||
|
matchedCookieNames: [],
|
||||||
|
lookupDetails: [],
|
||||||
|
error: error.message || "Unable to read cookies."
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
sendResponse({
|
|
||||||
ok: false,
|
|
||||||
cookieName: "",
|
|
||||||
token: "",
|
|
||||||
matchedCookieNames: [],
|
|
||||||
lookupDetails: [],
|
|
||||||
error: error.message || "Unable to read cookies."
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||||
|
refreshXsrfCookie(sender)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: error.message || "Unable to refresh XSRF cookie."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refreshXsrfCookie(sender) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const previous = (await getXsrfTokenCookie()).cookie;
|
||||||
|
const createProperties = {
|
||||||
|
url: OPPORTUNITIES_LIST_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
createProperties.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
createProperties.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshTab;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshTab = await tabsCreate(createProperties);
|
||||||
|
const current = await waitForXsrfCookieChange(previous);
|
||||||
|
const refreshed = hasCookieChanged(previous, current);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: Boolean(current),
|
||||||
|
refreshed,
|
||||||
|
cookieName: current ? current.name : "",
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
error: refreshed ? "" : "XSRF cookie was not created or changed before timeout."
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (refreshTab && Number.isInteger(refreshTab.id)) {
|
||||||
|
try {
|
||||||
|
await tabsRemove(refreshTab.id);
|
||||||
|
} catch (error) {
|
||||||
|
// The temporary tab may already have been closed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForXsrfCookieChange(previousCookie) {
|
||||||
|
const deadline = Date.now() + COOKIE_REFRESH_TIMEOUT_MS;
|
||||||
|
let currentCookie = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
currentCookie = (await getXsrfTokenCookie()).cookie;
|
||||||
|
|
||||||
|
if (hasCookieChanged(previousCookie, currentCookie)) {
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(COOKIE_REFRESH_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCookieChanged(previousCookie, currentCookie) {
|
||||||
|
if (!currentCookie || !currentCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousCookie) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousCookie.name !== currentCookie.name
|
||||||
|
|| previousCookie.value !== currentCookie.value
|
||||||
|
|| previousCookie.expirationDate !== currentCookie.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsFirefoxCookieStoreTabs() {
|
||||||
|
return Boolean(
|
||||||
|
runtimeApi.runtime
|
||||||
|
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsCreate(details) {
|
||||||
|
if (runtimeApi.tabs.create.length <= 1) {
|
||||||
|
return runtimeApi.tabs.create(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.create(details, (tab) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsRemove(tabId) {
|
||||||
|
if (runtimeApi.tabs.remove.length <= 1) {
|
||||||
|
return runtimeApi.tabs.remove(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.remove(tabId, () => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(durationMs) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
async function getXsrfTokenCookie() {
|
async function getXsrfTokenCookie() {
|
||||||
const lookupDetails = [];
|
const lookupDetails = [];
|
||||||
const allCookies = [];
|
const allCookies = [];
|
||||||
|
|||||||
199
src/content.js
199
src/content.js
@@ -502,8 +502,98 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestTokenRelay() {
|
async function requestTokenRelay() {
|
||||||
const xsrfTokenSource = await getXsrfToken();
|
let refreshResult = null;
|
||||||
const requestEntry = logRequestStart({
|
let refreshAttempted = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const xsrfTokenSource = await getXsrfToken();
|
||||||
|
|
||||||
|
if (!xsrfTokenSource.token) {
|
||||||
|
if (refreshAttempted) {
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh.");
|
||||||
|
throw new Error("XSRF token cookie not found after automatic refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshResult = await refreshXsrfCookie("missing-cookie");
|
||||||
|
refreshAttempted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(TOKEN_RELAY_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"x-xsrf-token": xsrfTokenSource.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await readResponseBodyForDebug(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted && isInvalidXsrfStatus(response.status)) {
|
||||||
|
refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`);
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
|
||||||
|
return responseData.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||||
|
return logRequestStart({
|
||||||
name: "requestTokenRelay",
|
name: "requestTokenRelay",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: TOKEN_RELAY_URL,
|
url: TOKEN_RELAY_URL,
|
||||||
@@ -519,56 +609,25 @@
|
|||||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||||
: "(none)",
|
: "(none)",
|
||||||
xsrfTokenError: xsrfTokenSource.error || ""
|
xsrfTokenError: xsrfTokenSource.error || "",
|
||||||
|
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!xsrfTokenSource.token) {
|
function isInvalidXsrfStatus(status) {
|
||||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
return status === 400 || status === 401 || status === 403;
|
||||||
throw new Error("XSRF token cookie not found.");
|
}
|
||||||
|
|
||||||
|
function formatXsrfRefreshResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
return "not-requested";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const outcome = result.refreshed ? "refreshed" : "not-refreshed";
|
||||||
const response = await fetch(TOKEN_RELAY_URL, {
|
const cookieName = result.cookieName || "(none)";
|
||||||
method: "GET",
|
const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
|
||||||
credentials: "include",
|
return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
|
||||||
headers: {
|
|
||||||
"x-xsrf-token": xsrfTokenSource.token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseBody = await readResponseBodyForDebug(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseData = await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
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)));
|
|
||||||
|
|
||||||
return responseData.access_token;
|
|
||||||
} catch (error) {
|
|
||||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
@@ -2106,6 +2165,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshXsrfCookie(reason) {
|
||||||
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
|
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: "Runtime messaging API unavailable."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
runtimeApi.runtime.sendMessage({
|
||||||
|
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||||
|
}, (response) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: lastError.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
...(response || {
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: "Empty refresh response."
|
||||||
|
}),
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthBadge(status) {
|
function updateAuthBadge(status) {
|
||||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user