diff --git a/src/helpers/avToggle.ts b/src/helpers/avToggle.ts index 3cf958d..9071856 100644 --- a/src/helpers/avToggle.ts +++ b/src/helpers/avToggle.ts @@ -34,3 +34,11 @@ export function setAntivirusEnabled(enabled: boolean): void { } antivirusEnabled = Boolean(enabled); } + +/** + * Useful if env / configuration changes at runtime (tests, hot reload). + */ +export function resetAntivirusEnabledToDefault(): void { + antivirusEnabled = CLAMAV_CONFIGURED ? ANTIVIRUS_ENABLED_DEFAULT : false; +} + diff --git a/src/helpers/erugo.ts b/src/helpers/erugo.ts index b7c6508..fad17d1 100644 --- a/src/helpers/erugo.ts +++ b/src/helpers/erugo.ts @@ -1,92 +1,44 @@ import { - ERUGO_API_TOKEN, ERUGO_BASE_URL, - ERUGO_CONFIGURED, + ERUGO_API_TOKEN, ERUGO_DEFAULT_EXPIRY_HOURS, + ERUGO_CONFIGURED, } from "./env"; -export type ErugoShareOptions = { +/** + * Send a local file to Erugo using Bun FormData. + * If recipient_email is provided, Erugo will send the share link via email + * (same behavior as Erugo UI). + */ +export async function sendFileToErugo(options: { fullPath: string; filename: string; - /** - * Name shown in Erugo UI. If not provided, defaults to filename. - */ + shareName?: string; - /** - * Optional email of the recipient to notify. - */ - recipientEmail?: string; - /** - * Optional description / message. - */ description?: string; - /** - * Optional expiry in hours. Defaults to ERUGO_DEFAULT_EXPIRY_HOURS. - */ + + recipientEmail?: string; + recipientName?: string; + expiryHours?: number; -}; - -export type ErugoShareResponse = Record & { - share_url: string | null; -}; - -function ensureConfigured() { +}) { if (!ERUGO_CONFIGURED) { - throw new Error( - "Erugo is not configured. Set ERUGO_BASE_URL and ERUGO_API_TOKEN.", - ); + throw new Error("Erugo integration is not configured"); } -} - -function getRecord(value: unknown): Record | null { - if (!value || typeof value !== "object" || Array.isArray(value)) return null; - return value as Record; -} - -function getNested(obj: unknown, path: string[]): unknown { - let cur: unknown = obj; - for (const key of path) { - const rec = getRecord(cur); - if (!rec) return undefined; - cur = rec[key]; - } - return cur; -} - -function pickFirstString(values: unknown[]): string | null { - for (const v of values) { - if (typeof v === "string" && v.trim().length > 0) return v; - } - return null; -} - -function safeJsonParse(text: string): unknown { - try { - return JSON.parse(text); - } catch { - return { raw: text }; - } -} - -/** - * Sends a file to Erugo and returns a normalized response that always includes `share_url`. - */ -export async function sendFileToErugo( - options: ErugoShareOptions, -): Promise { - ensureConfigured(); const { fullPath, filename, shareName, - recipientEmail, description, + recipientEmail, + recipientName, expiryHours, } = options; - const base = ERUGO_BASE_URL.replace(/\/$/, ""); - const url = `${base}/api/integrations/convertx/share`; + const url = + `${ERUGO_BASE_URL.replace(/\/$/, "")}` + + `/api/integrations/convertx/share`; const form = new FormData(); @@ -94,54 +46,55 @@ export async function sendFileToErugo( const bunFile = Bun.file(fullPath); form.append("file", bunFile, filename); - // Name + // Share name (Erugo UI uses "name") form.append("name", (shareName?.trim() || filename).toString()); - // Recipient email (if supported by Erugo) - if (recipientEmail && recipientEmail.trim().length > 0) { - // Some Erugo versions may expect "email" or "recipient_email". - // Include both to maximize compatibility. - form.append("email", recipientEmail.trim()); - form.append("recipient_email", recipientEmail.trim()); - } - - // Description / message - if (description && description.trim().length > 0) { + // Optional description + if (description && description.trim()) { form.append("description", description.trim()); - form.append("message", description.trim()); } - // Expiry + // Recipient (THIS is what triggers email in Erugo) + if (recipientEmail && recipientEmail.trim()) { + form.append("recipient_email", recipientEmail.trim()); + + if (recipientName && recipientName.trim()) { + form.append("recipient_name", recipientName.trim()); + } + } + + // Expiry (support both variants used in different Erugo versions) const finalExpiry = - typeof expiryHours === "number" && Number.isFinite(expiryHours) + typeof expiryHours === "number" ? expiryHours : ERUGO_DEFAULT_EXPIRY_HOURS; - // Support multiple field names across Erugo versions form.append("expires_in_hours", String(finalExpiry)); form.append("expiry_hours", String(finalExpiry)); - // Log request + // 🔍 LOG BEFORE REQUEST console.log("[ConvertX] Erugo upload ->", { url, filename, - shareName: shareName?.trim() || filename, - hasRecipient: Boolean(recipientEmail && recipientEmail.trim().length > 0), - hasDescription: Boolean(description && description.trim().length > 0), - expiryHours: finalExpiry, + shareName, + hasRecipient: Boolean(recipientEmail), + recipientMasked: recipientEmail + ? recipientEmail.replace(/(.{2}).+(@.*)/, "$1***$2") + : null, }); const res = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${ERUGO_API_TOKEN}`, + // DO NOT set Content-Type manually for FormData }, body: form, }); const text = await res.text(); - // Log response + // 🔍 LOG RESPONSE console.log("[ConvertX] Erugo response <-", { status: res.status, contentType: res.headers.get("content-type"), @@ -149,26 +102,28 @@ export async function sendFileToErugo( }); if (!res.ok) { - throw new Error(`Erugo share failed: HTTP ${res.status} – ${text.slice(0, 800)}`); + throw new Error( + `Erugo share failed: HTTP ${res.status} – ${text.slice(0, 800)}`, + ); } - const parsed = safeJsonParse(text); - const rec = getRecord(parsed) ?? { value: parsed }; - - const share_url = pickFirstString([ - // common direct fields - getNested(rec, ["share_url"]), - getNested(rec, ["share_link"]), - getNested(rec, ["url"]), - // nested patterns - getNested(rec, ["data", "url"]), - getNested(rec, ["data", "share", "url"]), - getNested(rec, ["data", "share_url"]), - getNested(rec, ["data", "share_link"]), - ]); + let json: any = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + json = { raw: text }; + } return { - ...rec, - share_url, + ...json, + share_url: + json?.share_url || + json?.share_link || + json?.data?.url || + json?.data?.share?.url || + json?.data?.share_url || + json?.data?.share_link || + null, }; } +