Files
documenso/packages/lib/server-only/envelope-item/replace-envelope-item-pdf.ts
T
Lucas Smith d5ce222482 feat: add CSC AES/QES signing (v1 instance-wide config) (#2874)
Adds Cloud Signature Consortium (CSC) integration for AES/QES signing
against a configured TSP. v1 ships as instance-wide configuration via
environment variables, with per-envelope signature level selection,
license gating, and an OAuth-driven signing flow (capture + FIFO
signers, SAD session, blocking/in-progress recipient pages).

Includes signature level compatibility checks (role, signing order,
dictate next signer), envelope mutability assertions, Prisma migration
for signature level and CSC tables, and docs for the new signing
certificate options.
2026-06-16 23:37:34 +10:00

231 lines
6.1 KiB
TypeScript

import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { Envelope, Field, Recipient } from '@prisma/client';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
import { convertPlaceholdersToFieldInputs, extractPdfPlaceholders } from '../pdf/auto-place-fields';
import { findRecipientByPlaceholder } from '../pdf/helpers';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
type UnsafeReplaceEnvelopeItemPdfOptions = {
envelope: Pick<Envelope, 'id' | 'type' | 'formValues'>;
/**
* Recipients used to resolve placeholder field assignments.
* When provided and placeholders are found in the replacement PDF,
* fields will be auto-created for matching recipients.
*/
recipients: Recipient[];
/**
* The ID of the envelope item which we will be replacing the PDF for.
*/
envelopeItemId: string;
/**
* The ID of the old document data we will be deleting.
*/
oldDocumentDataId: string;
/**
* The data we will be replacing.
*/
data: {
title?: string;
order?: number;
file: File;
};
user: {
id: number;
name: string | null;
email: string;
};
apiRequestMetadata: ApiRequestMetadata;
};
type UnsafeReplaceEnvelopeItemPdfResult = {
updatedItem: {
id: string;
title: string;
envelopeId: string;
order: number;
documentDataId: string;
};
/**
* The full list of fields for the envelope after the replacement.
*
* Only returned when fields were created or deleted during the replacement,
* otherwise `undefined`.
*/
fields: Field[] | undefined;
};
export const UNSAFE_replaceEnvelopeItemPdf = async ({
envelope,
recipients,
envelopeItemId,
oldDocumentDataId,
data,
user,
apiRequestMetadata,
}: UnsafeReplaceEnvelopeItemPdfOptions): Promise<UnsafeReplaceEnvelopeItemPdfResult> => {
let buffer = Buffer.from(await data.file.arrayBuffer());
if (envelope.formValues) {
buffer = await insertFormValuesInPdf({ pdf: buffer, formValues: envelope.formValues });
}
const normalized = await normalizePdf(buffer, {
flattenForm: envelope.type !== 'TEMPLATE',
});
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
// Upload the new PDF and get a new DocumentData record.
const { documentData: newDocumentData, filePageCount } = await putPdfFileServerSide({
name: data.file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(cleanedPdf),
});
let didFieldsChange = false;
const updatedEnvelopeItem = await prisma.$transaction(async (tx) => {
await assertEnvelopeMutable(envelope, tx);
const updatedItem = await tx.envelopeItem.update({
where: {
id: envelopeItemId,
envelopeId: envelope.id,
},
data: {
documentDataId: newDocumentData.id,
title: data.title,
order: data.order,
},
});
// Todo: Audit log if we're updating the title or order.
// Delete fields that reference pages beyond the new PDF's page count.
const outOfBoundsFields = await tx.field.findMany({
where: {
envelopeId: envelope.id,
envelopeItemId,
page: {
gt: filePageCount,
},
},
select: {
id: true,
},
});
const deletedFieldIds = outOfBoundsFields.map((f) => f.id);
if (deletedFieldIds.length > 0) {
await tx.field.deleteMany({
where: {
id: {
in: deletedFieldIds,
},
},
});
didFieldsChange = true;
}
if (recipients.length > 0 && placeholders.length > 0) {
const orderedRecipients = [...recipients].sort((a, b) => {
const aOrder = a.signingOrder ?? Number.MAX_SAFE_INTEGER;
const bOrder = b.signingOrder ?? Number.MAX_SAFE_INTEGER;
if (aOrder !== bOrder) {
return aOrder - bOrder;
}
return a.id - b.id;
});
const fieldsToCreate = convertPlaceholdersToFieldInputs(
placeholders,
(recipientPlaceholder, placeholder) =>
findRecipientByPlaceholder(recipientPlaceholder, placeholder, orderedRecipients, orderedRecipients),
updatedItem.id,
);
if (fieldsToCreate.length > 0) {
await tx.field.createMany({
data: fieldsToCreate.map((field) => ({
envelopeId: envelope.id,
envelopeItemId: updatedItem.id,
recipientId: field.recipientId,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta || undefined,
})),
});
didFieldsChange = true;
}
}
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_PDF_REPLACED,
envelopeId: envelope.id,
data: {
envelopeItemId: updatedItem.id,
envelopeItemTitle: updatedItem.title,
},
user: {
name: user.name,
email: user.email,
},
requestMetadata: apiRequestMetadata.requestMetadata,
}),
});
return updatedItem;
});
// Delete the old DocumentData (now orphaned).
await prisma.documentData.delete({
where: {
id: oldDocumentDataId,
},
});
let fields: Field[] | undefined;
if (didFieldsChange) {
try {
fields = await prisma.field.findMany({
where: {
envelopeId: envelope.id,
},
});
} catch (err) {
// Do nothing.
console.error(err);
}
}
return {
updatedItem: updatedEnvelopeItem,
fields,
};
};