mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
refactor(api): drop Bunny for pluggable cache purge adapters (#2742)
This commit is contained in:
1 parent
84dcf6b8a8
commit
ca7ddd9272
45 files changed
+929
-977
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user