Files
zeekayandHanzo Dev 2c295299ae config: the lists that gate abuse now actually load, from Hanzo KV
@vercel/edge-config read NOTHING in production. EDGE_CONFIG is unset on this
deployment, so every `get()` threw, and every call site caught it and fell back
to an empty default. Several of them additionally checked `process.env.
EDGE_CONFIG` and returned early, so the fallback was not even a fallback — it was
the only path.

What that meant, live:

  emails / keywords  the blacklist matched nothing — abuse filtering was off
  trustedTeams       no team was ever trusted
  betaFeatures       no beta feature was ever on
  customEmail        no team ever got its own sender address

lib/config.ts reads these from Hanzo KV under a `config:` prefix, so a value
changes with a KV write rather than a deploy — which is the reason the app
reached for an edge config service to begin with. Reads never throw: a KV outage
returns the caller's fallback, exactly as these call sites already assumed, so
config can never take down a request path.

customEmail is a Record, not a list, and now says so — it was being read through
the list helper, which would have discarded it.

@vercel/functions and @vercel/edge-config have no importers left and are dropped
from package.json; the lockfile is regenerated and `npm ci --dry-run` is clean.
@vercel/blob still has 10 importers and stays until storage moves to S3.

Verified with npx tsc --noEmit: 100 errors before and after.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:37:48 -07:00

