209 lines
5.8 KiB
TypeScript
209 lines
5.8 KiB
TypeScript
import { ExtensionRequest, GetCookiesResponse, ProviderInfo, StoredRefreshEntry } from './types/messages';
|
|
import { getAllProviders, getProvider } from './providers/provider.registry';
|
|
import { CookieProvider } from './providers/cookie-provider.interface';
|
|
|
|
const EXTENSION_VERSION = '2.0.0';
|
|
const REFRESH_ALARM_NAME = 'cookie-refresh';
|
|
const STORAGE_KEY = 'refreshEntries';
|
|
|
|
const ALLOWED_ORIGIN_PATTERNS = [
|
|
/^https?:\/\/localhost(:\d+)?$/,
|
|
/^https?:\/\/([a-z0-9-]+\.)*postiz\.com$/,
|
|
];
|
|
|
|
function isOriginAllowed(origin: string | undefined): boolean {
|
|
if (!origin) return false;
|
|
return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
|
|
}
|
|
|
|
async function extractCookies(provider: CookieProvider): Promise<GetCookiesResponse> {
|
|
const allCookies = await chrome.cookies.getAll({ url: provider.url });
|
|
|
|
const extracted: Record<string, string> = {};
|
|
const missingRequired: string[] = [];
|
|
|
|
for (const def of provider.cookies) {
|
|
const found = allCookies.find((c) => c.name === def.name);
|
|
if (found) {
|
|
extracted[def.name] = found.value;
|
|
} else if (def.required) {
|
|
missingRequired.push(def.name);
|
|
}
|
|
}
|
|
|
|
if (missingRequired.length > 0) {
|
|
return {
|
|
success: false,
|
|
provider: provider.identifier,
|
|
error: `Missing required cookies: ${missingRequired.join(', ')}. User may need to log in to ${provider.name}.`,
|
|
missingCookies: missingRequired,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
provider: provider.identifier,
|
|
cookies: extracted,
|
|
};
|
|
}
|
|
|
|
// --- Refresh Token Storage Helpers ---
|
|
|
|
async function getStoredEntries(): Promise<Record<string, StoredRefreshEntry>> {
|
|
const result = await chrome.storage.local.get(STORAGE_KEY);
|
|
return result[STORAGE_KEY] || {};
|
|
}
|
|
|
|
async function setStoredEntries(entries: Record<string, StoredRefreshEntry>): Promise<void> {
|
|
await chrome.storage.local.set({ [STORAGE_KEY]: entries });
|
|
}
|
|
|
|
async function ensureAlarm(): Promise<void> {
|
|
const existing = await chrome.alarms.get(REFRESH_ALARM_NAME);
|
|
if (!existing) {
|
|
chrome.alarms.create(REFRESH_ALARM_NAME, { periodInMinutes: 1440 });
|
|
}
|
|
}
|
|
|
|
async function clearAlarmIfEmpty(): Promise<void> {
|
|
const entries = await getStoredEntries();
|
|
if (Object.keys(entries).length === 0) {
|
|
await chrome.alarms.clear(REFRESH_ALARM_NAME);
|
|
}
|
|
}
|
|
|
|
// --- Background Cookie Refresh ---
|
|
|
|
async function refreshAllCookies(): Promise<void> {
|
|
const entries = await getStoredEntries();
|
|
for (const [integrationId, entry] of Object.entries(entries)) {
|
|
try {
|
|
const provider = getProvider(entry.provider);
|
|
if (!provider) continue;
|
|
|
|
const cookieResult = await extractCookies(provider);
|
|
if (!cookieResult.success) continue;
|
|
|
|
const base64Cookies = btoa(JSON.stringify(cookieResult.cookies));
|
|
|
|
await fetch(`${entry.backendUrl}/integrations/extension-refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ jwt: entry.jwt, cookies: base64Cookies }),
|
|
});
|
|
} catch {
|
|
// Silently skip — will retry next cycle
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Alarm Listener ---
|
|
|
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
if (alarm.name === REFRESH_ALARM_NAME) {
|
|
refreshAllCookies();
|
|
}
|
|
});
|
|
|
|
// --- Ensure alarm on startup ---
|
|
|
|
(async () => {
|
|
const entries = await getStoredEntries();
|
|
if (Object.keys(entries).length > 0) {
|
|
await ensureAlarm();
|
|
}
|
|
})();
|
|
|
|
// --- Message Listener ---
|
|
|
|
chrome.runtime.onMessageExternal.addListener(
|
|
(
|
|
message: ExtensionRequest,
|
|
sender: chrome.runtime.MessageSender,
|
|
sendResponse: (response: unknown) => void
|
|
) => {
|
|
const origin = sender.origin ?? sender.url;
|
|
if (!isOriginAllowed(origin)) {
|
|
sendResponse({ error: 'Unauthorized origin' });
|
|
return true;
|
|
}
|
|
|
|
switch (message.type) {
|
|
case 'PING': {
|
|
sendResponse({ status: 'ok', version: EXTENSION_VERSION });
|
|
break;
|
|
}
|
|
|
|
case 'GET_PROVIDERS': {
|
|
const providers = getAllProviders();
|
|
const providerInfos: ProviderInfo[] = providers.map((p) => ({
|
|
identifier: p.identifier,
|
|
name: p.name,
|
|
url: p.url,
|
|
cookieNames: p.cookies.map((c) => c.name),
|
|
}));
|
|
sendResponse({ providers: providerInfos });
|
|
break;
|
|
}
|
|
|
|
case 'GET_COOKIES': {
|
|
const provider = getProvider(message.provider);
|
|
if (!provider) {
|
|
sendResponse({
|
|
success: false,
|
|
provider: message.provider,
|
|
error: `Unknown provider: ${message.provider}`,
|
|
});
|
|
break;
|
|
}
|
|
|
|
extractCookies(provider)
|
|
.then((result) => sendResponse(result))
|
|
.catch((err) =>
|
|
sendResponse({
|
|
success: false,
|
|
provider: message.provider,
|
|
error: `Failed to extract cookies: ${err.message}`,
|
|
})
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
case 'STORE_REFRESH_TOKEN': {
|
|
(async () => {
|
|
const entries = await getStoredEntries();
|
|
entries[message.integrationId] = {
|
|
jwt: message.jwt,
|
|
backendUrl: message.backendUrl,
|
|
provider: message.provider,
|
|
};
|
|
await setStoredEntries(entries);
|
|
await ensureAlarm();
|
|
sendResponse({ success: true });
|
|
})().catch(() => sendResponse({ success: false }));
|
|
|
|
return true;
|
|
}
|
|
|
|
case 'REMOVE_REFRESH_TOKEN': {
|
|
(async () => {
|
|
const entries = await getStoredEntries();
|
|
delete entries[message.integrationId];
|
|
await setStoredEntries(entries);
|
|
await clearAlarmIfEmpty();
|
|
sendResponse({ success: true });
|
|
})().catch(() => sendResponse({ success: false }));
|
|
|
|
return true;
|
|
}
|
|
|
|
default: {
|
|
sendResponse({ error: `Unknown message type: ${(message as any).type}` });
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
);
|