mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
297 lines
8.6 KiB
JavaScript
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);
|
|
}
|