import { NextRequest } from 'next/server'; import { verifyAgentDownloadToken } from '@/lib/signing/token'; import { db } from '@/lib/db'; import { documents } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import path from 'node:path'; import { readFile } from 'node:fs/promises'; const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); // GET /api/documents/[id]/download?adt=[agentDownloadToken] // Requires: valid agent-download JWT in adt query param (generated server-side in document detail page) // No Auth.js session check at this route — the short-lived JWT IS the credential (same as client download pattern) export async function GET( req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const url = new URL(req.url); const adt = url.searchParams.get('adt'); if (!adt) { return new Response(JSON.stringify({ error: 'Missing download token' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } let documentId: string; try { const verified = await verifyAgentDownloadToken(adt); documentId = verified.documentId; } catch { return new Response(JSON.stringify({ error: 'Download link expired or invalid' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } // Defense in depth: token documentId must match route [id] param if (documentId !== id) { return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' }, }); } const doc = await db.query.documents.findFirst({ where: eq(documents.id, documentId), columns: { id: true, name: true, signedFilePath: true }, }); if (!doc || !doc.signedFilePath) { return new Response(JSON.stringify({ error: 'Signed PDF not found' }), { status: 404, headers: { 'Content-Type': 'application/json' }, }); } // Path traversal guard — required on every file read from uploads/ const absPath = path.join(UPLOADS_DIR, doc.signedFilePath); if (!absPath.startsWith(UPLOADS_DIR)) { return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' }, }); } let fileBuffer: Buffer; try { fileBuffer = await readFile(absPath); } catch { return new Response(JSON.stringify({ error: 'File not found on disk' }), { status: 404, headers: { 'Content-Type': 'application/json' }, }); } const safeName = doc.name.replace(/[^a-zA-Z0-9-_ ]/g, ''); return new Response(new Uint8Array(fileBuffer), { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="${safeName}_signed.pdf"`, }, }); }