refactor(api): collapse single-impl interfaces and facades (#3382)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 23:04:42 +02:00
1 parent e7b13fd19f
commit c16d414d93
426 files changed
+2085 -6445

No files matched your search

@@ -1,23 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {types} from 'cassandra-driver';
type CassandraValue =
| string
| number
| bigint
| boolean
| Buffer
| Date
| types.LocalDate
| Set<unknown>
| Map<unknown, unknown>
| Array<unknown>
| Record<string, unknown>
| null;
export type CassandraParams = Record<string, CassandraValue>;
export interface PreparedQuery<P extends CassandraParams = CassandraParams> {
cql: string;
params: P;
}
+4 -61
View File
@@ -1,7 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {CassandraParams, PreparedQuery} from '@pkgs/cassandra/src/CassandraTypes';
import {type Logger, NoopLogger} from '@pkgs/cassandra/src/Logger';
import cassandra from 'cassandra-driver';
const distance = cassandra.types.distance;
@@ -22,45 +20,19 @@ interface CassandraConfig {
readTimeoutMs?: number | undefined;
}
interface CassandraClientOptions {
logger?: Logger | undefined;
}
interface CassandraExecuteOptions {
prepare?: boolean | undefined;
}
interface CassandraBatchOptions {
prepare?: boolean | undefined;
}
interface ICassandraClient {
connect(): Promise<void>;
shutdown(): Promise<void>;
execute<P extends CassandraParams>(
query: PreparedQuery<P>,
options?: CassandraExecuteOptions,
): Promise<cassandra.types.ResultSet>;
batch(queries: Array<PreparedQuery>, options?: CassandraBatchOptions): Promise<void>;
getNativeClient(): cassandra.Client;
}
interface DefaultClientState {
client: CassandraClient | null;
logger: Logger;
}
const defaultClientState: DefaultClientState = {
client: null,
logger: NoopLogger,
};
class CassandraClient implements ICassandraClient {
class CassandraClient {
private readonly config: CassandraConfig;
private logger: Logger;
private client: cassandra.Client | null;
public constructor(config: CassandraConfig, options: CassandraClientOptions = {}) {
public constructor(config: CassandraConfig) {
this.config = {
hosts: [...config.hosts],
port: config.port,
@@ -70,7 +42,6 @@ class CassandraClient implements ICassandraClient {
password: config.password,
readTimeoutMs: config.readTimeoutMs,
};
this.logger = options.logger ?? NoopLogger;
this.client = null;
}
@@ -109,14 +80,6 @@ class CassandraClient implements ICassandraClient {
});
await client.connect();
this.client = client;
this.logger.info(
{
hosts: this.config.hosts,
keyspace: this.config.keyspace,
local_dc: this.config.localDc,
},
'Connected to Cassandra',
);
}
public async shutdown(): Promise<void> {
@@ -126,26 +89,6 @@ class CassandraClient implements ICassandraClient {
const activeClient = this.client;
this.client = null;
await activeClient.shutdown();
this.logger.info({}, 'Cassandra connection closed');
}
public async execute<P extends CassandraParams>(
query: PreparedQuery<P>,
options: CassandraExecuteOptions = {},
): Promise<cassandra.types.ResultSet> {
return this.getNativeClient().execute(query.cql, query.params, {
prepare: options.prepare ?? true,
});
}
public async batch(queries: Array<PreparedQuery>, options: CassandraBatchOptions = {}): Promise<void> {
if (queries.length === 0) {
return;
}
const batch = queries.map((query) => ({query: query.cql, params: query.params}));
await this.getNativeClient().batch(batch, {
prepare: options.prepare ?? true,
});
}
public getNativeClient(): cassandra.Client {
@@ -160,7 +103,7 @@ export async function initCassandra(config: CassandraConfig): Promise<void> {
if (defaultClientState.client !== null) {
await defaultClientState.client.shutdown();
}
const client = new CassandraClient(config, {logger: defaultClientState.logger});
const client = new CassandraClient(config);
await client.connect();
defaultClientState.client = client;
}
@@ -173,7 +116,7 @@ export async function shutdownCassandra(): Promise<void> {
defaultClientState.client = null;
}
function getDefaultCassandraClient(): ICassandraClient {
function getDefaultCassandraClient(): CassandraClient {
if (defaultClientState.client === null) {
throw new Error('Cassandra client is not initialized. Call initCassandra() first.');
}
-13
View File
@@ -1,13 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface Logger {
info(context: Record<string, unknown>, message: string): void;
debug(context: Record<string, unknown>, message: string): void;
error(context: Record<string, unknown>, message: string): void;
}
export const NoopLogger: Logger = {
info(): void {},
debug(): void {},
error(): void {},
};
@@ -1,26 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {I18nResult} from '@fluxer/i18n/src/runtime/I18nTypes';
import {getEmailTemplate} from '@pkgs/email/src/email_i18n/EmailI18n';
import type {EmailTemplateVariables} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
import type {EmailTemplate, EmailTemplateKey} from '@pkgs/email/src/email_i18n/EmailI18nTypes.generated';
export interface IEmailI18nService {
getTemplate<T extends EmailTemplateKey>(
templateKey: T,
locale: string | null,
variables: EmailTemplateVariables[T],
productName: string,
): I18nResult<EmailTemplateKey, EmailTemplate>;
}
export class EmailI18nService implements IEmailI18nService {
getTemplate<T extends EmailTemplateKey>(
templateKey: T,
locale: string | null,
variables: EmailTemplateVariables[T],
productName: string,
): I18nResult<EmailTemplateKey, EmailTemplate> {
return getEmailTemplate(templateKey, locale, variables, productName);
}
}
+2 -5
View File
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createLogger} from '@fluxer/logger/src/Logger';
import type {IEmailI18nService} from '@pkgs/email/src/EmailI18nService';
import type {EmailConfig, IEmailProvider, UserBouncedEmailChecker} from '@pkgs/email/src/EmailProviderTypes';
import {getEmailTemplate} from '@pkgs/email/src/email_i18n/EmailI18n';
import type {
EmailLegalLinks,
EmailTemplateVariables,
@@ -37,18 +37,15 @@ function legalLinkVariables(config: EmailConfig): {
export class EmailService implements IEmailService {
private readonly config: EmailConfig;
private readonly emailI18n: IEmailI18nService;
private readonly provider: IEmailProvider | null;
private readonly bouncedEmailChecker: UserBouncedEmailChecker | null;
constructor(
config: EmailConfig,
emailI18n: IEmailI18nService,
provider: IEmailProvider | null = null,
bouncedEmailChecker: UserBouncedEmailChecker | null = null,
) {
this.config = config;
this.emailI18n = emailI18n;
this.provider = provider;
this.bouncedEmailChecker = bouncedEmailChecker;
}
@@ -394,7 +391,7 @@ export class EmailService implements IEmailService {
locale: string | null,
variables: EmailTemplateVariables[T],
): Promise<boolean> {
const result = this.emailI18n.getTemplate(templateKey, locale, variables, this.config.productName);
const result = getEmailTemplate(templateKey, locale, variables, this.config.productName);
if (!result.ok) {
logger.error({key: templateKey, locale: result.locale, error: result.error}, 'Failed to resolve email template');
return false;
@@ -1,15 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IEmailService} from '@pkgs/email/src/IEmailService';
export interface SentEmailRecord {
to: string;
type: string;
timestamp: Date;
metadata: Record<string, string>;
}
export interface ITestEmailService extends IEmailService {
listSentEmails(): Array<SentEmailRecord>;
clearSentEmails(): void;
}
@@ -1,7 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {ReportReceivedTargetKind} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
import type {ITestEmailService, SentEmailRecord} from '@pkgs/email/src/ITestEmailService';
import type {IEmailService} from '@pkgs/email/src/IEmailService';
export interface SentEmailRecord {
to: string;
type: string;
timestamp: Date;
metadata: Record<string, string>;
}
function maskToken(token: string): string {
if (token.length <= 8) {
@@ -12,7 +19,7 @@ function maskToken(token: string): string {
const noopLogger = {info: (_message: string) => {}};
export class TestEmailService implements ITestEmailService {
export class TestEmailService implements IEmailService {
private readonly sentEmails: Array<SentEmailRecord> = [];
private readonly logger: {
info: (message: string) => void;
@@ -63,26 +63,6 @@ describe('KVClient cluster hash slots', () => {
store.clear();
});
it('reads several keys without a command spanning hash slots', async () => {
expect(computeHashSlot('slot:alpha')).not.toBe(computeHashSlot('slot:beta'));
const client = createClusteredClient();
await client.set('slot:alpha', 'one');
await expect(client.mget('slot:alpha', 'slot:beta')).resolves.toEqual(['one', null]);
expect(crossSlotCommands()).toEqual([]);
});
it('writes several keys without a command spanning hash slots', async () => {
expect(computeHashSlot('slot:alpha')).not.toBe(computeHashSlot('slot:beta'));
const client = createClusteredClient();
await client.mset('slot:alpha', 'one', 'slot:beta', 'two');
expect(store.get('slot:alpha')).toBe('one');
expect(store.get('slot:beta')).toBe('two');
expect(crossSlotCommands()).toEqual([]);
});
it('deletes several keys without a command spanning hash slots', async () => {
expect(computeHashSlot('slot:alpha')).not.toBe(computeHashSlot('slot:beta'));
const client = createClusteredClient();
@@ -1,8 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {LocaleCode} from '@fluxer/constants/src/Locales';
import {resolveLocaleFromAcceptLanguageHeader} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation';
export function parseAcceptLanguage(acceptLanguageHeader: string | null | undefined): LocaleCode {
return resolveLocaleFromAcceptLanguageHeader(acceptLanguageHeader);
}
@@ -17,7 +17,7 @@ interface AcceptLanguagePreference {
const TRADITIONAL_CHINESE_SUBTAGS: ReadonlySet<string> = new Set(['hant', 'tw', 'hk', 'mo']);
const SIMPLIFIED_CHINESE_SUBTAGS: ReadonlySet<string> = new Set(['hans', 'cn', 'sg']);
export function resolveLocaleFromAcceptLanguageHeader(acceptLanguageHeader: string | null | undefined): LocaleCode {
export function parseAcceptLanguage(acceptLanguageHeader: string | null | undefined): LocaleCode {
if (!acceptLanguageHeader) {
return DEFAULT_LOCALE;
}
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {type LocaleCode, Locales} from '@fluxer/constants/src/Locales';
import {parseAcceptLanguage} from '@pkgs/locale/src/LocaleService';
import {parseAcceptLanguage} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation';
import {describe, expect, it} from 'vitest';
interface AcceptLanguageCase {
@@ -12,18 +12,10 @@
"import": "./src/MediaProxyUtils.ts",
"types": "./src/MediaProxyUtils.ts"
},
"./src/ExternalMediaProxyUrlBuilder": {
"import": "./src/ExternalMediaProxyUrlBuilder.ts",
"types": "./src/ExternalMediaProxyUrlBuilder.ts"
},
"./src/ExternalMediaProxyPathCodec": {
"import": "./src/ExternalMediaProxyPathCodec.ts",
"types": "./src/ExternalMediaProxyPathCodec.ts"
},
"./src/MediaProxySigner": {
"import": "./src/MediaProxySigner.ts",
"types": "./src/MediaProxySigner.ts"
},
"./src/AttachmentUrlSignature": {
"import": "./src/AttachmentUrlSignature.ts",
"types": "./src/AttachmentUrlSignature.ts"
@@ -1,50 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {buildExternalMediaProxyPath} from '@pkgs/media_proxy_utils/src/ExternalMediaProxyPathCodec';
import {createMediaProxySigner, type IMediaProxySigner} from '@pkgs/media_proxy_utils/src/MediaProxySigner';
export interface ExternalMediaProxyUrlBuilderOptions {
mediaProxyEndpoint: string;
mediaProxySecretKey: string;
}
export interface IExternalMediaProxyUrlBuilder {
buildExternalMediaProxyUrl(inputUrl: string): string;
}
interface InternalExternalMediaProxyUrlBuilderOptions {
mediaProxyEndpoint: string;
mediaProxySigner: IMediaProxySigner;
}
function normalizeMediaProxyEndpoint(mediaProxyEndpoint: string): string {
return mediaProxyEndpoint.replace(/\/+$/u, '');
}
class ExternalMediaProxyUrlBuilder implements IExternalMediaProxyUrlBuilder {
private readonly mediaProxyEndpoint: string;
private readonly mediaProxySigner: IMediaProxySigner;
constructor(options: InternalExternalMediaProxyUrlBuilderOptions) {
this.mediaProxyEndpoint = options.mediaProxyEndpoint;
this.mediaProxySigner = options.mediaProxySigner;
}
buildExternalMediaProxyUrl(inputUrl: string): string {
const proxyUrlPath = buildExternalMediaProxyPath(inputUrl);
const signature = this.mediaProxySigner.createSignature(proxyUrlPath);
return `${this.mediaProxyEndpoint}/external/${signature}/${proxyUrlPath}`;
}
}
export function createExternalMediaProxyUrlBuilder(
options: ExternalMediaProxyUrlBuilderOptions,
): IExternalMediaProxyUrlBuilder {
const mediaProxySigner = createMediaProxySigner({
mediaProxySecretKey: options.mediaProxySecretKey,
});
return new ExternalMediaProxyUrlBuilder({
mediaProxyEndpoint: normalizeMediaProxyEndpoint(options.mediaProxyEndpoint),
mediaProxySigner,
});
}
@@ -1,42 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import crypto from 'node:crypto';
const BASE64_URL_PADDING_REGEX = /=*$/;
export interface MediaProxySignerOptions {
mediaProxySecretKey: string;
}
export interface IMediaProxySigner {
createSignature(proxyUrlPath: string): string;
verifySignature(proxyUrlPath: string, providedSignature: string): boolean;
}
class MediaProxySigner implements IMediaProxySigner {
private readonly mediaProxySecretKey: string;
constructor(options: MediaProxySignerOptions) {
this.mediaProxySecretKey = options.mediaProxySecretKey;
}
createSignature(proxyUrlPath: string): string {
const hmac = crypto.createHmac('sha256', this.mediaProxySecretKey);
hmac.update(proxyUrlPath);
return hmac.digest('base64url').replace(BASE64_URL_PADDING_REGEX, '');
}
verifySignature(proxyUrlPath: string, providedSignature: string): boolean {
const expectedSignature = this.createSignature(proxyUrlPath);
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
const providedBuffer = Buffer.from(providedSignature, 'utf8');
if (expectedBuffer.length !== providedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, providedBuffer);
}
}
export function createMediaProxySigner(options: MediaProxySignerOptions): IMediaProxySigner {
return new MediaProxySigner(options);
}
@@ -1,25 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import crypto from 'node:crypto';
import {createExternalMediaProxyUrlBuilder} from '@pkgs/media_proxy_utils/src/ExternalMediaProxyUrlBuilder';
const BASE64_URL_REGEX = /=*$/;
export function createSignature(inputString: string, mediaProxySecretKey: string): string {
const hmac = crypto.createHmac('sha256', mediaProxySecretKey);
hmac.update(inputString);
return hmac.digest('base64url').replace(BASE64_URL_REGEX, '');
}
export function verifySignature(proxyUrlPath: string, providedSignature: string, mediaProxySecretKey: string): boolean {
const expectedSignature = createSignature(proxyUrlPath, mediaProxySecretKey);
const expectedBuffer = Buffer.from(expectedSignature);
const providedBuffer = Buffer.from(providedSignature);
if (expectedBuffer.length !== providedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, providedBuffer);
}
import {buildExternalMediaProxyPath} from '@pkgs/media_proxy_utils/src/ExternalMediaProxyPathCodec';
export interface ExternalMediaProxyURLOptions {
inputURL: string;
@@ -28,9 +10,12 @@ export interface ExternalMediaProxyURLOptions {
}
export function getExternalMediaProxyURL(options: ExternalMediaProxyURLOptions): string {
const builder = createExternalMediaProxyUrlBuilder({
mediaProxyEndpoint: options.mediaProxyEndpoint,
mediaProxySecretKey: options.mediaProxySecretKey,
});
return builder.buildExternalMediaProxyUrl(options.inputURL);
const endpoint = options.mediaProxyEndpoint.replace(/\/+$/u, '');
const proxyUrlPath = buildExternalMediaProxyPath(options.inputURL);
const signature = crypto
.createHmac('sha256', options.mediaProxySecretKey)
.update(proxyUrlPath)
.digest('base64url')
.replace(/=*$/, '');
return `${endpoint}/external/${signature}/${proxyUrlPath}`;
}
-4
View File
@@ -9,10 +9,6 @@
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fluxer/logger": "workspace:*",
"itty-time": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:ts7"
@@ -1,8 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {VirusScanProviderResult} from '@pkgs/virus_scan/src/VirusScanProviderResult';
export interface IVirusScanProvider {
scanFile(filePath: string): Promise<VirusScanProviderResult>;
scanBuffer(buffer: Buffer): Promise<VirusScanProviderResult>;
}
@@ -1,183 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import crypto from 'node:crypto';
import {createReadStream} from 'node:fs';
import path from 'node:path';
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
import type {IVirusHashCache} from '@pkgs/virus_scan/src/cache/IVirusHashCache';
import type {IVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/IVirusScanFailureReporter';
import {NoopVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/NoopVirusScanFailureReporter';
import type {IVirusScanProvider} from '@pkgs/virus_scan/src/IVirusScanProvider';
import type {IVirusScanService} from '@pkgs/virus_scan/src/IVirusScanService';
import type {VirusScanResult} from '@pkgs/virus_scan/src/VirusScanResult';
interface VirusScanConfig {
failOpen: boolean;
cachedThreatLabel?: string;
}
interface VirusScanServiceDependencies {
provider: IVirusScanProvider;
virusHashCache: IVirusHashCache;
logger: LoggerInterface;
config: VirusScanConfig;
failureReporter?: IVirusScanFailureReporter;
}
export class VirusScanService implements IVirusScanService {
readonly enabled = true;
private readonly cachedThreatLabel: string;
private readonly failureReporter: IVirusScanFailureReporter;
constructor(private dependencies: VirusScanServiceDependencies) {
this.cachedThreatLabel = dependencies.config.cachedThreatLabel ?? 'Cached virus signature';
this.failureReporter = dependencies.failureReporter ?? new NoopVirusScanFailureReporter();
}
async initialize(): Promise<void> {
await this.failureReporter.initialize();
}
async scanFile(filePath: string): Promise<VirusScanResult> {
const filename = path.basename(filePath);
const fileHash = await this.hashFile(filePath);
const isCachedVirus = await this.dependencies.virusHashCache.isKnownVirusHash(fileHash);
if (isCachedVirus) {
return {
isClean: false,
threat: this.cachedThreatLabel,
fileHash,
};
}
try {
const scanResult = await this.dependencies.provider.scanFile(filePath);
if (scanResult.isClean) {
return {
isClean: true,
fileHash,
};
}
if (!scanResult.threat) {
throw new Error('Virus scan provider returned infected status without threat name');
}
await this.dependencies.virusHashCache.cacheVirusHash(fileHash);
return {
isClean: false,
threat: scanResult.threat,
fileHash,
};
} catch (error) {
this.dependencies.logger.error(
{
error: this.describeError(error),
filename,
fileHash,
},
'Virus scan failed',
);
await this.reportScanFailure(error, filename, fileHash);
if (this.dependencies.config.failOpen) {
return {
isClean: true,
fileHash,
};
}
throw new Error(`Virus scan failed: ${this.describeError(error)}`);
}
}
async scanBuffer(buffer: Buffer, filename: string): Promise<VirusScanResult> {
const fileHash = this.createFileHashFromBuffer(buffer);
const isCachedVirus = await this.dependencies.virusHashCache.isKnownVirusHash(fileHash);
if (isCachedVirus) {
return {
isClean: false,
threat: this.cachedThreatLabel,
fileHash,
};
}
try {
const scanResult = await this.dependencies.provider.scanBuffer(buffer);
if (scanResult.isClean) {
return {
isClean: true,
fileHash,
};
}
if (!scanResult.threat) {
throw new Error('Virus scan provider returned infected status without threat name');
}
await this.dependencies.virusHashCache.cacheVirusHash(fileHash);
return {
isClean: false,
threat: scanResult.threat,
fileHash,
};
} catch (error) {
this.dependencies.logger.error(
{
error: this.describeError(error),
filename,
fileHash,
},
'Virus scan failed',
);
await this.reportScanFailure(error, filename, fileHash);
if (this.dependencies.config.failOpen) {
return {
isClean: true,
fileHash,
};
}
throw new Error(`Virus scan failed: ${this.describeError(error)}`);
}
}
private createFileHashFromBuffer(buffer: Buffer): string {
return crypto.createHash('sha256').update(buffer).digest('hex');
}
private async hashFile(filePath: string): Promise<string> {
const hash = crypto.createHash('sha256');
const stream = createReadStream(filePath, {highWaterMark: 1024 * 1024});
try {
for await (const chunk of stream) {
hash.update(chunk as Buffer);
}
} catch (error) {
stream.destroy();
throw error;
}
return hash.digest('hex');
}
private describeError(error: unknown): string {
if (typeof error === 'string') {
return error;
}
if (error instanceof Error) {
return error.message;
}
return 'Unknown error';
}
private async reportScanFailure(error: unknown, filename: string, fileHash: string): Promise<void> {
try {
await this.failureReporter.reportFailure({
error,
filename,
fileHash,
failOpen: this.dependencies.config.failOpen,
});
} catch (reportError) {
this.dependencies.logger.warn(
{
error: this.describeError(reportError),
filename,
fileHash,
},
'Failed to report virus scan failure',
);
}
}
}
@@ -1,6 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface IVirusHashCache {
isKnownVirusHash(fileHash: string): Promise<boolean>;
cacheVirusHash(fileHash: string): Promise<void>;
}
@@ -1,6 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface IVirusScanCacheStore {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
}
-36
View File
@@ -1,36 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IVirusHashCache} from '@pkgs/virus_scan/src/cache/IVirusHashCache';
import type {IVirusScanCacheStore} from '@pkgs/virus_scan/src/cache/IVirusScanCacheStore';
import {seconds} from 'itty-time';
interface VirusHashCacheConfig {
keyPrefix?: string;
ttlSeconds?: number;
}
export class VirusHashCache implements IVirusHashCache {
private readonly keyPrefix: string;
private readonly ttlSeconds: number;
constructor(
private cacheStore: IVirusScanCacheStore,
config: VirusHashCacheConfig = {},
) {
this.keyPrefix = config.keyPrefix ?? 'virus';
this.ttlSeconds = config.ttlSeconds ?? seconds('7 days');
}
async isKnownVirusHash(fileHash: string): Promise<boolean> {
const cachedValue = await this.cacheStore.get(this.buildCacheKey(fileHash));
return cachedValue != null;
}
async cacheVirusHash(fileHash: string): Promise<void> {
await this.cacheStore.set(this.buildCacheKey(fileHash), 'true', this.ttlSeconds);
}
private buildCacheKey(fileHash: string): string {
return `${this.keyPrefix}:${fileHash}`;
}
}
@@ -1,8 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {VirusScanFailureContext} from '@pkgs/virus_scan/src/failures/VirusScanFailureContext';
export interface IVirusScanFailureReporter {
initialize(): Promise<void>;
reportFailure(context: VirusScanFailureContext): Promise<void>;
}
@@ -1,10 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/IVirusScanFailureReporter';
import type {VirusScanFailureContext} from '@pkgs/virus_scan/src/failures/VirusScanFailureContext';
export class NoopVirusScanFailureReporter implements IVirusScanFailureReporter {
async initialize(): Promise<void> {}
async reportFailure(_context: VirusScanFailureContext): Promise<void> {}
}
@@ -1,8 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface VirusScanFailureContext {
error: unknown;
filename: string;
fileHash: string;
failOpen: boolean;
}
@@ -3,7 +3,6 @@
import {createReadStream} from 'node:fs';
import {createConnection} from 'node:net';
import {Readable} from 'node:stream';
import type {IVirusScanProvider} from '@pkgs/virus_scan/src/IVirusScanProvider';
import type {VirusScanProviderResult} from '@pkgs/virus_scan/src/VirusScanProviderResult';
interface ClamAVConfig {
@@ -12,7 +11,7 @@ interface ClamAVConfig {
streamChunkBytes?: number;
}
export class ClamAVProvider implements IVirusScanProvider {
export class ClamAVProvider {
private readonly streamChunkBytes: number;
constructor(private config: ClamAVConfig) {
-4
View File
@@ -4,10 +4,6 @@
"private": true,
"type": "module",
"exports": {
"./WorkerContext": {
"import": "./src/context/WorkerContext.ts",
"types": "./src/context/WorkerContext.ts"
},
"./WorkerTask": {
"import": "./src/contracts/WorkerTask.ts",
"types": "./src/contracts/WorkerTask.ts"
@@ -1,18 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
let workerDependencies: unknown | null = null;
export function setWorkerDependencies<T>(dependencies: T): void {
workerDependencies = dependencies;
}
export function getWorkerDependencies<T>(): T {
if (!workerDependencies) {
throw new Error('Worker dependencies have not been initialized. Call setWorkerDependencies() first.');
}
return workerDependencies as T;
}
export function clearWorkerDependencies(): void {
workerDependencies = null;
}