refactor(api): drop Bunny for pluggable cache purge adapters (#2742)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-13 20:28:56 +02:00
1 parent 84dcf6b8a8
commit ca7ddd9272
45 files changed
+929 -977

No files matched your search

+31 -5
View File
@@ -11,7 +11,7 @@ import {
parsePublicOrigin,
parseWebOrigin,
} from '@fluxer/config/src/EndpointDerivation';
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
import {CACHE_PURGE_ADAPTER_NAMES, type MasterConfig} from '@fluxer/config/src/MasterConfig';
let cachedConfig: MasterConfig | null = null;
@@ -235,10 +235,13 @@ function defaultConfig(): MasterConfig {
youtube: {
api_key: '',
},
bunny: {
purge_enabled: false,
api_key: '',
pull_zone_id: 0,
cache_purge: {
adapter: 'none',
http: {
endpoint: '',
token: '',
timeout_ms: 10_000,
},
},
blocklist_feeds: {},
risk_integration: {
@@ -449,6 +452,27 @@ function validateApiWorkerConfig(config: MasterConfig): void {
}
}
function validateCachePurgeConfig(config: MasterConfig): void {
const cachePurge = config.integrations.cache_purge;
if (cachePurge.adapter !== 'http') {
return;
}
requireString(cachePurge.http.endpoint, 'FLUXER_CACHE_PURGE_HTTP_ENDPOINT');
const endpoint = URL.parse(cachePurge.http.endpoint);
if (
endpoint === null ||
(endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') ||
endpoint.username !== '' ||
endpoint.password !== ''
) {
throw new Error('FLUXER_CACHE_PURGE_HTTP_ENDPOINT must be an absolute http or https URL without credentials');
}
if (!/^[\x21-\x7e]*$/u.test(cachePurge.http.token)) {
throw new Error('FLUXER_CACHE_PURGE_HTTP_TOKEN must contain only visible ASCII characters');
}
assertIntegerInRange(cachePurge.http.timeout_ms, 'FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS', 1_000, 10_000);
}
function validateDomain(value: string, envName: string): void {
if (value === '') return;
const parsed = URL.parse(`http://${value}/`);
@@ -507,6 +531,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
assertOneOf(config.integrations.email.provider, ['smtp', 'none'], 'FLUXER_EMAIL_PROVIDER');
assertOneOf(config.integrations.captcha.provider, ['hcaptcha', 'turnstile', 'none'], 'FLUXER_CAPTCHA_PROVIDER');
assertOneOf(config.integrations.search.engine, ['elasticsearch', 'meilisearch'], 'FLUXER_SEARCH_ENGINE');
assertOneOf(config.integrations.cache_purge.adapter, CACHE_PURGE_ADAPTER_NAMES, 'FLUXER_CACHE_PURGE_ADAPTER');
assertOneOf(
config.instance.abuse_policy.direct_contact_spam.action,
['flag_spammer', 'suppress_delivery'],
@@ -515,6 +540,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
validatePostgresConfig(config);
validateCaptchaConfig(config);
validateApiWorkerConfig(config);
validateCachePurgeConfig(config);
assertIntegerInRange(config.services.api.max_inflight_requests, 'FLUXER_API_MAX_INFLIGHT_REQUESTS', 1, 100_000);
assertIntegerInRange(config.services.api.headers_timeout_ms, 'FLUXER_API_HEADERS_TIMEOUT_MS', 1_000, 3_600_000);
assertIntegerInRange(config.services.api.request_timeout_ms, 'FLUXER_API_REQUEST_TIMEOUT_MS', 1_000, 3_600_000);
+9 -4
View File
@@ -5,6 +5,8 @@ import type {DerivedEndpoints} from '@fluxer/config/src/EndpointDerivation';
export type RuntimeEnv = 'development' | 'production' | 'test';
export type DatabaseBackend = 'postgres' | 'cassandra';
export type PublicScheme = 'http' | 'https';
export const CACHE_PURGE_ADAPTER_NAMES = ['none', 'http'] as const;
export type CachePurgeAdapterName = (typeof CACHE_PURGE_ADAPTER_NAMES)[number];
export interface InstanceBrandingConfig {
product_name: string;
@@ -274,10 +276,13 @@ export interface MasterConfig {
youtube: {
api_key: string;
};
bunny: {
purge_enabled: boolean;
api_key: string;
pull_zone_id: number;
cache_purge: {
adapter: CachePurgeAdapterName;
http: {
endpoint: string;
token: string;
timeout_ms: number;
};
};
blocklist_feeds: {
enabled?: boolean;
@@ -513,6 +513,90 @@ describe('ConfigLoader', () => {
expect((await loadConfig()).integrations.captcha.enabled).toBe(false);
});
test('defaults the cache purge adapter to none', async () => {
stubMinimalEnv();
expect((await loadConfig()).integrations.cache_purge).toEqual({
adapter: 'none',
http: {endpoint: '', token: '', timeout_ms: 10_000},
});
});
test('reads the http cache purge settings from the environment', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TOKEN: 'purge-token',
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: '5000',
});
expect((await loadConfig()).integrations.cache_purge).toEqual({
adapter: 'http',
http: {endpoint: 'https://purge.internal/purge', token: 'purge-token', timeout_ms: 5000},
});
});
test('rejects an unknown cache purge adapter', async () => {
stubMinimalEnv({FLUXER_CACHE_PURGE_ADAPTER: 'varnish'});
await expect(loadConfig()).rejects.toThrow('Invalid FLUXER_CACHE_PURGE_ADAPTER: varnish');
});
test('rejects the http cache purge adapter without an endpoint', async () => {
stubMinimalEnv({FLUXER_CACHE_PURGE_ADAPTER: 'http'});
await expect(loadConfig()).rejects.toThrow('FLUXER_CACHE_PURGE_HTTP_ENDPOINT is required');
});
test('rejects a cache purge endpoint that is not an absolute http URL', async () => {
for (const endpoint of ['/purge', 'purge.internal/purge', 'ftp://purge.internal/purge']) {
stubMinimalEnv({FLUXER_CACHE_PURGE_ADAPTER: 'http', FLUXER_CACHE_PURGE_HTTP_ENDPOINT: endpoint});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CACHE_PURGE_HTTP_ENDPOINT must be an absolute http or https URL without credentials',
);
}
});
test('rejects a cache purge endpoint that carries credentials', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purger:[email protected]/purge',
});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CACHE_PURGE_HTTP_ENDPOINT must be an absolute http or https URL without credentials',
);
});
test('rejects a cache purge timeout outside 1000 to 10000', async () => {
for (const timeout of ['999', '10001']) {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: timeout,
});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS must be an integer between 1000 and 10000',
);
}
});
test('rejects a cache purge token with spaces or control characters without echoing it', async () => {
for (const token of ['secret\nvalue', 'secret\rvalue', 'secret value', 'secret-value\n']) {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TOKEN: token,
});
await expect(loadConfig()).rejects.toThrow(
/^FLUXER_CACHE_PURGE_HTTP_TOKEN must contain only visible ASCII characters$/,
);
}
});
test('leaves the cache purge settings unvalidated when the adapter is none', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'ftp://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: '1',
});
expect((await loadConfig()).integrations.cache_purge.adapter).toBe('none');
});
test('leaves Bluesky login off with no legal URLs by default', async () => {
stubMinimalEnv();
@@ -318,10 +318,14 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_CLAMAV_FAIL_OPEN: {path: ['integrations', 'clamav', 'fail_open'], parse: parseBoolean},
FLUXER_KLIPY_API_KEY: {path: ['integrations', 'klipy', 'api_key']},
FLUXER_YOUTUBE_API_KEY: {path: ['integrations', 'youtube', 'api_key']},
FLUXER_BUNNY_PURGE_ENABLED: {path: ['integrations', 'bunny', 'purge_enabled'], parse: parseBoolean},
FLUXER_CACHE_PURGE_ADAPTER: {path: ['integrations', 'cache_purge', 'adapter']},
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: {path: ['integrations', 'cache_purge', 'http', 'endpoint']},
FLUXER_CACHE_PURGE_HTTP_TOKEN: {path: ['integrations', 'cache_purge', 'http', 'token']},
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: {
path: ['integrations', 'cache_purge', 'http', 'timeout_ms'],
parse: parseInteger,
},
FLUXER_BLOCKLIST_FEEDS_ENABLED: {path: ['integrations', 'blocklist_feeds', 'enabled'], parse: parseBoolean},
FLUXER_BUNNY_API_KEY: {path: ['integrations', 'bunny', 'api_key']},
FLUXER_BUNNY_PULL_ZONE_ID: {path: ['integrations', 'bunny', 'pull_zone_id'], parse: parseInteger},
FLUXER_RISK_INTEGRATION_ENABLED: {path: ['integrations', 'risk_integration', 'enabled'], parse: parseBoolean},
FLUXER_RISK_IPINFO_API_KEY: {path: ['integrations', 'risk_integration', 'ipinfo_api_key']},
FLUXER_ACCOUNT_POLICY_DSL: {