From 2a5310464462c48829030081c706dcf706ff09d9 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 12 Feb 2026 18:03:18 +1100 Subject: [PATCH] fix: wip --- .../create-embedding-envelope.ts | 37 +++ .../create-embedding-envelope.types.ts | 8 + .../create-embedding-presign-token.types.ts | 4 +- .../server/envelope-router/create-envelope.ts | 302 ++++++++++-------- 4 files changed, 213 insertions(+), 138 deletions(-) create mode 100644 packages/trpc/server/embedding-router/create-embedding-envelope.ts create mode 100644 packages/trpc/server/embedding-router/create-embedding-envelope.types.ts diff --git a/packages/trpc/server/embedding-router/create-embedding-envelope.ts b/packages/trpc/server/embedding-router/create-embedding-envelope.ts new file mode 100644 index 000000000..c52ecb852 --- /dev/null +++ b/packages/trpc/server/embedding-router/create-embedding-envelope.ts @@ -0,0 +1,37 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; + +import { createEnvelopeRouteCaller } from '../envelope-router/create-envelope'; +import { procedure } from '../trpc'; +import { + ZCreateEmbeddingEnvelopeRequestSchema, + ZCreateEmbeddingEnvelopeResponseSchema, +} from './create-embedding-envelope.types'; + +export const createEmbeddingEnvelopeRoute = procedure + .input(ZCreateEmbeddingEnvelopeRequestSchema) + .output(ZCreateEmbeddingEnvelopeResponseSchema) + .mutation(async ({ input, ctx }) => { + const { req } = ctx; + + const authorizationHeader = req.headers.get('authorization'); + + const [presignToken] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0); + + if (!presignToken) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'No presign token provided', + }); + } + + const apiToken = await verifyEmbeddingPresignToken({ token: presignToken }); + + const { userId, teamId } = apiToken; + + return await createEnvelopeRouteCaller({ + userId, + teamId, + input, + requestMetadata: ctx.metadata, + }); + }); diff --git a/packages/trpc/server/embedding-router/create-embedding-envelope.types.ts b/packages/trpc/server/embedding-router/create-embedding-envelope.types.ts new file mode 100644 index 000000000..722218f9e --- /dev/null +++ b/packages/trpc/server/embedding-router/create-embedding-envelope.types.ts @@ -0,0 +1,8 @@ +import { + ZCreateEnvelopeRequestSchema, + ZCreateEnvelopeResponseSchema, +} from '../envelope-router/create-envelope.types'; + +export const ZCreateEmbeddingEnvelopeRequestSchema = ZCreateEnvelopeRequestSchema; + +export const ZCreateEmbeddingEnvelopeResponseSchema = ZCreateEnvelopeResponseSchema; diff --git a/packages/trpc/server/embedding-router/create-embedding-presign-token.types.ts b/packages/trpc/server/embedding-router/create-embedding-presign-token.types.ts index ecfe434af..b42612ebc 100644 --- a/packages/trpc/server/embedding-router/create-embedding-presign-token.types.ts +++ b/packages/trpc/server/embedding-router/create-embedding-presign-token.types.ts @@ -24,7 +24,9 @@ export const ZCreateEmbeddingPresignTokenRequestSchema = z.object({ scope: z .string() .optional() - .describe('Resource restriction. Example: documentId:1, templateId:2'), + .describe( + 'Resource restriction. V1 embeds only support documentId:1, templateId:2. V2 embeds only support envelope:envelope_123', + ), }); export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({ diff --git a/packages/trpc/server/envelope-router/create-envelope.ts b/packages/trpc/server/envelope-router/create-envelope.ts index ead1debfa..e95d7eff4 100644 --- a/packages/trpc/server/envelope-router/create-envelope.ts +++ b/packages/trpc/server/envelope-router/create-envelope.ts @@ -5,10 +5,12 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields'; import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf'; +import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf'; import { authenticatedProcedure } from '../trpc'; +import type { TCreateEnvelopeRequest } from './create-envelope.types'; import { ZCreateEnvelopeRequestSchema, ZCreateEnvelopeResponseSchema, @@ -20,150 +22,176 @@ export const createEnvelopeRoute = authenticatedProcedure .input(ZCreateEnvelopeRequestSchema) .output(ZCreateEnvelopeResponseSchema) .mutation(async ({ input, ctx }) => { - const { user, teamId } = ctx; + ctx.logger.info({ + input: { + folderId: input.payload.folderId, + }, + }); - const { payload, files } = input; + return await createEnvelopeRouteCaller({ + userId: ctx.user.id, + teamId: ctx.teamId, + input, + requestMetadata: ctx.metadata, + }); + }); - const { - title, +type CreateEnvelopeRouteOptions = { + /** + * Verified user ID. + */ + userId: number; + + /** + * Unverified team ID. + */ + teamId: number; + input: TCreateEnvelopeRequest; + requestMetadata: ApiRequestMetadata; +}; + +export const createEnvelopeRouteCaller = async ({ + userId, + teamId, + input, + requestMetadata, +}: CreateEnvelopeRouteOptions) => { + const { payload, files } = input; + + const { + title, + type, + externalId, + visibility, + globalAccessAuth, + globalActionAuth, + formValues, + recipients, + folderId, + meta, + attachments, + delegatedDocumentOwner, + } = payload; + + const { remaining, maximumEnvelopeItemCount } = await getServerLimits({ + userId, + teamId, + }); + + if (remaining.documents <= 0) { + throw new AppError(AppErrorCode.LIMIT_EXCEEDED, { + message: 'You have reached your document limit for this month. Please upgrade your plan.', + statusCode: 400, + }); + } + + if (files.length > maximumEnvelopeItemCount) { + throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', { + message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`, + statusCode: 400, + }); + } + + if (files.some((file) => !file.type.startsWith('application/pdf'))) { + throw new AppError('INVALID_DOCUMENT_FILE', { + message: 'You cannot upload non-PDF files', + statusCode: 400, + }); + } + + // For each file: normalize, extract & clean placeholders, then upload. + const envelopeItems = await Promise.all( + files.map(async (file) => { + let pdf = Buffer.from(await file.arrayBuffer()); + + if (formValues) { + // eslint-disable-next-line require-atomic-updates + pdf = await insertFormValuesInPdf({ + pdf, + formValues, + }); + } + + const normalized = await normalizePdf(pdf, { + flattenForm: type !== EnvelopeType.TEMPLATE, + }); + + const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized); + + const { id: documentDataId } = await putPdfFileServerSide({ + name: file.name, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(cleanedPdf), + }); + + return { + title: file.name, + documentDataId, + placeholders, + }; + }), + ); + + const recipientsToCreate = recipients?.map((recipient) => ({ + email: recipient.email, + name: recipient.name, + role: recipient.role, + signingOrder: recipient.signingOrder, + accessAuth: recipient.accessAuth, + actionAuth: recipient.actionAuth, + fields: recipient.fields?.map((field) => { + let documentDataId: string | undefined = undefined; + + if (typeof field.identifier === 'string') { + documentDataId = envelopeItems.find( + (item) => item.title === field.identifier, + )?.documentDataId; + } + + if (typeof field.identifier === 'number') { + documentDataId = envelopeItems.at(field.identifier)?.documentDataId; + } + + if (field.identifier === undefined) { + documentDataId = envelopeItems.at(0)?.documentDataId; + } + + if (!documentDataId) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Document data not found', + }); + } + + return { + ...field, + documentDataId, + }; + }), + })); + + const envelope = await createEnvelope({ + userId, + teamId, + internalVersion: 2, + data: { type, + title, externalId, + formValues, visibility, globalAccessAuth, globalActionAuth, - formValues, - recipients, + recipients: recipientsToCreate, folderId, - meta, - attachments, + envelopeItems, delegatedDocumentOwner, - } = payload; - - ctx.logger.info({ - input: { - folderId, - }, - }); - - const { remaining, maximumEnvelopeItemCount } = await getServerLimits({ - userId: user.id, - teamId, - }); - - if (remaining.documents <= 0) { - throw new AppError(AppErrorCode.LIMIT_EXCEEDED, { - message: 'You have reached your document limit for this month. Please upgrade your plan.', - statusCode: 400, - }); - } - - if (files.length > maximumEnvelopeItemCount) { - throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', { - message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`, - statusCode: 400, - }); - } - - if (files.some((file) => !file.type.startsWith('application/pdf'))) { - throw new AppError('INVALID_DOCUMENT_FILE', { - message: 'You cannot upload non-PDF files', - statusCode: 400, - }); - } - - // For each file: normalize, extract & clean placeholders, then upload. - const envelopeItems = await Promise.all( - files.map(async (file) => { - let pdf = Buffer.from(await file.arrayBuffer()); - - if (formValues) { - // eslint-disable-next-line require-atomic-updates - pdf = await insertFormValuesInPdf({ - pdf, - formValues, - }); - } - - const normalized = await normalizePdf(pdf, { - flattenForm: type !== EnvelopeType.TEMPLATE, - }); - - const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized); - - const { id: documentDataId } = await putPdfFileServerSide({ - name: file.name, - type: 'application/pdf', - arrayBuffer: async () => Promise.resolve(cleanedPdf), - }); - - return { - title: file.name, - documentDataId, - placeholders, - }; - }), - ); - - const recipientsToCreate = recipients?.map((recipient) => ({ - email: recipient.email, - name: recipient.name, - role: recipient.role, - signingOrder: recipient.signingOrder, - accessAuth: recipient.accessAuth, - actionAuth: recipient.actionAuth, - fields: recipient.fields?.map((field) => { - let documentDataId: string | undefined = undefined; - - if (typeof field.identifier === 'string') { - documentDataId = envelopeItems.find( - (item) => item.title === field.identifier, - )?.documentDataId; - } - - if (typeof field.identifier === 'number') { - documentDataId = envelopeItems.at(field.identifier)?.documentDataId; - } - - if (field.identifier === undefined) { - documentDataId = envelopeItems.at(0)?.documentDataId; - } - - if (!documentDataId) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document data not found', - }); - } - - return { - ...field, - documentDataId, - }; - }), - })); - - const envelope = await createEnvelope({ - userId: user.id, - teamId, - internalVersion: 2, - data: { - type, - title, - externalId, - formValues, - visibility, - globalAccessAuth, - globalActionAuth, - recipients: recipientsToCreate, - folderId, - envelopeItems, - delegatedDocumentOwner, - }, - attachments, - meta, - requestMetadata: ctx.metadata, - }); - - return { - id: envelope.id, - }; + }, + attachments, + meta, + requestMetadata, }); + + return { + id: envelope.id, + }; +};