369 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextApiRequest, NextApiResponse } from "next";
import { DocumentPage } from "@prisma/client";
import { getStringList } from "@/lib/config";
import { after } from "@/lib/after";
import * as mupdf from "mupdf";
import { putFileServer } from "@/lib/files/put-file-server";
import prisma from "@/lib/prisma";
import { log } from "@/lib/utils";
// This function can run for a maximum of 120 seconds
export const config = {
maxDuration: 180,
};
export default async (req: NextApiRequest, res: NextApiResponse) => {
// check if post method
if (req.method !== "POST") {
res.status(405).json({ error: "Method Not Allowed" });
return;
}
// Extract the API Key from the Authorization header
const authHeader = req.headers.authorization;
const token = authHeader?.split(" ")[1]; // Assuming the format is "Bearer [token]"
// Check if the API Key matches
if (token !== process.env.INTERNAL_API_KEY) {
res.status(401).json({ message: "Unauthorized" });
return;
}
const { documentVersionId, pageNumber, url, teamId, trustedTeam } =
req.body as {
documentVersionId: string;
pageNumber: number;
url: string;
teamId: string;
trustedTeam?: boolean;
};
try {
// Fetch the PDF data
let response: Response;
try {
response = await fetch(url);
} catch (error) {
log({
message: `Failed to fetch PDF in conversion process with error: \n\n Error: ${error} \n\n \`Metadata: {teamId: ${teamId}, documentVersionId: ${documentVersionId}, pageNumber: ${pageNumber}}\``,
type: "error",
mention: true,
});
throw new Error(`Failed to fetch pdf on document page ${pageNumber}`);
}
// Convert the response to a buffer
const pdfData = await response.arrayBuffer();
// Create a MuPDF instance
var doc = new mupdf.PDFDocument(pdfData);
console.log("Original document size:", pdfData.byteLength);
const page = doc.loadPage(pageNumber - 1); // 0-based page index
// get the bounds of the page for orientation and scaling
const bounds = page.getBounds();
const [ulx, uly, lrx, lry] = bounds;
const widthInPoints = Math.abs(lrx - ulx);
const heightInPoints = Math.abs(lry - uly);
// Validate document dimensions
if (widthInPoints <= 0 || heightInPoints <= 0) {
throw new Error(
`Invalid page dimensions: ${widthInPoints} × ${heightInPoints} points`,
);
}
// Log original dimensions for debugging
console.log(
`Original page dimensions: ${widthInPoints} × ${heightInPoints} points (${(widthInPoints / 72).toFixed(1)}" × ${(heightInPoints / 72).toFixed(1)}")`,
);
if (pageNumber === 1) {
// get the orientation of the document and update document version
const isVertical = heightInPoints > widthInPoints;
await prisma.documentVersion.update({
where: { id: documentVersionId },
data: { isVertical },
});
}
// Calculate optimal scale factor based on document dimensions and memory constraints
const getOptimalScaleFactor = (width: number, height: number): number => {
// Maximum reasonable pixel dimensions to prevent memory issues
const MAX_PIXEL_DIMENSION = 8000;
const MAX_TOTAL_PIXELS = 32_000_000; // ~32MP to stay within memory limits
// Start with default scaling logic
// Note: Avoid scale factor 3 exactly due to mupdf 1.26.4 rendering bug with tiling patterns
let scaleFactor = width >= 1600 ? 2 : 2.95;
// Check if scaled dimensions would exceed limits
const scaledWidth = width * scaleFactor;
const scaledHeight = height * scaleFactor;
const totalPixels = scaledWidth * scaledHeight;
// Reduce scale factor if dimensions are too large
if (
scaledWidth > MAX_PIXEL_DIMENSION ||
scaledHeight > MAX_PIXEL_DIMENSION ||
totalPixels > MAX_TOTAL_PIXELS
) {
// Calculate maximum safe scale factor
const maxScaleByWidth = MAX_PIXEL_DIMENSION / width;
const maxScaleByHeight = MAX_PIXEL_DIMENSION / height;
const maxScaleByTotal = Math.sqrt(MAX_TOTAL_PIXELS / (width * height));
scaleFactor = Math.min(
maxScaleByWidth,
maxScaleByHeight,
maxScaleByTotal,
);
// Ensure minimum scale factor of 1
scaleFactor = Math.max(1, Math.floor(scaleFactor * 10) / 10); // Round down to 1 decimal
console.log(
`Large document detected. Reduced scale factor from ${width >= 1600 ? 2 : 2.95} to ${scaleFactor}`,
);
}
return scaleFactor;
};
const scaleFactor = getOptimalScaleFactor(widthInPoints, heightInPoints);
const doc_to_screen = mupdf.Matrix.scale(scaleFactor, scaleFactor);
console.log("Scale factor:", scaleFactor);
console.log(
"Final dimensions:",
`${widthInPoints * scaleFactor} × ${heightInPoints * scaleFactor}`,
);
// get links
const links = page.getLinks();
const embeddedLinks = links.map((link) => {
const coords = link.getBounds().join(",");
// Check if this is an internal link (GoTo action for TOC, etc.)
if (!link.isExternal()) {
try {
// Resolve internal link to page number (0-indexed from mupdf)
const targetPage = doc.resolveLink(link);
if (targetPage >= 0) {
return {
href: `#page=${targetPage + 1}`, // Convert to 1-indexed for frontend
coords,
isInternal: true,
targetPage: targetPage + 1,
};
}
} catch (e) {
console.log("Failed to resolve internal link:", e);
}
// Fallback for unresolvable internal links
return { href: "", coords, isInternal: true };
}
// External URI link
return { href: link.getURI(), coords, isInternal: false };
});
// Check embedded links for blocked keywords (skip for trusted teams)
if (embeddedLinks.length > 0 && !trustedTeam) {
try {
const keywords = await getStringList("keywords");
if (Array.isArray(keywords) && keywords.length > 0) {
for (const link of embeddedLinks) {
if (link.href) {
const matchedKeyword = keywords.find(
(keyword) =>
typeof keyword === "string" &&
link.href.toLowerCase().includes(keyword.toLowerCase()),
);
if (matchedKeyword) {
after(
log({
message: `Document processing blocked: ${matchedKeyword} \n\n \`Metadata: {teamId: ${teamId}, documentVersionId: ${documentVersionId}, pageNumber: ${pageNumber}}\``,
type: "error",
mention: true,
}),
);
res.status(400).json({
error: "Document processing blocked",
matchedUrl: link.href,
matchedKeyword: matchedKeyword,
pageNumber: pageNumber,
});
return;
}
}
}
}
} catch (error) {
// Log error but continue processing if check fails
console.log("Failed to check keywords:", error);
}
}
// Will be updated if we use a reduced scale factor
let actualScaleFactor = scaleFactor;
const metadata = {
originalWidth: widthInPoints,
originalHeight: heightInPoints,
width: widthInPoints * actualScaleFactor,
height: heightInPoints * actualScaleFactor,
scaleFactor: actualScaleFactor,
};
// Estimate memory usage before creating pixmap
const finalWidth = Math.floor(widthInPoints * scaleFactor);
const finalHeight = Math.floor(heightInPoints * scaleFactor);
const estimatedMemoryMB = (finalWidth * finalHeight * 3) / (1024 * 1024); // RGB = 3 bytes per pixel
console.log(
`Estimated memory usage: ${estimatedMemoryMB.toFixed(1)}MB for ${finalWidth} × ${finalHeight} pixels`,
);
// Warn if memory usage is high
if (estimatedMemoryMB > 200) {
console.warn(
`High memory usage expected: ${estimatedMemoryMB.toFixed(1)}MB. Consider reducing document size.`,
);
}
console.time("toPixmap");
let scaledPixmap;
try {
scaledPixmap = page.toPixmap(
doc_to_screen,
mupdf.ColorSpace.DeviceRGB,
false,
true,
);
} catch (error) {
// If pixmap creation fails, try with a smaller scale factor
console.error(
"Pixmap creation failed, attempting with reduced scale factor:",
error,
);
const reducedScaleFactor = Math.max(1, scaleFactor * 0.5);
console.log(`Retrying with reduced scale factor: ${reducedScaleFactor}`);
const reduced_doc_to_screen = mupdf.Matrix.scale(
reducedScaleFactor,
reducedScaleFactor,
);
scaledPixmap = page.toPixmap(
reduced_doc_to_screen,
mupdf.ColorSpace.DeviceRGB,
false,
true,
);
// Update metadata with actual scale factor used
actualScaleFactor = reducedScaleFactor;
metadata.width = widthInPoints * actualScaleFactor;
metadata.height = heightInPoints * actualScaleFactor;
metadata.scaleFactor = actualScaleFactor;
console.log(
"Successfully created pixmap with reduced scale factor:",
actualScaleFactor,
);
}
console.timeEnd("toPixmap");
console.time("compare");
console.time("asPNG");
const pngBuffer = scaledPixmap.asPNG(); // as PNG
console.timeEnd("asPNG");
console.time("asJPEG");
const jpegBuffer = scaledPixmap.asJPEG(80, false); // as JPEG
console.timeEnd("asJPEG");
const pngSize = pngBuffer.byteLength;
const jpegSize = jpegBuffer.byteLength;
let chosenBuffer;
let chosenFormat;
if (pngSize < jpegSize) {
chosenBuffer = pngBuffer;
chosenFormat = "png";
} else {
chosenBuffer = jpegBuffer;
chosenFormat = "jpeg";
}
console.log("Chosen format:", chosenFormat);
console.timeEnd("compare");
let buffer = Buffer.from(chosenBuffer);
// get docId from url with starts with "doc_" with regex
const match = url.match(/(doc_[^\/]+)\//);
const docId = match ? match[1] : undefined;
const { type, data } = await putFileServer({
file: {
name: `page-${pageNumber}.${chosenFormat}`,
type: `image/${chosenFormat}`,
buffer: buffer,
},
teamId: teamId,
docId: docId,
});
buffer = Buffer.alloc(0); // free memory
chosenBuffer = Buffer.alloc(0); // free memory
scaledPixmap.destroy(); // free memory
page.destroy(); // free memory
if (!data || !type) {
throw new Error(`Failed to upload document page ${pageNumber}`);
}
let documentPage: DocumentPage | null = null;
// Check if a documentPage with the same pageNumber and versionId already exists
const existingPage = await prisma.documentPage.findUnique({
where: {
pageNumber_versionId: {
pageNumber: pageNumber,
versionId: documentVersionId,
},
},
});
if (!existingPage) {
// Only create a new documentPage if it doesn't already exist
documentPage = await prisma.documentPage.create({
data: {
versionId: documentVersionId,
pageNumber: pageNumber,
file: data,
storageType: type,
pageLinks: embeddedLinks,
metadata: metadata,
},
});
} else {
documentPage = existingPage;
}
// Send the images as a response
res.status(200).json({ documentPageId: documentPage.id });
return;
} catch (error) {
log({
message: `Failed to convert page with error: \n\n Error: ${error} \n\n \`Metadata: {teamId: ${teamId}, documentVersionId: ${documentVersionId}, pageNumber: ${pageNumber}}\``,
type: "error",
mention: true,
});
throw error;
}
};