mirror of
https://github.com/documenso/documenso.git
synced 2026-10-04 02:33:54 +10:00
fix: reviewed
This commit is contained in:
@@ -38,9 +38,11 @@ import { isDocumentCompleted } from '../../utils/document';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
|
||||
import { getRecipientsInActiveSigningStep } from '../../utils/recipient-groups';
|
||||
import { getRecipientsWithMissingFields, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
|
||||
import { assertUserNotDisabledById } from '../user/assert-user-not-disabled';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
@@ -147,13 +149,17 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad
|
||||
envelope.documentMeta.signingOrder = DocumentSigningOrder.SEQUENTIAL;
|
||||
}
|
||||
|
||||
assertCompatibleRecipientGrouping({
|
||||
signatureLevel: envelope.signatureLevel,
|
||||
recipients: envelope.recipients,
|
||||
});
|
||||
|
||||
let recipientsToNotify = envelope.recipients;
|
||||
|
||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
// Get the currently active recipient.
|
||||
recipientsToNotify = envelope.recipients
|
||||
.filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC)
|
||||
.slice(0, 1);
|
||||
// Get the currently active signing group. Recipients sharing the lowest
|
||||
// pending signing order act in parallel within their group.
|
||||
recipientsToNotify = getRecipientsInActiveSigningStep(envelope.recipients);
|
||||
}
|
||||
|
||||
if (envelope.envelopeItems.length === 0) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { isTspEnvelope } from '../../types/signature-level';
|
||||
import { effectiveOrder } from '../../utils/recipient-groups';
|
||||
import { isCcRecipient } from '../../utils/recipients';
|
||||
|
||||
type AssertCompatibleRecipientGroupingOptions = {
|
||||
signatureLevel: string;
|
||||
recipients: Array<Pick<Recipient, 'role'> & { signingOrder?: number | null }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reject recipient signing groups on AES/QES envelopes.
|
||||
*
|
||||
* A "group" is two or more signing recipients sharing a signing step, which
|
||||
* they may then complete in any order — including at the same time. That is
|
||||
* parallel signing scoped to one step
|
||||
*
|
||||
* Recipients sharing a step are detected by {@link effectiveOrder}, so an
|
||||
* absent signing order counts too — every unordered recipient lands in the
|
||||
* same tail step and would sign in parallel.
|
||||
*
|
||||
* CC recipients are ignored: they never sign, and their signing order carries
|
||||
* no meaning anywhere else.
|
||||
*
|
||||
* SES envelopes pass through unchanged — signing groups are an SES feature.
|
||||
*/
|
||||
export const assertCompatibleRecipientGrouping = ({
|
||||
signatureLevel,
|
||||
recipients,
|
||||
}: AssertCompatibleRecipientGroupingOptions): void => {
|
||||
if (!isTspEnvelope({ signatureLevel })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seenOrders = new Set<number>();
|
||||
|
||||
for (const recipient of recipients) {
|
||||
if (isCcRecipient(recipient)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const order = effectiveOrder(recipient);
|
||||
|
||||
if (seenOrders.has(order)) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Envelopes signed at '${signatureLevel}' cannot place two recipients in the same signing step — a signing group is parallel signing within one step, which breaks the per-recipient /ByteRange invariant TSP signatures rely on. Give every signing recipient a distinct signingOrder.`,
|
||||
});
|
||||
}
|
||||
|
||||
seenOrders.add(order);
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
extractRecipientToNewStep,
|
||||
flattenRecipientGroups,
|
||||
getNextDictatableRecipient,
|
||||
getRecipientsInActiveSigningStep,
|
||||
groupRecipientsBySigningOrder,
|
||||
@@ -450,51 +449,6 @@ describe('locked step guards', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenRecipientGroups', () => {
|
||||
const recipient = (id: number, signingOrder: number | null, role: RecipientRole = RecipientRole.SIGNER) => ({
|
||||
id,
|
||||
signingOrder,
|
||||
role,
|
||||
});
|
||||
|
||||
it('returns no changes when every signing recipient already has their own step', () => {
|
||||
expect(flattenRecipientGroups([recipient(1, 1), recipient(2, 2)])).toEqual([]);
|
||||
});
|
||||
|
||||
// Sparse but distinct orders are valid; only a shared step needs repairing.
|
||||
it('leaves a valid but sparse sequence alone', () => {
|
||||
expect(flattenRecipientGroups([recipient(1, 1), recipient(2, 5)])).toEqual([]);
|
||||
});
|
||||
|
||||
it('splits a shared step while preserving relative order', () => {
|
||||
const changes = flattenRecipientGroups([recipient(1, 1), recipient(2, 2), recipient(3, 2)]);
|
||||
|
||||
// Only the second member of the shared step has to move.
|
||||
expect(changes).toEqual([{ id: 3, signingOrder: 3 }]);
|
||||
});
|
||||
|
||||
it('gives every unordered recipient a distinct step', () => {
|
||||
const changes = flattenRecipientGroups([recipient(1, null), recipient(2, null)]);
|
||||
|
||||
expect(changes).toEqual([
|
||||
{ id: 1, signingOrder: 1 },
|
||||
{ id: 2, signingOrder: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores CC recipients', () => {
|
||||
expect(flattenRecipientGroups([recipient(1, 1), recipient(2, 1, RecipientRole.CC)])).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not mutate the input array order', () => {
|
||||
const recipients = [recipient(3, 2), recipient(1, 1), recipient(2, 2)];
|
||||
|
||||
flattenRecipientGroups(recipients);
|
||||
|
||||
expect(recipients.map((r) => r.id)).toEqual([3, 1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRecipientTurnBySigningOrder', () => {
|
||||
const recipient = (
|
||||
id: number,
|
||||
|
||||
+6
-2
@@ -1,5 +1,9 @@
|
||||
import { ZRecipientAccessAuthTypesSchema, ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||
import {
|
||||
ZRecipientEmailSchema,
|
||||
ZRecipientLiteSchema,
|
||||
ZRecipientSigningOrderSchema,
|
||||
} from '@documenso/lib/types/recipient';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -20,7 +24,7 @@ export const ZUpdateEnvelopeRecipientSchema = z.object({
|
||||
email: ZRecipientEmailSchema.optional(),
|
||||
name: z.string().max(255).optional(),
|
||||
role: z.nativeEnum(RecipientRole).optional(),
|
||||
signingOrder: z.number().optional(),
|
||||
signingOrder: ZRecipientSigningOrderSchema.optional(),
|
||||
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
import { ZRecipientEmailSchema, ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ZUseEnvelopePayloadSchema = z.object({
|
||||
id: z.number().describe('The ID of the recipient in the template.'),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string().max(255).optional(),
|
||||
signingOrder: z.number().optional(),
|
||||
signingOrder: ZRecipientSigningOrderSchema.optional(),
|
||||
}),
|
||||
)
|
||||
.describe('The information of the recipients to create the document with.')
|
||||
|
||||
Reference in New Issue
Block a user