claude-code-router/extension/chrome/background.js

262 lines
7.5 KiB
JavaScript

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (!message || message.type !== "ccr-login-import-confirm") {
return false;
}
runImport(message.importUrl)
.then((result) => sendResponse({ ok: true, result }))
.catch((error) => sendResponse({ error: formatError(error), ok: false }));
return true;
});
async function runImport(importUrl) {
const normalizedImportUrl = normalizeImportUrl(importUrl);
if (!normalizedImportUrl) {
throw new Error("Invalid CCR import URL.");
}
const job = await fetchImportJob(normalizedImportUrl);
const domains = Array.isArray(job.domains) ? job.domains.map(normalizeDomain).filter(Boolean) : [];
if (domains.length === 0) {
throw new Error("CCR import job does not include any domains.");
}
await ensureHostPermissions(domains);
const cookies = await readCookiesForDomains(domains);
const localStorageEntries = await readLocalStorageForDomains(domains, cookies);
if (cookies.length === 0 && localStorageEntries.length === 0) {
throw new Error("No cookies or localStorage entries were found for the selected domains.");
}
return await submitLoginState(normalizedImportUrl, cookies, localStorageEntries, domains);
}
async function fetchImportJob(importUrl) {
const response = await fetch(importUrl, {
headers: {
"x-ccr-login-import": "chrome-extension"
}
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body?.error?.message || `CCR import job request failed (${response.status}).`);
}
if (!body.job || body.job.status !== "pending") {
throw new Error(`CCR import job is ${body.job?.status || "unavailable"}.`);
}
return body.job;
}
async function submitLoginState(importUrl, cookies, localStorage, domains) {
const response = await fetch(`${importUrl.replace(/\/+$/, "")}/cookies`, {
body: JSON.stringify({
cookies,
domains,
localStorage,
source: {
browser: "chrome",
extension: chrome.runtime.getManifest().version
}
}),
headers: {
"content-type": "application/json",
"x-ccr-login-import": "chrome-extension"
},
method: "POST"
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body?.error?.message || `CCR login import failed (${response.status}).`);
}
return body.result || {};
}
async function readCookiesForDomains(domains) {
const cookies = [];
for (const domain of domains) {
cookies.push(...await chrome.cookies.getAll({ domain }));
}
return dedupeCookies(cookies);
}
async function readLocalStorageForDomains(domains, cookies) {
const origins = localStorageOriginsForDomains(domains, cookies);
const entries = [];
for (const origin of origins) {
const entry = await readLocalStorageForOrigin(origin).catch(() => undefined);
if (entry && Object.keys(entry.items).length > 0) {
entries.push(entry);
}
}
return entries;
}
async function readLocalStorageForOrigin(origin) {
const tab = await chrome.tabs.create({
active: false,
url: `${origin}/`
});
try {
await waitForTabLoad(tab.id, 15000);
const [frameResult] = await chrome.scripting.executeScript({
func: () => {
const items = {};
for (let index = 0; index < window.localStorage.length; index += 1) {
const key = window.localStorage.key(index);
if (key !== null) {
items[key] = window.localStorage.getItem(key) || "";
}
}
return {
items,
origin: window.location.origin
};
},
target: { tabId: tab.id },
world: "MAIN"
});
const result = frameResult?.result;
if (!result || !allowedLocalStorageOrigin(result.origin, origin)) {
return undefined;
}
return result;
} finally {
if (tab.id !== undefined) {
await chrome.tabs.remove(tab.id).catch(() => undefined);
}
}
}
function waitForTabLoad(tabId, timeoutMs) {
return new Promise((resolve) => {
let done = false;
const finish = () => {
if (done) {
return;
}
done = true;
chrome.tabs.onUpdated.removeListener(listener);
clearTimeout(timeout);
resolve();
};
const listener = (updatedTabId, changeInfo) => {
if (updatedTabId === tabId && changeInfo.status === "complete") {
finish();
}
};
const timeout = setTimeout(finish, timeoutMs);
chrome.tabs.onUpdated.addListener(listener);
});
}
function dedupeCookies(cookies) {
const seen = new Set();
const result = [];
for (const cookie of cookies) {
const partitionKey = cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : "";
const key = `${cookie.storeId || ""}\n${cookie.domain}\n${cookie.path}\n${cookie.name}\n${partitionKey}`;
if (!seen.has(key)) {
seen.add(key);
result.push(cookie);
}
}
return result;
}
function localStorageOriginsForDomains(domains, cookies) {
const origins = new Set();
for (const domain of domains) {
if (domain === "localhost" || isIpAddress(domain)) {
origins.add(`http://${domain}`);
origins.add(`https://${domain}`);
} else {
origins.add(`https://${domain}`);
}
}
for (const cookie of cookies) {
const host = normalizeDomain(cookie.domain);
if (!host || !cookie.hostOnly || !domains.some((domain) => host === domain || host.endsWith(`.${domain}`))) {
continue;
}
origins.add(`${cookie.secure === false ? "http" : "https"}://${host}`);
}
return [...origins];
}
function originsForDomains(domains) {
return [...new Set(domains.flatMap((domain) => {
if (domain === "localhost" || isIpAddress(domain)) {
return [
`http://${domain}/*`,
`https://${domain}/*`
];
}
return [
`http://${domain}/*`,
`https://${domain}/*`,
`http://*.${domain}/*`,
`https://*.${domain}/*`
];
}))];
}
async function ensureHostPermissions(domains) {
const origins = originsForDomains(domains);
const granted = await chrome.permissions.contains({ origins });
if (granted) {
return;
}
throw new Error(
[
`CCR Login Import does not have Chrome site access for ${domains.join(", ")}.`,
"Reload the unpacked extension after updating it, then grant the extension site access for the requested domains in Chrome extensions settings."
].join(" ")
);
}
function normalizeImportUrl(value) {
const raw = typeof value === "string" ? value.trim() : "";
if (!raw) {
return "";
}
try {
const url = new URL(raw);
if (!["127.0.0.1", "localhost"].includes(url.hostname)) {
return "";
}
if (!/^\/chrome-import\/jobs\/[^/]+\/?$/.test(url.pathname)) {
return "";
}
url.pathname = url.pathname.replace(/\/+$/, "");
return url.toString();
} catch {
return "";
}
}
function normalizeDomain(value) {
return typeof value === "string"
? value.trim().replace(/^\*\./, "").replace(/^\./, "").toLowerCase()
: "";
}
function allowedLocalStorageOrigin(actualOrigin, requestedOrigin) {
try {
const actual = new URL(actualOrigin);
const requested = new URL(requestedOrigin);
return actual.protocol === requested.protocol && actual.hostname === requested.hostname && actual.port === requested.port;
} catch {
return false;
}
}
function isIpAddress(value) {
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value) || value.includes(":");
}
function formatError(error) {
return error instanceof Error ? error.message : String(error);
}