Files
documenso/packages/lib/server-only/signature-level/assert-compatible-recipient-grouping.ts
T
2026-08-26 12:10:29 +10:00

65 lines
2.5 KiB
TypeScript

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, so it breaks the same per-recipient
* `/ByteRange` invariant that {@link assertCompatibleSigningOrder} exists to
* protect: each TSP signature must cover the exact bytes it was applied to,
* and concurrent incremental updates over one base state cannot all verify.
*
* Note this is not caught by the `signingOrder = PARALLEL` guard: groups are
* expressed as duplicate `Recipient.signingOrder` values on a document whose
* `documentMeta.signingOrder` is SEQUENTIAL.
*
* 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.
*
* Schema-layer guard. {@link sendDocument} re-checks at distribution time as a
* defence-in-depth backstop.
*/
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);
}
};