feat(ext): verify cookies in-browser before uploading (1.0.7)
CI / lint (push) Successful in 4s
CI / backend-lint-and-test (push) Successful in 37s
CI / frontend-build (push) Successful in 40s
extension / lint (push) Successful in 36s
CI / intimp (push) Successful in 4m4s
CI / intapi (push) Successful in 8m2s
CI / intcore (push) Successful in 8m45s
extension / lint (pull_request) Successful in 14s

Pre-upload verify request: after capturing the live browser cookies,
hit a known authenticated endpoint with credentials:'include' from the
extension's background context. If the platform reports we're not
logged in, abort the upload so we don't overwrite FC-side credentials
with stale data.

- platforms.js: add `verify` config per cookie-auth platform
  - hentaifoundry: HEAD /?enterAgree=1 (mirrors gallery-dl's HF
    _init_site_filters; same 401 path the operator hit 2026-06-03)
  - patreon: GET /api/current_user (clean 401 when logged out)
  - subscribestar, deviantart: no stable auth endpoint, skip verify
- cookies.js: verifyCookiesForPlatform() returns {ok, status, reason}.
  ok=true/false/null tri-state — null = verify not configured, caller
  treats as "proceed".
- background.js EXPORT_COOKIES + EXPORT_ALL_COOKIES: verify gates the
  upload; failures bubble up with the platform's name + reason.
