Files
documenso/packages/lib/utils/document.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

153 lines
6.0 KiB
TypeScript

import type { DocumentMeta, Envelope, OrganisationGlobalSettings, Recipient, Team, User } from '@prisma/client';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
import { resolveSigningOrder } from '../server-only/signature-level/resolve-signing-order';
import type { TDocumentLite, TDocumentMany } from '../types/document';
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
import { SignatureLevel } from '../types/signature-level';
import { mapSecondaryIdToDocumentId } from './envelope';
import { mapRecipientToLegacyRecipient } from './recipients';
export const isDocumentCompleted = (document: Pick<Envelope, 'status'> | DocumentStatus) => {
const status = typeof document === 'string' ? document : document.status;
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
};
/**
* Extracts the derived document meta which should be used when creating a document
* from scratch, or from a template.
*
* Uses the following, the lower number overrides the higher number:
* 1. Merged organisation/team settings
* 2. Meta overrides
*
* @param settings - The merged organisation/team settings.
* @param overrideMeta - The meta to override the settings with.
* @param signatureLevel - The envelope's signature level. Optional; defaults
* to `SES` for backward compatibility, which preserves the legacy `PARALLEL`
* signing-order default. New callers should pass the resolved level so the
* TSP envelopes get the `SEQUENTIAL` default + assertion against explicit
* `PARALLEL`.
* @returns The derived document meta.
*/
export const extractDerivedDocumentMeta = (
settings: Omit<OrganisationGlobalSettings, 'id'>,
overrideMeta: Partial<DocumentMeta> | undefined | null,
signatureLevel: string = SignatureLevel.SES,
) => {
const meta = overrideMeta ?? {};
// Note: If you update this you will also need to update `create-document-from-template.ts`
// since there is custom work there which allows 3 overrides.
return {
language: meta.language || settings.documentLanguage,
timezone: meta.timezone || settings.documentTimezone || DEFAULT_DOCUMENT_TIME_ZONE,
dateFormat: meta.dateFormat || settings.documentDateFormat,
message: meta.message || null,
subject: meta.subject || null,
redirectUrl: meta.redirectUrl || null,
signingOrder: resolveSigningOrder({ signatureLevel, requested: meta.signingOrder }),
allowDictateNextSigner: meta.allowDictateNextSigner ?? false,
distributionMethod: meta.distributionMethod || DocumentDistributionMethod.EMAIL, // Todo: Make this a setting.
// Signature settings.
typedSignatureEnabled: meta.typedSignatureEnabled ?? settings.typedSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled ?? settings.drawSignatureEnabled,
// Email settings.
emailId: meta.emailId ?? settings.emailId,
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
emailSettings: meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
// Envelope expiration.
envelopeExpirationPeriod: meta.envelopeExpirationPeriod ?? settings.envelopeExpirationPeriod ?? null,
// Reminder settings.
reminderSettings: meta.reminderSettings ?? settings.reminderSettings ?? null,
} satisfies Omit<DocumentMeta, 'id'>;
};
/**
* Map an envelope to a legacy document lite response entity.
*
* Do not use spread operator here to avoid unexpected behavior.
*/
export const mapEnvelopeToDocumentLite = (envelope: Envelope): TDocumentLite => {
const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
return {
id: documentId, // Use legacy ID.
envelopeId: envelope.id,
internalVersion: envelope.internalVersion,
visibility: envelope.visibility,
status: envelope.status,
source: envelope.source,
externalId: envelope.externalId,
userId: envelope.userId,
authOptions: envelope.authOptions,
formValues: envelope.formValues,
title: envelope.title,
createdAt: envelope.createdAt,
documentDataId: '', // Backwards compatibility.
updatedAt: envelope.updatedAt,
completedAt: envelope.completedAt,
deletedAt: envelope.deletedAt,
teamId: envelope.teamId,
folderId: envelope.folderId,
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
templateId: envelope.templateId,
};
};
type MapEnvelopeToDocumentManyOptions = Envelope & {
user: Pick<User, 'id' | 'name' | 'email'>;
team: Pick<Team, 'id' | 'url'>;
recipients: Recipient[];
};
/**
* Map an envelope to a legacy document many response entity.
*
* Do not use spread operator here to avoid unexpected behavior.
*/
export const mapEnvelopesToDocumentMany = (envelope: MapEnvelopeToDocumentManyOptions): TDocumentMany => {
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
return {
id: legacyDocumentId, // Use legacy ID.
envelopeId: envelope.id,
internalVersion: envelope.internalVersion,
visibility: envelope.visibility,
status: envelope.status,
source: envelope.source,
externalId: envelope.externalId,
userId: envelope.userId,
authOptions: envelope.authOptions,
formValues: envelope.formValues,
title: envelope.title,
createdAt: envelope.createdAt,
documentDataId: '', // Backwards compatibility.
updatedAt: envelope.updatedAt,
completedAt: envelope.completedAt,
deletedAt: envelope.deletedAt,
teamId: envelope.teamId,
folderId: envelope.folderId,
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
templateId: envelope.templateId,
user: {
id: envelope.userId,
name: envelope.user.name,
email: envelope.user.email,
},
team: {
id: envelope.teamId,
url: envelope.team.url,
},
recipients: envelope.recipients.map((recipient) => mapRecipientToLegacyRecipient(recipient, envelope)),
};
};