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

297 lines
8.6 KiB
JavaScript

const importUrlInput = document.getElementById("import-url");
const importButton = document.getElementById("import-button");
const statusElement = document.getElementById("status");
document.addEventListener("DOMContentLoaded", () => {
void restoreLastImportUrl();
});
importButton.addEventListener("click", () => {
void runImport();
});
async function restoreLastImportUrl() {
const stored = await chrome.storage.local.get(["lastImportUrl"]);
if (typeof stored.lastImportUrl === "string") {
importUrlInput.value = stored.lastImportUrl;
}
}
async function runImport() {
const importUrl = normalizeImportUrl(importUrlInput.value);
if (!importUrl) {
setStatus("Paste the import URL copied from CCR.", "error");
return;
}
importButton.disabled = true;
try {
await chrome.storage.local.set({ lastImportUrl: importUrl });
setStatus("Reading CCR import job...");
const job = await fetchImportJob(importUrl);
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);
setStatus(`Reading cookies for ${domains.join(", ")}...`);
const cookies = await readCookiesForDomains(domains);
setStatus(`Reading localStorage for ${domains.join(", ")}...`);
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.");
}
setStatus(`Sending ${cookies.length} cookies and ${localStorageItemCount(localStorageEntries)} localStorage items to CCR...`);
const result = await submitCookies(importUrl, cookies, localStorageEntries, domains);
setStatus(
`Imported ${result.cookieImported ?? 0} cookies and ${result.localStorageImported ?? 0} localStorage items. Skipped ${result.skipped ?? 0}.`,
"ok"
);
} catch (error) {
setStatus(formatError(error), "error");
} finally {
importButton.disabled = false;
}
}
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 submitCookies(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 cookie 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);
window.clearTimeout(timeout);
resolve();
};
const listener = (updatedTabId, changeInfo) => {
if (updatedTabId === tabId && changeInfo.status === "complete") {
finish();
}
};
const timeout = window.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 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 localStorageItemCount(entries) {
return entries.reduce((count, entry) => count + Object.keys(entry.items || {}).length, 0);
}
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 = 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 isIpAddress(value) {
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value) || value.includes(":");
}
function setStatus(message, kind = "") {
statusElement.textContent = message;
statusElement.className = `status ${kind}`.trim();
}
function formatError(error) {
return error instanceof Error ? error.message : String(error);
}