chore: merge main, resolve biome formatting conflicts

Merge origin/main into feat/document-file-conversion. Conflicts were
format-only (Tailwind class ordering, single-line vs multi-line) plus
two semantic merges:

- files.helpers.ts: combine main's pending-PDF download path with the
  branch's original-source-file (DOCX/PNG/JPEG) download path
- download-pdf.ts: combine main's versionToFilenameSuffix helper with
  the branch's server-provided Content-Disposition filename support
This commit is contained in:
ephraimduncan
2026-05-12 11:28:47 +00:00
1495 changed files with 22068 additions and 33465 deletions
+173 -32
View File
@@ -1,47 +1,92 @@
import {
type DocumentDataType,
DocumentStatus,
type EnvelopeType,
type TemplateType,
} from '@prisma/client';
import { EnvelopeType as EnvelopeTypeEnum, TemplateType as TemplateTypeEnum } from '@prisma/client';
import contentDisposition from 'content-disposition';
import { type Context } from 'hono';
import { getFileExtensionForMimeType } from '@documenso/lib/constants/upload';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { generatePartialSignedPdf } from '@documenso/lib/server-only/pdf/generate-partial-signed-pdf';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { prisma } from '@documenso/prisma';
import {
type DocumentDataType,
DocumentStatus,
type EnvelopeType,
EnvelopeType as EnvelopeTypeEnum,
type RecipientRole,
type SigningStatus,
type TemplateType,
TemplateType as TemplateTypeEnum,
} from '@prisma/client';
import contentDisposition from 'content-disposition';
import type { Context } from 'hono';
import { match } from 'ts-pattern';
import type { HonoEnv } from '../../router';
type HandleEnvelopeItemFileRequestOptions = {
title: string;
type DocumentDataInput = {
type: DocumentDataType;
data: string;
initialData: string;
originalData?: string | null;
originalMimeType?: string | null;
};
type EnvelopeForPendingDownload = {
id: string;
status: DocumentStatus;
documentData: {
type: DocumentDataType;
data: string;
initialData: string;
originalData?: string | null;
originalMimeType?: string | null;
};
version: 'signed' | 'original';
isDownload: boolean;
context: Context<HonoEnv>;
internalVersion: number;
recipients: Array<{
role: RecipientRole;
signingStatus: SigningStatus;
}>;
};
/**
* Helper function to handle envelope item file requests (both view and download)
* Options shape varies by `version`:
* - `signed` / `original`: serves stored bytes; only needs envelope `status` for cache headers.
* - `pending`: generates a fresh PDF with currently-inserted fields burned in; needs the
* full envelope (id, status, internalVersion, recipients) plus envelopeItemId to query fields.
*/
export const handleEnvelopeItemFileRequest = async ({
type HandleEnvelopeItemFileRequestOptions = {
title: string;
documentData: DocumentDataInput;
isDownload: boolean;
context: Context<HonoEnv>;
} & (
| {
version: 'signed' | 'original';
status: DocumentStatus;
}
| {
version: 'pending';
envelopeItemId: string;
envelope: EnvelopeForPendingDownload;
}
);
/**
* Single entry point for envelope item file requests (view and download).
*
* Dispatches on `version`:
* - `signed` / `original`: returns the stored PDF bytes as-is.
* - `pending`: generates an on-demand PDF with all currently-inserted fields burned in.
*/
export const handleEnvelopeItemFileRequest = async (options: HandleEnvelopeItemFileRequestOptions) => {
if (options.version === 'pending') {
return handlePendingFileRequest(options);
}
return handleStaticFileRequest(options);
};
type StaticFileRequestOptions = Extract<HandleEnvelopeItemFileRequestOptions, { version: 'signed' | 'original' }>;
const handleStaticFileRequest = async ({
title,
status,
documentData,
version,
isDownload,
context: c,
}: HandleEnvelopeItemFileRequestOptions) => {
}: StaticFileRequestOptions) => {
const shouldServeOriginalSourceFile =
version === 'original' &&
documentData.originalData &&
@@ -54,9 +99,7 @@ export const handleEnvelopeItemFileRequest = async ({
? documentData.data
: documentData.initialData;
const contentType = shouldServeOriginalSourceFile
? documentData.originalMimeType!
: 'application/pdf';
const contentType = shouldServeOriginalSourceFile ? documentData.originalMimeType! : 'application/pdf';
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
@@ -112,6 +155,107 @@ export const handleEnvelopeItemFileRequest = async ({
return c.body(file);
};
type PendingFileRequestOptions = Extract<HandleEnvelopeItemFileRequestOptions, { version: 'pending' }>;
const handlePendingFileRequest = async ({
title,
envelopeItemId,
envelope,
documentData,
context: c,
}: PendingFileRequestOptions) => {
if (envelope.status !== DocumentStatus.PENDING) {
const errorCode = match(envelope.status)
.with(DocumentStatus.DRAFT, () => AppErrorCode.ENVELOPE_DRAFT)
.with(DocumentStatus.COMPLETED, () => AppErrorCode.ENVELOPE_COMPLETED)
.with(DocumentStatus.REJECTED, () => AppErrorCode.ENVELOPE_REJECTED)
.otherwise(() => AppErrorCode.INVALID_REQUEST);
throw new AppError(errorCode, {
message: `Envelope ${envelope.id} must be pending to download a partially signed PDF`,
statusCode: 400,
});
}
if (envelope.internalVersion !== 2) {
throw new AppError(AppErrorCode.ENVELOPE_LEGACY, {
message: `Envelope ${envelope.id} is a legacy envelope and does not support partially signed PDF downloads`,
statusCode: 400,
});
}
const fields = await prisma.field.findMany({
where: {
envelopeItemId,
inserted: true,
},
include: {
signature: true,
},
orderBy: {
id: 'asc',
},
});
const etag = Buffer.from(
sha256(
JSON.stringify({
envelopeStatus: envelope.status,
fields: fields.map((field) => ({
id: field.id,
customText: field.customText,
signatureId: field.signature?.id ?? null,
signatureCreated: field.signature?.created ?? null,
})),
}),
),
).toString('hex');
if (c.req.header('If-None-Match') === etag) {
c.header('ETag', etag);
c.header('Cache-Control', 'no-store, private');
return c.body(null, 304);
}
const file = await getFileServerSide({
type: documentData.type,
data: documentData.initialData,
}).catch((error) => {
console.error(error);
return null;
});
if (!file) {
return c.json({ error: 'File not found' }, 404);
}
const pdf = await generatePartialSignedPdf({
pdfData: file,
fields,
});
c.get('logger').info({
source: 'pendingPdfDownload',
envelopeId: envelope.id,
envelopeItemId,
insertedFieldCount: fields.length,
etag,
});
c.header('Content-Type', 'application/pdf');
c.header('Cache-Control', 'no-store, private');
c.header('ETag', etag);
const baseTitle = title.replace(/\.pdf$/i, '');
const filename = `${baseTitle}_pending.pdf`;
c.header('Content-Disposition', contentDisposition(filename));
return c.body(pdf);
};
type CheckEnvelopeFileAccessOptions = {
userId: number;
teamId: number;
@@ -138,10 +282,7 @@ export const checkEnvelopeFileAccess = async ({
return true;
}
if (
envelopeType === EnvelopeTypeEnum.TEMPLATE &&
templateType === TemplateTypeEnum.ORGANISATION
) {
if (envelopeType === EnvelopeTypeEnum.TEMPLATE && templateType === TemplateTypeEnum.ORGANISATION) {
const orgAccess = await prisma.team.findFirst({
where: {
id: teamId,
+91 -60
View File
@@ -1,7 +1,3 @@
import { sValidator } from '@hono/standard-validator';
import type { Prisma } from '@prisma/client';
import { Hono } from 'hono';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@@ -9,6 +5,9 @@ import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embeddin
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { prisma } from '@documenso/prisma';
import { sValidator } from '@hono/standard-validator';
import type { Prisma } from '@prisma/client';
import { Hono } from 'hono';
import type { HonoEnv } from '../../router';
import { checkEnvelopeFileAccess, handleEnvelopeItemFileRequest } from './files.helpers';
@@ -126,10 +125,7 @@ export const filesRoute = new Hono<HonoEnv>()
});
if (!hasAccess) {
return c.json(
{ error: 'User does not have access to the team that this envelope is associated with' },
403,
);
return c.json({ error: 'User does not have access to the team that this envelope is associated with' }, 403);
}
if (!envelopeItem.documentData) {
@@ -150,66 +146,101 @@ export const filesRoute = new Hono<HonoEnv>()
'/envelope/:envelopeId/envelopeItem/:envelopeItemId/download/:version?',
sValidator('param', ZGetEnvelopeItemFileDownloadRequestParamsSchema),
async (c) => {
const { envelopeId, envelopeItemId, version } = c.req.valid('param');
const logger = c.get('logger');
const session = await getOptionalSession(c);
try {
const { envelopeId, envelopeItemId, version } = c.req.valid('param');
if (!session.user) {
return c.json({ error: 'Unauthorized' }, 401);
}
const session = await getOptionalSession(c);
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
},
include: {
envelopeItems: {
where: {
id: envelopeItemId,
if (!session.user) {
return c.json({ error: 'Unauthorized' }, 401);
}
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
},
include: {
envelopeItems: {
where: {
id: envelopeItemId,
},
include: {
documentData: true,
},
},
include: {
documentData: true,
recipients: {
select: {
role: true,
signingStatus: true,
},
},
},
},
});
});
if (!envelope) {
return c.json({ error: 'Envelope not found' }, 404);
if (!envelope) {
return c.json({ error: 'Envelope not found' }, 404);
}
const [envelopeItem] = envelope.envelopeItems;
if (!envelopeItem) {
return c.json({ error: 'Envelope item not found' }, 404);
}
const hasDownloadAccess = await checkEnvelopeFileAccess({
userId: session.user.id,
teamId: envelope.teamId,
envelopeType: envelope.type,
templateType: envelope.templateType,
});
if (!hasDownloadAccess) {
return c.json(
{
error: 'User does not have access to the team that this envelope is associated with',
},
403,
);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
const baseOptions = {
title: envelopeItem.title,
documentData: envelopeItem.documentData,
isDownload: true,
context: c,
} as const;
if (version === 'pending') {
return await handleEnvelopeItemFileRequest({
...baseOptions,
version,
envelopeItemId: envelopeItem.id,
envelope,
});
}
return await handleEnvelopeItemFileRequest({
...baseOptions,
version,
status: envelope.status,
});
} catch (error) {
logger.error(error);
if (error instanceof AppError) {
const { status, body } = AppError.toRestAPIError(error);
return c.json({ error: body.message, code: error.code }, status);
}
return c.json({ error: 'Internal server error' }, 500);
}
const [envelopeItem] = envelope.envelopeItems;
if (!envelopeItem) {
return c.json({ error: 'Envelope item not found' }, 404);
}
const hasDownloadAccess = await checkEnvelopeFileAccess({
userId: session.user.id,
teamId: envelope.teamId,
envelopeType: envelope.type,
templateType: envelope.templateType,
});
if (!hasDownloadAccess) {
return c.json(
{ error: 'User does not have access to the team that this envelope is associated with' },
403,
);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
return await handleEnvelopeItemFileRequest({
title: envelopeItem.title,
status: envelope.status,
documentData: envelopeItem.documentData,
version,
isDownload: true,
context: c,
});
},
)
.get(
+6 -15
View File
@@ -1,6 +1,5 @@
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { z } from 'zod';
export const ZUploadPdfRequestSchema = z.object({
file: z.instanceof(File),
@@ -32,36 +31,28 @@ export const ZGetEnvelopeItemFileRequestParamsSchema = z.object({
envelopeItemId: z.string().min(1),
});
export type TGetEnvelopeItemFileRequestParams = z.infer<
typeof ZGetEnvelopeItemFileRequestParamsSchema
>;
export type TGetEnvelopeItemFileRequestParams = z.infer<typeof ZGetEnvelopeItemFileRequestParamsSchema>;
export const ZGetEnvelopeItemFileRequestQuerySchema = z.object({
token: z.string().optional(),
});
export type TGetEnvelopeItemFileRequestQuery = z.infer<
typeof ZGetEnvelopeItemFileRequestQuerySchema
>;
export type TGetEnvelopeItemFileRequestQuery = z.infer<typeof ZGetEnvelopeItemFileRequestQuerySchema>;
export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({
token: z.string().min(1),
envelopeItemId: z.string().min(1),
});
export type TGetEnvelopeItemFileTokenRequestParams = z.infer<
typeof ZGetEnvelopeItemFileTokenRequestParamsSchema
>;
export type TGetEnvelopeItemFileTokenRequestParams = z.infer<typeof ZGetEnvelopeItemFileTokenRequestParamsSchema>;
export const ZGetEnvelopeItemFileDownloadRequestParamsSchema = z.object({
envelopeId: z.string().min(1),
envelopeItemId: z.string().min(1),
version: z.enum(['signed', 'original']).default('signed'),
version: z.enum(['signed', 'original', 'pending']).default('signed'),
});
export type TGetEnvelopeItemFileDownloadRequestParams = z.infer<
typeof ZGetEnvelopeItemFileDownloadRequestParamsSchema
>;
export type TGetEnvelopeItemFileDownloadRequestParams = z.infer<typeof ZGetEnvelopeItemFileDownloadRequestParamsSchema>;
export const ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema = z.object({
token: z.string().min(1),
@@ -1,10 +1,9 @@
import { prisma } from '@documenso/prisma';
import { sValidator } from '@hono/standard-validator';
import type { Prisma } from '@prisma/client';
import { Hono } from 'hono';
import { z } from 'zod';
import { prisma } from '@documenso/prisma';
import type { HonoEnv } from '../../../router';
import { handleEnvelopeItemPdfRequest } from './get-envelope-item-pdf';
@@ -1,14 +1,13 @@
import { sValidator } from '@hono/standard-validator';
import type { DocumentData, EnvelopeItem } from '@prisma/client';
import { type Context, Hono } from 'hono';
import { z } from 'zod';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import type { DocumentDataVersion } from '@documenso/lib/types/document';
import { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { prisma } from '@documenso/prisma';
import { sValidator } from '@hono/standard-validator';
import type { DocumentData, EnvelopeItem } from '@prisma/client';
import { type Context, Hono } from 'hono';
import { z } from 'zod';
import type { HonoEnv } from '../../../router';
import { checkEnvelopeFileAccess } from '../files.helpers';