refactor(platforms): retire deviantart end-to-end (#3069)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m43s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m43s
Executes the 2026-07-05 product decision (FC downloaders = art-dedicated
services only), which removed Twitter/X and Bluesky but left deviantart
fully wired for seven weeks — the half-retired state rule 22 exists to
prevent.
Removed: the PlatformInfo module and its registry entry, the gallery-dl
extractor block, extension_service's artist-page pattern, the extension's
PLATFORMS + PLATFORM_ARTIST_PATTERNS entries, its manifest host permission
and content-script match, the frontend icon/colour/label, and the operator-
facing "supported platforms" list that still advertised it.
Two judgment calls, both recorded in migration 0088:
* existing `source` rows are DISABLED, not deleted. The row is the only
record of the artist's DeviantArt URL. Disabling is also required for
correctness rather than tidiness: with the platform unregistered the
download path falls through to gallery-dl, which carries its OWN
deviantart extractor, so an enabled row would have kept downloading
from a dropped platform.
* the `credential` row IS deleted — a live session cookie for a site FC
will never call again.
Adds the invariant whose absence is why manifest.json drifted in the first
place: nothing tied its domain lists back to the platform table. The
extension suite now asserts both directions, plus that no host permission
belongs to an unclaimed domain (`*://*/*` exempted — FC is self-hosted at
an operator-chosen URL the extension cannot enumerate).
Extension version 1.0.10 -> 1.0.11: ci.yml's guard hard-fails a packaged
extension change without a bump. No release is cut — build.yml's
sign-extension job only runs on main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -1,9 +1,9 @@
|
||||
# FabledCurator Firefox Extension
|
||||
|
||||
Self-hosted Firefox extension that pushes session cookies from supported
|
||||
platforms (Patreon, SubscribeStar, Hentai-Foundry, Discord, Pixiv,
|
||||
DeviantArt) into FabledCurator, and lets you add a creator as a Source
|
||||
from their page in one click.
|
||||
platforms (Patreon, SubscribeStar, Hentai-Foundry, Discord, Pixiv)
|
||||
into FabledCurator, and lets you add a creator as a Source from their
|
||||
page in one click.
|
||||
|
||||
## Install (operator)
|
||||
|
||||
|
||||
@@ -68,16 +68,6 @@ const PLATFORMS = {
|
||||
urlPattern: /^https?:\/\/(www\.)?pixiv\.net/,
|
||||
note: 'Click to authenticate via OAuth',
|
||||
},
|
||||
deviantart: {
|
||||
name: 'DeviantArt',
|
||||
domains: ['.deviantart.com', 'www.deviantart.com', 'deviantart.com'],
|
||||
authType: 'cookies',
|
||||
color: '#05CC47',
|
||||
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.
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -98,7 +88,6 @@ const PLATFORM_ARTIST_PATTERNS = {
|
||||
patreon: /^https?:\/\/(www\.)?patreon\.com\/(?:cw\/|c\/)?(?!(?:home|search|messages|notifications|library|settings|posts)(?:[\/?#]|$))[^/?#]+/i,
|
||||
subscribestar: /^https?:\/\/(www\.)?subscribestar\.(com|adult)\/(?!feed$|messages$|library$)[^/?#]+\/?$/i,
|
||||
hentaifoundry: /^https?:\/\/(www\.)?hentai-foundry\.com\/user\/[^/?#]+/i,
|
||||
deviantart: /^https?:\/\/(www\.)?deviantart\.com\/(?!home$|watch\b|tag\b|browse\b)[^/?#]+\/?$/i,
|
||||
pixiv: /^https?:\/\/(www\.)?pixiv\.net\/(en\/)?users\/\d+/i,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "FabledCurator",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
|
||||
|
||||
"browser_specific_settings": {
|
||||
@@ -33,7 +33,6 @@
|
||||
"*://*.hentai-foundry.com/*",
|
||||
"*://*.discord.com/*",
|
||||
"*://*.pixiv.net/*",
|
||||
"*://*.deviantart.com/*",
|
||||
"*://app-api.pixiv.net/*",
|
||||
"*://oauth.secure.pixiv.net/*",
|
||||
"*://*/*"
|
||||
@@ -61,7 +60,6 @@
|
||||
"*://*.subscribestar.com/*",
|
||||
"*://*.subscribestar.adult/*",
|
||||
"*://*.hentai-foundry.com/*",
|
||||
"*://*.deviantart.com/*",
|
||||
"*://*.pixiv.net/*"
|
||||
],
|
||||
"js": ["lib/platforms.js", "content/content-script.js"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fabledcurator-extension",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"private": true,
|
||||
"description": "Firefox extension for FabledCurator",
|
||||
"comment_ignore_files": "The --ignore-files list comes from scripts/packaging.sh, the single source of truth shared with ci.yml's guard and the derived-version patch count. `set -f` is REQUIRED before the substitution: without it the shell globs `test/**` against the working tree and silently narrows the pattern to whatever files happen to exist.",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import { loadLib } from './helpers/loadLib.js'
|
||||
|
||||
const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const manifest = JSON.parse(readFileSync(path.join(EXT_DIR, 'manifest.json'), 'utf8'))
|
||||
|
||||
const { getPlatformFromUrl, isArtistPage, PLATFORMS, PLATFORM_ARTIST_PATTERNS } = loadLib(
|
||||
'platforms.js',
|
||||
['getPlatformFromUrl', 'isArtistPage', 'PLATFORMS', 'PLATFORM_ARTIST_PATTERNS']
|
||||
@@ -13,7 +19,6 @@ describe('getPlatformFromUrl', () => {
|
||||
expect(getPlatformFromUrl('https://www.hentai-foundry.com/user/someone')).toBe('hentaifoundry')
|
||||
expect(getPlatformFromUrl('https://discord.com/channels/@me')).toBe('discord')
|
||||
expect(getPlatformFromUrl('https://www.pixiv.net/en/users/123')).toBe('pixiv')
|
||||
expect(getPlatformFromUrl('https://www.deviantart.com/someone')).toBe('deviantart')
|
||||
})
|
||||
|
||||
it('accepts http as well as https, with or without www', () => {
|
||||
@@ -26,6 +31,15 @@ describe('getPlatformFromUrl', () => {
|
||||
expect(getPlatformFromUrl('https://not-patreon.com/Atole')).toBe(null)
|
||||
expect(getPlatformFromUrl('')).toBe(null)
|
||||
})
|
||||
|
||||
it('returns null for deviantart, retired at #3069', () => {
|
||||
// The 2026-07-05 product decision (FC downloaders = art-dedicated services
|
||||
// only) left deviantart wired for seven weeks. Asserting the negative is
|
||||
// what keeps a partial retirement from being re-completed by accident.
|
||||
expect(getPlatformFromUrl('https://www.deviantart.com/someone')).toBe(null)
|
||||
expect(PLATFORMS.deviantart).toBeUndefined()
|
||||
expect(PLATFORM_ARTIST_PATTERNS.deviantart).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isArtistPage', () => {
|
||||
@@ -72,12 +86,6 @@ describe('isArtistPage', () => {
|
||||
expect(isArtistPage('https://www.pixiv.net/en/artworks/999', 'pixiv')).toBe(false)
|
||||
})
|
||||
|
||||
it('excludes DeviantArt navigation roots', () => {
|
||||
expect(isArtistPage('https://www.deviantart.com/someone', 'deviantart')).toBe(true)
|
||||
expect(isArtistPage('https://www.deviantart.com/home', 'deviantart')).toBe(false)
|
||||
expect(isArtistPage('https://www.deviantart.com/watch', 'deviantart')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for a platform with no artist pattern (discord)', () => {
|
||||
expect(isArtistPage('https://discord.com/channels/@me', 'discord')).toBe(false)
|
||||
})
|
||||
@@ -116,7 +124,6 @@ describe('platform table integrity', () => {
|
||||
patreon: 'https://www.patreon.com/cw/Atole',
|
||||
subscribestar: 'https://subscribestar.adult/someone',
|
||||
hentaifoundry: 'https://www.hentai-foundry.com/user/someone',
|
||||
deviantart: 'https://www.deviantart.com/someone',
|
||||
pixiv: 'https://www.pixiv.net/en/users/12345'
|
||||
}
|
||||
for (const [key, url] of Object.entries(samples)) {
|
||||
@@ -125,3 +132,59 @@ describe('platform table integrity', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('manifest.json agrees with the platform table', () => {
|
||||
// #3069: deviantart was dropped from the product in July but survived in
|
||||
// manifest.json until late August, because NOTHING tied the manifest's
|
||||
// domain lists back to PLATFORMS. These two specs are that tie. Both
|
||||
// directions matter: a stale match ships host access the product decided
|
||||
// not to use, and a missing one silently kills the Add-to-FC button.
|
||||
const matches = manifest.content_scripts[0].matches
|
||||
// '*://*.patreon.com/*' -> '.patreon.com', the form PLATFORMS.domains uses.
|
||||
const hostOf = (m) => m.replace(/^\*:\/\/\*/, '').replace(/\/\*$/, '')
|
||||
|
||||
it('injects the content script only on domains a platform claims', () => {
|
||||
for (const m of matches) {
|
||||
const host = hostOf(m)
|
||||
const owner = Object.entries(PLATFORMS).find(
|
||||
([, p]) => p.domains.includes(host)
|
||||
)
|
||||
expect(owner, `no platform claims content-script match "${m}"`).toBeTruthy()
|
||||
// The content script exists to draw the Add-as-source button, so a
|
||||
// platform with no artist pattern (discord) has no business here.
|
||||
expect(
|
||||
PLATFORM_ARTIST_PATTERNS[owner[0]],
|
||||
`"${m}" injects for ${owner[0]}, which has no artist pattern`
|
||||
).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('injects on every platform that has an artist pattern', () => {
|
||||
const covered = new Set(
|
||||
matches
|
||||
.map(hostOf)
|
||||
.map((h) => Object.entries(PLATFORMS).find(([, p]) => p.domains.includes(h)))
|
||||
.filter(Boolean)
|
||||
.map(([key]) => key)
|
||||
)
|
||||
for (const key of Object.keys(PLATFORM_ARTIST_PATTERNS)) {
|
||||
expect(covered, `${key} has an artist pattern but no content-script match`).toContain(key)
|
||||
}
|
||||
})
|
||||
|
||||
it('requests no host permission for a domain no platform claims', () => {
|
||||
// '*://*/*' is the deliberate exception: FC is self-hosted at an arbitrary
|
||||
// operator-chosen URL, so the extension cannot enumerate its own backend.
|
||||
// Every OTHER entry is a platform domain and must still have an owner.
|
||||
for (const h of manifest.host_permissions) {
|
||||
if (h === '*://*/*') continue
|
||||
const host = hostOf(h)
|
||||
// pixiv's OAuth/API hosts are pixiv infrastructure, not creator pages,
|
||||
// so they are matched by suffix rather than by the domains list.
|
||||
const claimed = Object.values(PLATFORMS).some(
|
||||
(p) => p.domains.includes(host) || p.domains.some((d) => host.endsWith(d))
|
||||
)
|
||||
expect(claimed, `host permission "${h}" belongs to no platform`).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user