// Producer-side helper for the toolctrl.file-scan BullMQ queue. Web routes // call enqueueFileScan(...) after a successful upload to trigger the // asynchronous post-upload ClamAV scan in the worker. Mirrors the // TransactionalEmailQueue.server.ts shape so the queue lifecycle is the // same shared pattern. import { buildFileScanJob, enqueueJob, queueNames, resolveRedisConnectionOptions, resolveRedisConnectionOptionsFromProcessEnv, type RedisConnectionEnv, type TypedQueue, } from "@toolctrl/queue"; export type EnqueueFileScanInput = { bucket: string; key: string; tenantId?: string; uploadId?: string; contentType?: string; // AK-P4a §5 — File row id so the worker can write the verdict back onto // File.scanStatus (clean|infected). See enqueueFileScan callers. fileId?: string; metadata?: Record; }; export type EnqueueFileScanResult = | { enqueued: true; jobId?: string } | { enqueued: false; reason: "scan-disabled" | "queue-unavailable" }; type Dependencies = { env?: RedisConnectionEnv & { SCAN_QUEUE_ENABLED?: string }; queue?: TypedQueue; }; export async function enqueueFileScan(input: EnqueueFileScanInput, dependencies: Dependencies = {}): Promise { const env = dependencies.env ?? (process.env as RedisConnectionEnv & { SCAN_QUEUE_ENABLED?: string }); if (env.SCAN_QUEUE_ENABLED !== "true") { return { enqueued: false, reason: "scan-disabled" }; } let queue = dependencies.queue; let shouldCloseQueue = false; const connection = queue ? undefined : dependencies.env ? resolveRedisConnectionOptions({ env: dependencies.env }) : resolveRedisConnectionOptionsFromProcessEnv(); if (!queue && !connection) { return { enqueued: false, reason: "queue-unavailable" }; } if (!queue && connection) { const queueRuntimeModule = "@toolctrl/queue/runtime"; const { createQueue } = (await import(/* @vite-ignore */ queueRuntimeModule)) as typeof import("@toolctrl/queue/runtime"); queue = createQueue(queueNames.fileScan, { connection }); shouldCloseQueue = true; } if (!queue) { return { enqueued: false, reason: "queue-unavailable" }; } try { const descriptor = buildFileScanJob({ bucket: input.bucket, key: input.key, tenantId: input.tenantId, uploadId: input.uploadId, contentType: input.contentType, fileId: input.fileId, metadata: input.metadata, }); const job = await enqueueJob(queue, descriptor, { ...(input.tenantId !== undefined ? { tenantId: input.tenantId } : {}), }); return { enqueued: true, jobId: typeof job.id === "string" ? job.id : undefined }; } finally { if (shouldCloseQueue && queue) { await queue.close().catch(() => {}); } } }