import { storage } from "@/server/storage/adapter"; import { ServiceError, type ServiceCtx } from "@/server/services/context"; import { sha256Hex } from "@/server/services/documents/store"; /** * Server-side byte access to stored documents (workers, PDF rendering, import extraction). * * NO visibility/scope check here — this is not a download path. User-facing downloads must go * through `services/documents/access.ts#openDocumentContent` (/files/[documentId]). */ /** Read raw bytes by storage key; null when the object does not exist (or storage is the stub). */ export async function readStoredBytes(storageKey: string): Promise { const content = await storage.get(storageKey); if (!content) return null; const chunks: Uint8Array[] = []; const reader = content.stream.getReader(); for (;;) { const { done, value } = await reader.read(); if (done) break; if (value) chunks.push(value); } return Buffer.concat(chunks); } /** * Load a tenant document's bytes and verify its SHA-256 checksum. * Throws `not_found` (missing/soft-deleted/foreign tenant/no content) or `invalid` (checksum mismatch). */ export async function readDocumentBytes( ctx: ServiceCtx, documentId: string, ): Promise<{ bytes: Buffer; mimeType: string; fileName: string; checksum: string }> { const document = await ctx.db.document.findFirst({ where: { id: documentId, deletedAt: null }, select: { storageKey: true, mimeType: true, fileName: true, checksum: true }, }); if (!document) throw new ServiceError("not_found", "document not found"); // defense in depth: the key must carry the tenant prefix if (!document.storageKey.startsWith(`${ctx.tenantId}/`)) throw new ServiceError("not_found", "document content not available"); const bytes = await readStoredBytes(document.storageKey); if (!bytes) throw new ServiceError("not_found", "document content not available"); if (sha256Hex(bytes) !== document.checksum) throw new ServiceError("invalid", "document checksum mismatch"); return { bytes, mimeType: document.mimeType, fileName: document.fileName, checksum: document.checksum }; }