refactor(extension): DRY the web-root transform + cookie-export flow (#161)
Behavior-preserving (extension JS has lint-only CI + your manual test): - api.webRoot() single-sources the baseUrl→web-root transform (strip trailing slash + /api) that was copy-pasted in background.js's self-update check and OPEN_ARTIST_PAGE, whose comments even cross-referenced each other. - exportPlatformCookies(key) shares the extract→verify→upload spine between EXPORT_COOKIES (single) and EXPORT_ALL_COOKIES; it returns a structured outcome so each caller keeps its own response/skip messages verbatim. - popup.js mutedNote(text) replaces the "centered muted note" div hand-rolled in the platform-loading, sources-loading, and empty-sources renderers. No version bump — no behavior change, so it rides the next real ext release rather than forcing an AMO re-sign + reinstall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi
This commit is contained in:
@@ -60,9 +60,8 @@ async function checkForUpdateInfo() {
|
|||||||
}
|
}
|
||||||
const currentVersion = browser.runtime.getManifest().version;
|
const currentVersion = browser.runtime.getManifest().version;
|
||||||
const latestVersion = info && info.version ? info.version : null;
|
const latestVersion = info && info.version ? info.version : null;
|
||||||
// latest_url is served from the web root; strip the /api suffix off baseUrl
|
// latest_url is served from the web root, not the JSON API.
|
||||||
// (same transform as OPEN_ARTIST_PAGE).
|
const base = api.webRoot();
|
||||||
const base = (api.baseUrl || '').replace(/\/+$/, '').replace(/\/api$/, '');
|
|
||||||
return {
|
return {
|
||||||
updateAvailable: !!latestVersion && versionIsNewer(latestVersion, currentVersion),
|
updateAvailable: !!latestVersion && versionIsNewer(latestVersion, currentVersion),
|
||||||
currentVersion,
|
currentVersion,
|
||||||
@@ -211,6 +210,21 @@ browser.webRequest.onBeforeRedirect.addListener(
|
|||||||
{ urls: ['https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback*'] },
|
{ urls: ['https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback*'] },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Extract → verify → upload one cookie-auth platform. Returns a structured
|
||||||
|
// outcome so the two callers (EXPORT_COOKIES single, EXPORT_ALL_COOKIES) shape
|
||||||
|
// their own response + skip semantics. Verifies the captured cookies are
|
||||||
|
// actually live BEFORE uploading, so a confirmed-stale session doesn't overwrite
|
||||||
|
// good FC-side credentials; platforms with no verify config (v.ok === null) fall
|
||||||
|
// through to upload.
|
||||||
|
async function exportPlatformCookies(key) {
|
||||||
|
const cookies = await extractCookiesForPlatform(key);
|
||||||
|
if (cookies.length === 0) return { status: 'empty' };
|
||||||
|
const v = await verifyCookiesForPlatform(key);
|
||||||
|
if (v.ok === false) return { status: 'stale', reason: v.reason, cookieCount: cookies.length };
|
||||||
|
await api.uploadCredentials(key, 'cookies', toNetscapeFormat(cookies));
|
||||||
|
return { status: 'ok', cookieCount: cookies.length, verified: v.ok === true };
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Message router ----
|
// ---- Message router ----
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener(async (msg) => {
|
browser.runtime.onMessage.addListener(async (msg) => {
|
||||||
@@ -255,22 +269,14 @@ browser.runtime.onMessage.addListener(async (msg) => {
|
|||||||
if (!platform) return { error: `Unknown platform: ${key}` };
|
if (!platform) return { error: `Unknown platform: ${key}` };
|
||||||
try {
|
try {
|
||||||
if (platform.authType === 'cookies') {
|
if (platform.authType === 'cookies') {
|
||||||
const cookies = await extractCookiesForPlatform(key);
|
const r = await exportPlatformCookies(key);
|
||||||
if (cookies.length === 0) return { error: 'No cookies found — log in first.' };
|
if (r.status === 'empty') return { error: 'No cookies found — log in first.' };
|
||||||
// Verify the captured cookies are actually live BEFORE
|
if (r.status === 'stale') {
|
||||||
// uploading. Skips upload on confirmed-stale sessions so we
|
|
||||||
// don't overwrite FC-side credentials with garbage. Platforms
|
|
||||||
// without a verify config (verify.ok === null) fall through
|
|
||||||
// to upload as before.
|
|
||||||
const v = await verifyCookiesForPlatform(key);
|
|
||||||
if (v.ok === false) {
|
|
||||||
return {
|
return {
|
||||||
error: `Captured ${cookies.length} ${platform.name} cookies but they don't appear authenticated (${v.reason}). Log in again in this browser, then retry.`,
|
error: `Captured ${r.cookieCount} ${platform.name} cookies but they don't appear authenticated (${r.reason}). Log in again in this browser, then retry.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const data = toNetscapeFormat(cookies);
|
return { success: true, cookieCount: r.cookieCount, verified: r.verified };
|
||||||
await api.uploadCredentials(key, 'cookies', data);
|
|
||||||
return { success: true, cookieCount: cookies.length, verified: v.ok === true };
|
|
||||||
}
|
}
|
||||||
if (key === 'discord') {
|
if (key === 'discord') {
|
||||||
if (!discordToken) return { error: 'Open discord.com to capture a token first.' };
|
if (!discordToken) return { error: 'Open discord.com to capture a token first.' };
|
||||||
@@ -298,18 +304,10 @@ browser.runtime.onMessage.addListener(async (msg) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const cookies = await extractCookiesForPlatform(key);
|
const r = await exportPlatformCookies(key);
|
||||||
if (cookies.length === 0) {
|
if (r.status === 'empty') results[key] = { skipped: true, reason: 'no cookies' };
|
||||||
results[key] = { skipped: true, reason: 'no cookies' };
|
else if (r.status === 'stale') results[key] = { error: `verify failed: ${r.reason}` };
|
||||||
continue;
|
else results[key] = { success: true, cookieCount: r.cookieCount, verified: r.verified };
|
||||||
}
|
|
||||||
const v = await verifyCookiesForPlatform(key);
|
|
||||||
if (v.ok === false) {
|
|
||||||
results[key] = { error: `verify failed: ${v.reason}` };
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await api.uploadCredentials(key, 'cookies', toNetscapeFormat(cookies));
|
|
||||||
results[key] = { success: true, cookieCount: cookies.length, verified: v.ok === true };
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
results[key] = { error: e.message };
|
results[key] = { error: e.message };
|
||||||
}
|
}
|
||||||
@@ -346,11 +344,9 @@ browser.runtime.onMessage.addListener(async (msg) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'OPEN_ARTIST_PAGE': {
|
case 'OPEN_ARTIST_PAGE': {
|
||||||
// apiUrl is configured with the /api suffix (see
|
// The SPA artist route (/artist/:slug) is served from the web root, not
|
||||||
// options/options.html placeholder); the SPA artist route is
|
// the JSON API — see api.webRoot().
|
||||||
// /artist/:slug, served from the same origin. Strip /api so the
|
const base = api.webRoot();
|
||||||
// browser-level URL hits the Vue router, not the JSON API.
|
|
||||||
const base = (api.baseUrl || '').replace(/\/+$/, '').replace(/\/api$/, '');
|
|
||||||
const slug = encodeURIComponent(msg.slug || '');
|
const slug = encodeURIComponent(msg.slug || '');
|
||||||
if (!base || !slug) return { error: 'apiUrl or slug missing' };
|
if (!base || !slug) return { error: 'apiUrl or slug missing' };
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ class FabledCuratorAPI {
|
|||||||
return this.request('GET', '/extension/manifest');
|
return this.request('GET', '/extension/manifest');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The web/SPA root: baseUrl with the trailing slash + `/api` suffix stripped.
|
||||||
|
// Where the Vue router (artist pages) and the served XPI live, NOT the JSON
|
||||||
|
// API. Used by OPEN_ARTIST_PAGE + the self-update check.
|
||||||
|
webRoot() {
|
||||||
|
return (this.baseUrl || '').replace(/\/+$/, '').replace(/\/api$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
// Connection test = the cheapest read with auth.
|
// Connection test = the cheapest read with auth.
|
||||||
testConnection() {
|
testConnection() {
|
||||||
return this.request('GET', '/credentials');
|
return this.request('GET', '/credentials');
|
||||||
|
|||||||
+12
-12
@@ -2,6 +2,15 @@ document.addEventListener('DOMContentLoaded', init);
|
|||||||
|
|
||||||
const CONNECTION_TEST_INTERVAL = 2 * 60 * 1000;
|
const CONNECTION_TEST_INTERVAL = 2 * 60 * 1000;
|
||||||
|
|
||||||
|
// A centered muted note div — the loading / empty state shared by the platform
|
||||||
|
// and sources lists.
|
||||||
|
function mutedNote(text) {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
|
||||||
|
d.textContent = text;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const cfg = await browser.runtime.sendMessage({ type: 'GET_CONFIG' });
|
const cfg = await browser.runtime.sendMessage({ type: 'GET_CONFIG' });
|
||||||
@@ -38,10 +47,7 @@ function showSetupRequired() {
|
|||||||
function showPlatformsLoading() {
|
function showPlatformsLoading() {
|
||||||
const c = document.getElementById('platforms-list');
|
const c = document.getElementById('platforms-list');
|
||||||
c.textContent = '';
|
c.textContent = '';
|
||||||
const d = document.createElement('div');
|
c.appendChild(mutedNote('Loading platforms…'));
|
||||||
d.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
|
|
||||||
d.textContent = 'Loading platforms…';
|
|
||||||
c.appendChild(d);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testConnectionIfNeeded() {
|
async function testConnectionIfNeeded() {
|
||||||
@@ -183,10 +189,7 @@ async function exportAllCookies() {
|
|||||||
async function loadSources() {
|
async function loadSources() {
|
||||||
const c = document.getElementById('sources-list');
|
const c = document.getElementById('sources-list');
|
||||||
c.textContent = '';
|
c.textContent = '';
|
||||||
const d = document.createElement('div');
|
c.appendChild(mutedNote('Loading sources…'));
|
||||||
d.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
|
|
||||||
d.textContent = 'Loading sources…';
|
|
||||||
c.appendChild(d);
|
|
||||||
const r = await browser.runtime.sendMessage({ type: 'LIST_SOURCES' });
|
const r = await browser.runtime.sendMessage({ type: 'LIST_SOURCES' });
|
||||||
c.textContent = '';
|
c.textContent = '';
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
@@ -197,10 +200,7 @@ async function loadSources() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!r.sources || r.sources.length === 0) {
|
if (!r.sources || r.sources.length === 0) {
|
||||||
const empty = document.createElement('div');
|
c.appendChild(mutedNote('No sources yet.'));
|
||||||
empty.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
|
|
||||||
empty.textContent = 'No sources yet.';
|
|
||||||
c.appendChild(empty);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const src of r.sources) c.appendChild(createSourceRow(src));
|
for (const src of r.sources) c.appendChild(createSourceRow(src));
|
||||||
|
|||||||
Reference in New Issue
Block a user