feat(api): purge media through the controller edge api (#3454)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-14 02:12:31 +02:00
1 parent f509bef1fb
commit 6e2bf3cbe6
13 files changed
+248 -22

No files matched your search

+29 -13
View File
@@ -218,6 +218,11 @@ function defaultConfig(): MasterConfig {
token: '',
timeout_ms: 10_000,
},
controller: {
endpoint: '',
token: '',
timeout_ms: 10_000,
},
},
blocklist_feeds: {},
breached_password_check: {},
@@ -484,26 +489,37 @@ function normalizeAppOriginAliases(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);
function validateCachePurgeTarget(
target: {endpoint: string; token: string; timeout_ms: number},
envPrefix: string,
protocols: ReadonlyArray<string>,
): void {
requireString(target.endpoint, `${envPrefix}_ENDPOINT`);
const endpoint = URL.parse(target.endpoint);
if (
endpoint === null ||
(endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') ||
!protocols.includes(endpoint.protocol) ||
endpoint.username !== '' ||
endpoint.password !== ''
) {
throw new Error('FLUXER_CACHE_PURGE_HTTP_ENDPOINT must be an absolute http or https URL without credentials');
const schemes = protocols.map((protocol) => protocol.slice(0, -1)).join(' or ');
throw new Error(`${envPrefix}_ENDPOINT must be an absolute ${schemes} URL without credentials`);
}
requireString(cachePurge.http.token, 'FLUXER_CACHE_PURGE_HTTP_TOKEN');
if (!/^[\x21-\x7e]+$/u.test(cachePurge.http.token)) {
throw new Error('FLUXER_CACHE_PURGE_HTTP_TOKEN must contain only visible ASCII characters');
requireString(target.token, `${envPrefix}_TOKEN`);
if (!/^[\x21-\x7e]+$/u.test(target.token)) {
throw new Error(`${envPrefix}_TOKEN must contain only visible ASCII characters`);
}
assertIntegerInRange(target.timeout_ms, `${envPrefix}_TIMEOUT_MS`, 1_000, 10_000);
}
function validateCachePurgeConfig(config: MasterConfig): void {
const cachePurge = config.integrations.cache_purge;
if (cachePurge.adapter === 'http') {
validateCachePurgeTarget(cachePurge.http, 'FLUXER_CACHE_PURGE_HTTP', ['http:', 'https:']);
}
if (cachePurge.adapter === 'controller') {
validateCachePurgeTarget(cachePurge.controller, 'FLUXER_CACHE_PURGE_CONTROLLER', ['https:']);
}
assertIntegerInRange(cachePurge.http.timeout_ms, 'FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS', 1_000, 10_000);
}
function validateStoreProductSlots(
+6 -1
View File
@@ -5,7 +5,7 @@ import type {DerivedEndpoints} from '@fluxer/config/src/EndpointDerivation';
type RuntimeEnv = 'development' | 'production' | 'test';
type DatabaseBackend = 'postgres' | 'cassandra';
type PublicScheme = 'http' | 'https';
export const CACHE_PURGE_ADAPTER_NAMES = ['none', 'http'] as const;
export const CACHE_PURGE_ADAPTER_NAMES = ['none', 'http', 'controller'] as const;
export type CachePurgeAdapterName = (typeof CACHE_PURGE_ADAPTER_NAMES)[number];
export const STORE_PRODUCT_SLOT_NAMES = ['monthly', 'yearly', 'gift_1_month', 'gift_1_year'] as const;
export type StoreProductSlotName = (typeof STORE_PRODUCT_SLOT_NAMES)[number];
@@ -243,6 +243,11 @@ export interface MasterConfig {
token: string;
timeout_ms: number;
};
controller: {
endpoint: string;
token: string;
timeout_ms: number;
};
};
blocklist_feeds: {
enabled?: boolean;
@@ -603,6 +603,46 @@ describe('ConfigLoader', () => {
}
});
test('rejects the controller cache purge adapter without an endpoint', async () => {
stubMinimalEnv({FLUXER_CACHE_PURGE_ADAPTER: 'controller'});
await expect(loadConfig()).rejects.toThrow('FLUXER_CACHE_PURGE_CONTROLLER_ENDPOINT is required');
});
test('rejects a controller cache purge endpoint that is not https', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'controller',
FLUXER_CACHE_PURGE_CONTROLLER_ENDPOINT: 'http://controller.test/v1/orgs/a/sites/b/purge',
FLUXER_CACHE_PURGE_CONTROLLER_TOKEN: 'pak_test',
});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CACHE_PURGE_CONTROLLER_ENDPOINT must be an absolute https URL without credentials',
);
});
test('rejects the controller cache purge adapter without a token', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'controller',
FLUXER_CACHE_PURGE_CONTROLLER_ENDPOINT: 'https://controller.test/v1/orgs/a/sites/b/purge',
});
await expect(loadConfig()).rejects.toThrow('FLUXER_CACHE_PURGE_CONTROLLER_TOKEN is required');
});
test('accepts a complete controller cache purge configuration', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'controller',
FLUXER_CACHE_PURGE_CONTROLLER_ENDPOINT: 'https://controller.test/v1/orgs/a/sites/b/purge',
FLUXER_CACHE_PURGE_CONTROLLER_TOKEN: 'pak_test',
FLUXER_CACHE_PURGE_CONTROLLER_TIMEOUT_MS: '5000',
});
const cachePurge = (await loadConfig()).integrations.cache_purge;
expect(cachePurge.adapter).toBe('controller');
expect(cachePurge.controller).toEqual({
endpoint: 'https://controller.test/v1/orgs/a/sites/b/purge',
token: 'pak_test',
timeout_ms: 5000,
});
});
test('leaves the cache purge settings unvalidated when the adapter is none', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'ftp://purge.internal/purge',
@@ -239,6 +239,12 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
path: ['integrations', 'cache_purge', 'http', 'timeout_ms'],
parse: parseInteger,
},
FLUXER_CACHE_PURGE_CONTROLLER_ENDPOINT: {path: ['integrations', 'cache_purge', 'controller', 'endpoint']},
FLUXER_CACHE_PURGE_CONTROLLER_TOKEN: {path: ['integrations', 'cache_purge', 'controller', 'token']},
FLUXER_CACHE_PURGE_CONTROLLER_TIMEOUT_MS: {
path: ['integrations', 'cache_purge', 'controller', 'timeout_ms'],
parse: parseInteger,
},
FLUXER_BLOCKLIST_FEEDS_ENABLED: {path: ['integrations', 'blocklist_feeds', 'enabled'], parse: parseBoolean},
FLUXER_BREACHED_PASSWORD_CHECK_ENABLED: {
path: ['integrations', 'breached_password_check', 'enabled'],