import { prepareCscRecipientSigning } from '@documenso/ee/server-only/signing/csc/prepare-recipient-signing'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token'; import { createEnvelopeRecipients } from '@documenso/lib/server-only/recipient/create-envelope-recipients'; import { deleteEnvelopeRecipient } from '@documenso/lib/server-only/recipient/delete-envelope-recipient'; import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id'; import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients'; import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients'; import { updateEnvelopeRecipients } from '@documenso/lib/server-only/recipient/update-envelope-recipients'; import { isTspEnvelope } from '@documenso/lib/types/signature-level'; import { unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { EnvelopeType } from '@prisma/client'; import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema'; import { authenticatedProcedure, procedure, router } from '../trpc'; import { findRecipientSuggestionsRoute } from './find-recipient-suggestions'; import { ZCompleteDocumentWithTokenMutationSchema, ZCompleteDocumentWithTokenResponseSchema, ZCreateDocumentRecipientRequestSchema, ZCreateDocumentRecipientResponseSchema, ZCreateDocumentRecipientsRequestSchema, ZCreateDocumentRecipientsResponseSchema, ZCreateTemplateRecipientRequestSchema, ZCreateTemplateRecipientResponseSchema, ZCreateTemplateRecipientsRequestSchema, ZCreateTemplateRecipientsResponseSchema, ZDeleteDocumentRecipientRequestSchema, ZDeleteTemplateRecipientRequestSchema, ZGetRecipientRequestSchema, ZGetRecipientResponseSchema, ZRejectDocumentWithTokenMutationSchema, ZSetDocumentRecipientsRequestSchema, ZSetDocumentRecipientsResponseSchema, ZSetTemplateRecipientsRequestSchema, ZSetTemplateRecipientsResponseSchema, ZUpdateDocumentRecipientRequestSchema, ZUpdateDocumentRecipientResponseSchema, ZUpdateDocumentRecipientsRequestSchema, ZUpdateDocumentRecipientsResponseSchema, ZUpdateTemplateRecipientRequestSchema, ZUpdateTemplateRecipientResponseSchema, ZUpdateTemplateRecipientsRequestSchema, ZUpdateTemplateRecipientsResponseSchema, } from './schema'; export const recipientRouter = router({ suggestions: { find: findRecipientSuggestionsRoute, }, /** * @public */ getDocumentRecipient: authenticatedProcedure .meta({ openapi: { method: 'GET', path: '/document/recipient/{recipientId}', summary: 'Get document recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Returns a single recipient. If you want to retrieve all the recipients for a document, use the "Get Document" endpoint.', tags: ['Document Recipients'], deprecated: true, }, }) .input(ZGetRecipientRequestSchema) .output(ZGetRecipientResponseSchema) .query(async ({ input, ctx }) => { const { teamId } = ctx; const { recipientId } = input; ctx.logger.info({ input: { recipientId, }, }); return await getRecipientById({ userId: ctx.user.id, teamId, recipientId, type: EnvelopeType.DOCUMENT, }); }), /** * @public */ createDocumentRecipient: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/document/recipient/create', summary: 'Create document recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Create a single recipient for a document.', tags: ['Document Recipients'], deprecated: true, }, }) .input(ZCreateDocumentRecipientRequestSchema) .output(ZCreateDocumentRecipientResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { documentId, recipient } = input; ctx.logger.info({ input: { documentId, }, }); const createdRecipients = await createEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { type: 'documentId', id: documentId, }, recipients: [recipient], requestMetadata: ctx.metadata, }); return createdRecipients.recipients[0]; }), /** * @public */ createDocumentRecipients: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/document/recipient/create-many', summary: 'Create document recipients', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Create multiple recipients for a document.', tags: ['Document Recipients'], deprecated: true, }, }) .input(ZCreateDocumentRecipientsRequestSchema) .output(ZCreateDocumentRecipientsResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { documentId, recipients } = input; ctx.logger.info({ input: { documentId, }, }); return await createEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { type: 'documentId', id: documentId, }, recipients, requestMetadata: ctx.metadata, }); }), /** * @public */ updateDocumentRecipient: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/document/recipient/update', summary: 'Update document recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Update a single recipient for a document.', tags: ['Document Recipients'], deprecated: true, }, }) .input(ZUpdateDocumentRecipientRequestSchema) .output(ZUpdateDocumentRecipientResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { documentId, recipient } = input; ctx.logger.info({ input: { documentId, }, }); const updatedRecipients = await updateEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { type: 'documentId', id: documentId, }, recipients: [recipient], requestMetadata: ctx.metadata, }); return updatedRecipients.recipients[0]; }), /** * @public */ updateDocumentRecipients: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/document/recipient/update-many', summary: 'Update document recipients', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Update multiple recipients for a document.', tags: ['Document Recipients'], deprecated: true, }, }) .input(ZUpdateDocumentRecipientsRequestSchema) .output(ZUpdateDocumentRecipientsResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { documentId, recipients } = input; ctx.logger.info({ input: { documentId, }, }); return await updateEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { type: 'documentId', id: documentId, }, recipients, requestMetadata: ctx.metadata, }); }), /** * @public */ deleteDocumentRecipient: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/document/recipient/delete', summary: 'Delete document recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide.', tags: ['Document Recipients'], deprecated: true, }, }) .input(ZDeleteDocumentRecipientRequestSchema) .output(ZSuccessResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { recipientId } = input; ctx.logger.info({ input: { recipientId, }, }); await deleteEnvelopeRecipient({ userId: ctx.user.id, teamId, recipientId, requestMetadata: ctx.metadata, }); return ZGenericSuccessResponse; }), /** * @private */ setDocumentRecipients: authenticatedProcedure .input(ZSetDocumentRecipientsRequestSchema) .output(ZSetDocumentRecipientsResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { documentId, recipients } = input; ctx.logger.info({ input: { documentId, }, }); return await setDocumentRecipients({ userId: ctx.user.id, teamId, id: { type: 'documentId', id: documentId, }, recipients: recipients.map((recipient) => ({ id: recipient.id, email: recipient.email, name: recipient.name, role: recipient.role, signingOrder: recipient.signingOrder, actionAuth: recipient.actionAuth, })), requestMetadata: ctx.metadata, }); }), /** * @public */ getTemplateRecipient: authenticatedProcedure .meta({ openapi: { method: 'GET', path: '/template/recipient/{recipientId}', summary: 'Get template recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Returns a single recipient. If you want to retrieve all the recipients for a template, use the "Get Template" endpoint.', tags: ['Template Recipients'], deprecated: true, }, }) .input(ZGetRecipientRequestSchema) .output(ZGetRecipientResponseSchema) .query(async ({ input, ctx }) => { const { teamId } = ctx; const { recipientId } = input; ctx.logger.info({ input: { recipientId, }, }); return await getRecipientById({ userId: ctx.user.id, teamId, recipientId, type: EnvelopeType.TEMPLATE, }); }), /** * @public */ createTemplateRecipient: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/template/recipient/create', summary: 'Create template recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Create a single recipient for a template.', tags: ['Template Recipients'], deprecated: true, }, }) .input(ZCreateTemplateRecipientRequestSchema) .output(ZCreateTemplateRecipientResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { templateId, recipient } = input; ctx.logger.info({ input: { templateId, }, }); const createdRecipients = await createEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { id: templateId, type: 'templateId', }, recipients: [recipient], requestMetadata: ctx.metadata, }); return createdRecipients.recipients[0]; }), /** * @public */ createTemplateRecipients: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/template/recipient/create-many', summary: 'Create template recipients', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Create multiple recipients for a template.', tags: ['Template Recipients'], deprecated: true, }, }) .input(ZCreateTemplateRecipientsRequestSchema) .output(ZCreateTemplateRecipientsResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { templateId, recipients } = input; ctx.logger.info({ input: { templateId, }, }); return await createEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { id: templateId, type: 'templateId', }, recipients, requestMetadata: ctx.metadata, }); }), /** * @public */ updateTemplateRecipient: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/template/recipient/update', summary: 'Update template recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Update a single recipient for a template.', tags: ['Template Recipients'], deprecated: true, }, }) .input(ZUpdateTemplateRecipientRequestSchema) .output(ZUpdateTemplateRecipientResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { templateId, recipient } = input; ctx.logger.info({ input: { templateId, }, }); const updatedRecipients = await updateEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { type: 'templateId', id: templateId, }, recipients: [recipient], requestMetadata: ctx.metadata, }); return updatedRecipients.recipients[0]; }), /** * @public */ updateTemplateRecipients: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/template/recipient/update-many', summary: 'Update template recipients', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide. Update multiple recipients for a template.', tags: ['Template Recipients'], deprecated: true, }, }) .input(ZUpdateTemplateRecipientsRequestSchema) .output(ZUpdateTemplateRecipientsResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { templateId, recipients } = input; ctx.logger.info({ input: { templateId, }, }); return await updateEnvelopeRecipients({ userId: ctx.user.id, teamId, id: { type: 'templateId', id: templateId, }, recipients, requestMetadata: ctx.metadata, }); }), /** * @public */ deleteTemplateRecipient: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/template/recipient/delete', summary: 'Delete template recipient', description: 'Deprecated: this endpoint is being replaced by the Envelope API. See https://docs.documenso.com/docs/developers/api/migrate-to-envelopes for the migration guide.', tags: ['Template Recipients'], deprecated: true, }, }) .input(ZDeleteTemplateRecipientRequestSchema) .output(ZSuccessResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { recipientId } = input; ctx.logger.info({ input: { recipientId, }, }); await deleteEnvelopeRecipient({ recipientId, userId: ctx.user.id, teamId, requestMetadata: ctx.metadata, }); return ZGenericSuccessResponse; }), /** * @private */ setTemplateRecipients: authenticatedProcedure .input(ZSetTemplateRecipientsRequestSchema) .output(ZSetTemplateRecipientsResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; const { templateId, recipients } = input; ctx.logger.info({ input: { templateId, }, }); return await setTemplateRecipients({ userId: ctx.user.id, teamId, id: { type: 'templateId', id: templateId, }, recipients: recipients.map((recipient) => ({ id: recipient.id, email: recipient.email, name: recipient.name, role: recipient.role, signingOrder: recipient.signingOrder, actionAuth: recipient.actionAuth, })), }); }), /** * @private */ completeDocumentWithToken: procedure .input(ZCompleteDocumentWithTokenMutationSchema) .output(ZCompleteDocumentWithTokenResponseSchema) .mutation(async ({ input, ctx }) => { try { const { token, documentId, accessAuthOptions, nextSigner, recipientOverride } = input; ctx.logger.info({ input: { documentId, }, }); // Branch on TSP envelopes before any SES side effects: TSP recipients // can't complete via this route — they go through the CSC sync sign // flow (`enterprise.csc.signEnvelope`). This route returns the redirect URL // for the credential-scope OAuth round-trip. const envelope = await prisma.envelope.findFirst({ where: { ...unsafeBuildEnvelopeIdQuery({ type: 'documentId', id: documentId }, EnvelopeType.DOCUMENT), recipients: { some: { token } }, }, select: { signatureLevel: true, internalVersion: true }, }); // The most common cause is a stale signing page: the document was // deleted, or the recipient was removed, after the link was opened. // Surface a NOT_FOUND instead of leaking a Prisma P2025 as a 500. if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found for the provided signing token', statusCode: 404, }); } if (isTspEnvelope(envelope)) { return await prepareCscRecipientSigning({ recipientToken: token, requestMetadata: ctx.metadata.requestMetadata, }); } await completeDocumentWithToken({ token, id: { type: 'documentId', id: documentId, }, accessAuthOptions, nextSigner, recipientOverride, userId: ctx.user?.id, requestMetadata: ctx.metadata.requestMetadata, }); return { status: 'SIGNED' as const }; } catch (err) { // Resolve retried, stale or concurrent duplicate completion requests // idempotently so the client routes the user to the completed page // instead of surfacing an error for a document that is signed. if (err instanceof AppError && err.code === AppErrorCode.RECIPIENT_ALREADY_SIGNED) { ctx.logger.info({ message: 'Recipient attempted to complete a document they have already signed', }); return { status: 'ALREADY_SIGNED' as const }; } // Log the error for debugging purposes. ctx.logger.error({ message: 'Error completing document with token', error: err instanceof AppError ? `[${err.code}]: ${err.message}` : String(err), }); // Raw console.log incase we're somehow dealing with a funky error object that doesn't serialize well. console.log('Error completing document with token', err); // Rethrow the error so that the client receives the appropriate error response. throw err; } }), /** * @private */ rejectDocumentWithToken: procedure.input(ZRejectDocumentWithTokenMutationSchema).mutation(async ({ input, ctx }) => { const { token, documentId, reason } = input; ctx.logger.info({ input: { documentId, }, }); return await rejectDocumentWithToken({ token, id: { type: 'documentId', id: documentId, }, reason, requestMetadata: ctx.metadata.requestMetadata, }); }), });