- popup.js: success message now appends "(verified ✓)" when applicable.
- manifest + package.json: 1.0.6 → 1.0.7.
This commit is contained in:
2026-06-03 14:04:45 -04:00
parent e450145304
commit d3245f0c22
6 changed files with 78 additions and 5 deletions
+18 -2
View File
@@ -194,9 +194,20 @@ browser.runtime.onMessage.addListener(async (msg) => {
if (platform.authType === 'cookies') { if (platform.authType === 'cookies') {
const cookies = await extractCookiesForPlatform(key); const cookies = await extractCookiesForPlatform(key);
if (cookies.length === 0) return { error: 'No cookies found — log in first.' }; if (cookies.length === 0) return { error: 'No cookies found — log in first.' };
// Verify the captured cookies are actually live BEFORE
// 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 {
error: `Captured ${cookies.length} ${platform.name} cookies but they don't appear authenticated (${v.reason}). Log in again in this browser, then retry.`,
};
}
const data = toNetscapeFormat(cookies); const data = toNetscapeFormat(cookies);
await api.uploadCredentials(key, 'cookies', data); await api.uploadCredentials(key, 'cookies', data);
return { success: true, cookieCount: cookies.length }; 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.' };
@@ -229,8 +240,13 @@ browser.runtime.onMessage.addListener(async (msg) => {
results[key] = { skipped: true, reason: 'no cookies' }; results[key] = { skipped: true, reason: 'no cookies' };
continue; continue;
} }
const v = await verifyCookiesForPlatform(key);
if (v.ok === false) {
results[key] = { error: `verify failed: ${v.reason}` };
continue;
}
await api.uploadCredentials(key, 'cookies', toNetscapeFormat(cookies)); await api.uploadCredentials(key, 'cookies', toNetscapeFormat(cookies));
results[key] = { success: true, cookieCount: cookies.length }; 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 };
} }
+35
View File
@@ -76,3 +76,38 @@ async function getCookieCount(platformKey) {
return 0; return 0;
} }
} }
/**
* Verify cookies are live by hitting an authenticated endpoint with the
* browser's current cookie jar. Returns:
* { ok: true, status } — verified
* { ok: false, status, reason } — endpoint said we're not logged in
* { ok: null, reason } — no verify config for this platform; caller
* should treat as "verify not available,
* proceed with upload"
*
* Implementation note: extensions with `host_permissions` for the target
* domain get the user's cookies auto-attached to fetch() — same set
* gallery-dl will later use on the backend.
*/
async function verifyCookiesForPlatform(platformKey) {
const platform = PLATFORMS[platformKey];
if (!platform) return { ok: false, reason: `Unknown platform: ${platformKey}` };
if (!platform.verify) return { ok: null, reason: 'verify-not-configured' };
const { url, method, okStatuses } = platform.verify;
let resp;
try {
resp = await fetch(url, { method, credentials: 'include', cache: 'no-store' });
} catch (e) {
return { ok: false, reason: `Verify request failed: ${e.message}` };
}
if (okStatuses.includes(resp.status)) {
return { ok: true, status: resp.status };
}
return {
ok: false,
status: resp.status,
reason: `${url} returned HTTP ${resp.status} — session looks stale or logged out`,
};
}
+21
View File
@@ -13,6 +13,13 @@ const PLATFORMS = {
authType: 'cookies', authType: 'cookies',
color: '#FF424D', color: '#FF424D',
urlPattern: /^https?:\/\/(www\.)?patreon\.com/, urlPattern: /^https?:\/\/(www\.)?patreon\.com/,
// Patreon's `/api/current_user` returns 200 + the logged-in user
// when authenticated, 401 otherwise. Cheapest definitive check.
verify: {
url: 'https://www.patreon.com/api/current_user',
method: 'GET',
okStatuses: [200],
},
}, },
subscribestar: { subscribestar: {
name: 'SubscribeStar', name: 'SubscribeStar',
@@ -26,6 +33,9 @@ const PLATFORMS = {
authType: 'cookies', authType: 'cookies',
color: '#FFD700', color: '#FFD700',
urlPattern: /^https?:\/\/(www\.)?subscribestar\.(com|adult)/, urlPattern: /^https?:\/\/(www\.)?subscribestar\.(com|adult)/,
// No known stable auth-required endpoint that returns a definitive
// status code; skipping verify so we don't false-positive-fail
// good cookies. Operator can add later if a clean endpoint surfaces.
}, },
hentaifoundry: { hentaifoundry: {
name: 'Hentai Foundry', name: 'Hentai Foundry',
@@ -33,6 +43,14 @@ const PLATFORMS = {
authType: 'cookies', authType: 'cookies',
color: '#9C27B0', color: '#9C27B0',
urlPattern: /^https?:\/\/(www\.)?hentai-foundry\.com/, urlPattern: /^https?:\/\/(www\.)?hentai-foundry\.com/,
// Mirror gallery-dl's _init_site_filters: HEAD on `?enterAgree=1`.
// Logged in → 200, logged out → 401. Catches the exact failure mode
// the backend extractor would hit later.
verify: {
url: 'https://www.hentai-foundry.com/?enterAgree=1',
method: 'HEAD',
okStatuses: [200],
},
}, },
discord: { discord: {
name: 'Discord', name: 'Discord',
@@ -56,6 +74,9 @@ const PLATFORMS = {
authType: 'cookies', authType: 'cookies',
color: '#05CC47', color: '#05CC47',
urlPattern: /^https?:\/\/(www\.)?deviantart\.com/, urlPattern: /^https?:\/\/(www\.)?deviantart\.com/,
// DA's logged-in-only endpoints sit behind their internal _napi
// namespace which shifts; skipping verify until a stable check
// surfaces. Same posture as SubscribeStar.
}, },
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "FabledCurator", "name": "FabledCurator",
"version": "1.0.6", "version": "1.0.7",
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.", "description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
"browser_specific_settings": { "browser_specific_settings": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "fabledcurator-extension", "name": "fabledcurator-extension",
"version": "1.0.6", "version": "1.0.7",
"private": true, "private": true,
"description": "Firefox extension for FabledCurator", "description": "Firefox extension for FabledCurator",
"scripts": { "scripts": {
+2 -1
View File
@@ -132,8 +132,9 @@ async function exportPlatformCookies(key, card) {
if (r.error) showError(r.error); if (r.error) showError(r.error);
else { else {
const n = r.cookieCount ?? null; const n = r.cookieCount ?? null;
const verifiedSuffix = r.verified ? ' (verified ✓)' : '';
const msg = n !== null const msg = n !== null
? `${PLATFORMS[key].name}: ${n} cookies exported` ? `${PLATFORMS[key].name}: ${n} cookies exported${verifiedSuffix}`
: `${PLATFORMS[key].name}: token exported`; : `${PLATFORMS[key].name}: token exported`;
showSuccess(msg); showSuccess(msg);
await loadPlatformStatus(); await loadPlatformStatus();