d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
646 lines
19 KiB
TypeScript
646 lines
19 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { getErrorMessage } from '@sim/utils/errors'
|
|
import { type NextRequest, NextResponse } from 'next/server'
|
|
import { docusignToolContract } from '@/lib/api/contracts/tools/docusign'
|
|
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
|
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
import {
|
|
assertKnownSizeWithinLimit,
|
|
DEFAULT_MAX_ERROR_BODY_BYTES,
|
|
isPayloadSizeLimitError,
|
|
readResponseJsonWithLimit,
|
|
readResponseTextWithLimit,
|
|
readResponseToBufferWithLimit,
|
|
} from '@/lib/core/utils/stream-limits'
|
|
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
|
import { uploadCopilotFile } from '@/lib/uploads/contexts/copilot'
|
|
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
|
|
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
|
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
|
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
|
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
|
|
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
|
|
|
const logger = createLogger('DocuSignAPI')
|
|
const MAX_DOCUSIGN_DOCUMENT_BYTES = 25 * 1024 * 1024
|
|
const MAX_LEGACY_INLINE_DOCUMENT_BYTES = 7 * 1024 * 1024
|
|
const MAX_DOCUSIGN_JSON_BYTES = 2 * 1024 * 1024
|
|
const DOCUSIGN_FETCH_TIMEOUT_MS = 30_000
|
|
|
|
interface DocuSignAccountInfo {
|
|
accountId: string
|
|
baseUri: string
|
|
}
|
|
|
|
async function readDocusignJson(
|
|
response: Response,
|
|
label: string
|
|
): Promise<Record<string, unknown>> {
|
|
return readResponseJsonWithLimit<Record<string, unknown>>(response, {
|
|
maxBytes: MAX_DOCUSIGN_JSON_BYTES,
|
|
label,
|
|
})
|
|
}
|
|
|
|
function docusignError(data: Record<string, unknown>, fallback: string): string {
|
|
return (
|
|
(typeof data.message === 'string' && data.message) ||
|
|
(typeof data.errorCode === 'string' && data.errorCode) ||
|
|
fallback
|
|
)
|
|
}
|
|
|
|
async function fetchDocusign(
|
|
input: string,
|
|
init: RequestInit = {},
|
|
parentSignal?: AbortSignal
|
|
): Promise<Response> {
|
|
const controller = new AbortController()
|
|
const timeout = setTimeout(() => {
|
|
controller.abort(new Error('DocuSign request timed out'))
|
|
}, DOCUSIGN_FETCH_TIMEOUT_MS)
|
|
const abort = () => controller.abort(parentSignal?.reason ?? new Error('Request aborted'))
|
|
parentSignal?.addEventListener('abort', abort, { once: true })
|
|
|
|
try {
|
|
return await fetch(input, { ...init, signal: controller.signal })
|
|
} finally {
|
|
clearTimeout(timeout)
|
|
parentSignal?.removeEventListener('abort', abort)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolves the user's DocuSign account info from their access token
|
|
* by calling the DocuSign userinfo endpoint.
|
|
*/
|
|
async function resolveAccount(
|
|
accessToken: string,
|
|
signal?: AbortSignal
|
|
): Promise<DocuSignAccountInfo> {
|
|
const response = await fetchDocusign(
|
|
'https://account-d.docusign.com/oauth/userinfo',
|
|
{
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
},
|
|
signal
|
|
)
|
|
|
|
if (!response.ok) {
|
|
const errorText = await readResponseTextWithLimit(response, {
|
|
maxBytes: DEFAULT_MAX_ERROR_BODY_BYTES,
|
|
label: 'DocuSign account error response',
|
|
}).catch(() => '')
|
|
logger.error('Failed to resolve DocuSign account', {
|
|
status: response.status,
|
|
error: errorText,
|
|
})
|
|
throw new Error(`Failed to resolve DocuSign account: ${response.status}`)
|
|
}
|
|
|
|
const data = await readDocusignJson(response, 'DocuSign account response')
|
|
const accounts = Array.isArray(data.accounts)
|
|
? (data.accounts as Array<{
|
|
is_default?: boolean
|
|
base_uri?: string
|
|
account_id?: string
|
|
}>)
|
|
: []
|
|
|
|
const defaultAccount = accounts.find((account) => account.is_default) ?? accounts[0]
|
|
if (!defaultAccount) {
|
|
throw new Error('No DocuSign accounts found for this user')
|
|
}
|
|
|
|
const baseUri = defaultAccount.base_uri
|
|
if (!baseUri) {
|
|
throw new Error('DocuSign account is missing base_uri')
|
|
}
|
|
const accountId = defaultAccount.account_id
|
|
if (!accountId) {
|
|
throw new Error('DocuSign account is missing account_id')
|
|
}
|
|
|
|
return {
|
|
accountId,
|
|
baseUri,
|
|
}
|
|
}
|
|
|
|
export const POST = withRouteHandler(async (request: NextRequest) => {
|
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
|
if (!authResult.success || !authResult.userId) {
|
|
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const parsed = await parseRequest(
|
|
docusignToolContract,
|
|
request,
|
|
{},
|
|
{
|
|
validationErrorResponse: (error) =>
|
|
NextResponse.json(
|
|
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
|
|
{ status: 400 }
|
|
),
|
|
}
|
|
)
|
|
if (!parsed.success) return parsed.response
|
|
|
|
const { accessToken, operation, ...params } = parsed.data.body
|
|
|
|
try {
|
|
const account = await resolveAccount(accessToken, request.signal)
|
|
const apiBase = `${account.baseUri}/restapi/v2.1/accounts/${account.accountId}`
|
|
const headers: Record<string, string> = {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'Content-Type': 'application/json',
|
|
}
|
|
|
|
switch (operation) {
|
|
case 'send_envelope':
|
|
return await handleSendEnvelope(apiBase, headers, params, authResult.userId, request.signal)
|
|
case 'create_from_template':
|
|
return await handleCreateFromTemplate(apiBase, headers, params, request.signal)
|
|
case 'get_envelope':
|
|
return await handleGetEnvelope(apiBase, headers, params, request.signal)
|
|
case 'list_envelopes':
|
|
return await handleListEnvelopes(apiBase, headers, params, request.signal)
|
|
case 'void_envelope':
|
|
return await handleVoidEnvelope(apiBase, headers, params, request.signal)
|
|
case 'download_document':
|
|
return await handleDownloadDocument(
|
|
apiBase,
|
|
headers,
|
|
params,
|
|
authResult.userId,
|
|
request.signal
|
|
)
|
|
case 'list_templates':
|
|
return await handleListTemplates(apiBase, headers, params, request.signal)
|
|
case 'list_recipients':
|
|
return await handleListRecipients(apiBase, headers, params, request.signal)
|
|
default:
|
|
return NextResponse.json(
|
|
{ success: false, error: `Unknown operation: ${operation}` },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
} catch (error) {
|
|
logger.error('DocuSign API error', { operation, error })
|
|
const message = getErrorMessage(error, 'Internal server error')
|
|
return NextResponse.json(
|
|
{ success: false, error: message },
|
|
{ status: isPayloadSizeLimitError(error) ? 413 : 500 }
|
|
)
|
|
}
|
|
})
|
|
|
|
async function handleSendEnvelope(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
userId: string,
|
|
signal?: AbortSignal
|
|
) {
|
|
const { signerEmail, signerName, emailSubject, emailBody, ccEmail, ccName, file, status } = params
|
|
|
|
if (!signerEmail || !signerName || !emailSubject) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'signerEmail, signerName, and emailSubject are required' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
let documentBase64 = ''
|
|
let documentName = 'document.pdf'
|
|
|
|
if (file) {
|
|
try {
|
|
const parsed = FileInputSchema.parse(file)
|
|
const userFiles = processFilesToUserFiles([parsed as RawFileInput], 'docusign-send', logger)
|
|
if (userFiles.length > 0) {
|
|
const userFile = userFiles[0]
|
|
const denied = await assertToolFileAccess(userFile.key, userId, 'docusign-send', logger)
|
|
if (denied) return denied
|
|
if (userFile.size > MAX_DOCUSIGN_DOCUMENT_BYTES) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Document is too large to send through DocuSign' },
|
|
{ status: 413 }
|
|
)
|
|
}
|
|
const { buffer } = await downloadServableFileFromStorage(
|
|
userFile,
|
|
'docusign-send',
|
|
logger,
|
|
{
|
|
maxBytes: MAX_DOCUSIGN_DOCUMENT_BYTES,
|
|
}
|
|
)
|
|
assertKnownSizeWithinLimit(buffer.length, MAX_DOCUSIGN_DOCUMENT_BYTES, 'DocuSign document')
|
|
documentBase64 = buffer.toString('base64')
|
|
documentName = userFile.name
|
|
}
|
|
} catch (fileError) {
|
|
const notReady = docNotReadyResponse(fileError)
|
|
if (notReady) return notReady
|
|
logger.error('Failed to process file for DocuSign envelope', { fileError })
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: isPayloadSizeLimitError(fileError)
|
|
? getErrorMessage(fileError, 'Document is too large to send through DocuSign')
|
|
: 'Failed to process uploaded file',
|
|
},
|
|
{ status: isPayloadSizeLimitError(fileError) ? 413 : 400 }
|
|
)
|
|
}
|
|
}
|
|
|
|
const envelopeBody: Record<string, unknown> = {
|
|
emailSubject,
|
|
status: (status as string) || 'sent',
|
|
recipients: {
|
|
signers: [
|
|
{
|
|
email: signerEmail,
|
|
name: signerName,
|
|
recipientId: '1',
|
|
routingOrder: '1',
|
|
tabs: {
|
|
signHereTabs: [
|
|
{
|
|
anchorString: '/sig1/',
|
|
anchorUnits: 'pixels',
|
|
anchorXOffset: '0',
|
|
anchorYOffset: '0',
|
|
},
|
|
],
|
|
dateSignedTabs: [
|
|
{
|
|
anchorString: '/date1/',
|
|
anchorUnits: 'pixels',
|
|
anchorXOffset: '0',
|
|
anchorYOffset: '0',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
carbonCopies: ccEmail
|
|
? [
|
|
{
|
|
email: ccEmail,
|
|
name: ccName || (ccEmail as string),
|
|
recipientId: '2',
|
|
routingOrder: '2',
|
|
},
|
|
]
|
|
: [],
|
|
},
|
|
}
|
|
|
|
if (emailBody) {
|
|
envelopeBody.emailBlurb = emailBody
|
|
}
|
|
|
|
if (documentBase64) {
|
|
envelopeBody.documents = [
|
|
{
|
|
documentBase64,
|
|
name: documentName,
|
|
fileExtension: documentName.split('.').pop() || 'pdf',
|
|
documentId: '1',
|
|
},
|
|
]
|
|
} else if (((status as string) || 'sent') === 'sent') {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'A document file is required to send an envelope' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const response = await fetchDocusign(
|
|
`${apiBase}/envelopes`,
|
|
{
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(envelopeBody),
|
|
},
|
|
signal
|
|
)
|
|
|
|
const data = await readDocusignJson(response, 'DocuSign send envelope response')
|
|
if (!response.ok) {
|
|
logger.error('DocuSign send envelope failed', { data, status: response.status })
|
|
return NextResponse.json(
|
|
{ success: false, error: docusignError(data, 'Failed to send envelope') },
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(data)
|
|
}
|
|
|
|
async function handleCreateFromTemplate(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
signal?: AbortSignal
|
|
) {
|
|
const { templateId, emailSubject, emailBody, templateRoles, status } = params
|
|
|
|
if (!templateId) {
|
|
return NextResponse.json({ success: false, error: 'templateId is required' }, { status: 400 })
|
|
}
|
|
|
|
let parsedRoles: unknown[] = []
|
|
if (templateRoles) {
|
|
if (typeof templateRoles === 'string') {
|
|
try {
|
|
parsedRoles = JSON.parse(templateRoles)
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Invalid JSON for templateRoles' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
} else if (Array.isArray(templateRoles)) {
|
|
parsedRoles = templateRoles
|
|
}
|
|
}
|
|
|
|
const envelopeBody: Record<string, unknown> = {
|
|
templateId,
|
|
status: (status as string) || 'sent',
|
|
templateRoles: parsedRoles,
|
|
}
|
|
|
|
if (emailSubject) envelopeBody.emailSubject = emailSubject
|
|
if (emailBody) envelopeBody.emailBlurb = emailBody
|
|
|
|
const response = await fetchDocusign(
|
|
`${apiBase}/envelopes`,
|
|
{
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(envelopeBody),
|
|
},
|
|
signal
|
|
)
|
|
|
|
const data = await readDocusignJson(response, 'DocuSign create from template response')
|
|
if (!response.ok) {
|
|
logger.error('DocuSign create from template failed', { data, status: response.status })
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: docusignError(data, 'Failed to create envelope from template'),
|
|
},
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(data)
|
|
}
|
|
|
|
async function handleGetEnvelope(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
signal?: AbortSignal
|
|
) {
|
|
const { envelopeId } = params
|
|
if (!envelopeId) {
|
|
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
|
}
|
|
|
|
const response = await fetchDocusign(
|
|
`${apiBase}/envelopes/${(envelopeId as string).trim()}?include=recipients,documents`,
|
|
{ headers },
|
|
signal
|
|
)
|
|
const data = await readDocusignJson(response, 'DocuSign envelope response')
|
|
|
|
if (!response.ok) {
|
|
return NextResponse.json(
|
|
{ success: false, error: docusignError(data, 'Failed to get envelope') },
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(data)
|
|
}
|
|
|
|
async function handleListEnvelopes(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
signal?: AbortSignal
|
|
) {
|
|
const queryParams = new URLSearchParams()
|
|
|
|
const fromDate = params.fromDate as string | undefined
|
|
if (fromDate) {
|
|
queryParams.append('from_date', fromDate)
|
|
} else {
|
|
const thirtyDaysAgo = new Date()
|
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
|
queryParams.append('from_date', thirtyDaysAgo.toISOString())
|
|
}
|
|
|
|
if (params.toDate) queryParams.append('to_date', params.toDate as string)
|
|
if (params.envelopeStatus) queryParams.append('status', params.envelopeStatus as string)
|
|
if (params.searchText) queryParams.append('search_text', params.searchText as string)
|
|
if (params.count) queryParams.append('count', params.count as string)
|
|
|
|
const response = await fetchDocusign(`${apiBase}/envelopes?${queryParams}`, { headers }, signal)
|
|
const data = await readDocusignJson(response, 'DocuSign envelope list response')
|
|
|
|
if (!response.ok) {
|
|
return NextResponse.json(
|
|
{ success: false, error: docusignError(data, 'Failed to list envelopes') },
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(data)
|
|
}
|
|
|
|
async function handleVoidEnvelope(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
signal?: AbortSignal
|
|
) {
|
|
const { envelopeId, voidedReason } = params
|
|
if (!envelopeId) {
|
|
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
|
}
|
|
if (!voidedReason) {
|
|
return NextResponse.json({ success: false, error: 'voidedReason is required' }, { status: 400 })
|
|
}
|
|
|
|
const response = await fetchDocusign(
|
|
`${apiBase}/envelopes/${(envelopeId as string).trim()}`,
|
|
{
|
|
method: 'PUT',
|
|
headers,
|
|
body: JSON.stringify({ status: 'voided', voidedReason }),
|
|
},
|
|
signal
|
|
)
|
|
|
|
const data = await readDocusignJson(response, 'DocuSign void envelope response')
|
|
if (!response.ok) {
|
|
return NextResponse.json(
|
|
{ success: false, error: docusignError(data, 'Failed to void envelope') },
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json({ envelopeId, status: 'voided' })
|
|
}
|
|
|
|
async function handleDownloadDocument(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
userId: string,
|
|
signal?: AbortSignal
|
|
) {
|
|
const { envelopeId, documentId } = params
|
|
if (!envelopeId) {
|
|
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
|
}
|
|
|
|
const docId = (documentId as string) || 'combined'
|
|
|
|
const response = await fetchDocusign(
|
|
`${apiBase}/envelopes/${(envelopeId as string).trim()}/documents/${docId}`,
|
|
{
|
|
headers: { Authorization: headers.Authorization },
|
|
},
|
|
signal
|
|
)
|
|
|
|
if (!response.ok) {
|
|
let errorText = ''
|
|
try {
|
|
errorText = await readResponseTextWithLimit(response, {
|
|
maxBytes: DEFAULT_MAX_ERROR_BODY_BYTES,
|
|
label: 'DocuSign document error response',
|
|
})
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return NextResponse.json(
|
|
{ success: false, error: `Failed to download document: ${response.status} ${errorText}` },
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || 'application/pdf'
|
|
const contentDisposition = response.headers.get('content-disposition') || ''
|
|
let fileName = `document-${docId}.pdf`
|
|
|
|
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
|
if (filenameMatch) {
|
|
fileName = filenameMatch[1].replace(/['"]/g, '')
|
|
}
|
|
|
|
const buffer = await readResponseToBufferWithLimit(response, {
|
|
maxBytes: MAX_DOCUSIGN_DOCUMENT_BYTES,
|
|
label: 'DocuSign document download',
|
|
})
|
|
|
|
const workspaceId = typeof params.workspaceId === 'string' ? params.workspaceId : undefined
|
|
const workflowId = typeof params.workflowId === 'string' ? params.workflowId : undefined
|
|
const executionId = typeof params.executionId === 'string' ? params.executionId : undefined
|
|
const legacyInlineContent =
|
|
buffer.length <= MAX_LEGACY_INLINE_DOCUMENT_BYTES
|
|
? { base64Content: buffer.toString('base64') }
|
|
: {}
|
|
|
|
if (workspaceId && workflowId && executionId) {
|
|
const file = await uploadExecutionFile(
|
|
{ workspaceId, workflowId, executionId },
|
|
buffer,
|
|
fileName,
|
|
contentType,
|
|
userId
|
|
)
|
|
return NextResponse.json({
|
|
file,
|
|
mimeType: contentType,
|
|
fileName,
|
|
...legacyInlineContent,
|
|
})
|
|
}
|
|
|
|
const file = await uploadCopilotFile({
|
|
buffer,
|
|
fileName,
|
|
contentType,
|
|
userId,
|
|
})
|
|
|
|
return NextResponse.json({ file, mimeType: contentType, fileName, ...legacyInlineContent })
|
|
}
|
|
|
|
async function handleListTemplates(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
signal?: AbortSignal
|
|
) {
|
|
const queryParams = new URLSearchParams()
|
|
if (params.searchText) queryParams.append('search_text', params.searchText as string)
|
|
if (params.count) queryParams.append('count', params.count as string)
|
|
|
|
const queryString = queryParams.toString()
|
|
const url = queryString ? `${apiBase}/templates?${queryString}` : `${apiBase}/templates`
|
|
|
|
const response = await fetchDocusign(url, { headers }, signal)
|
|
const data = await readDocusignJson(response, 'DocuSign template list response')
|
|
|
|
if (!response.ok) {
|
|
return NextResponse.json(
|
|
{ success: false, error: docusignError(data, 'Failed to list templates') },
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(data)
|
|
}
|
|
|
|
async function handleListRecipients(
|
|
apiBase: string,
|
|
headers: Record<string, string>,
|
|
params: Record<string, unknown>,
|
|
signal?: AbortSignal
|
|
) {
|
|
const { envelopeId } = params
|
|
if (!envelopeId) {
|
|
return NextResponse.json({ success: false, error: 'envelopeId is required' }, { status: 400 })
|
|
}
|
|
|
|
const response = await fetchDocusign(
|
|
`${apiBase}/envelopes/${(envelopeId as string).trim()}/recipients`,
|
|
{
|
|
headers,
|
|
},
|
|
signal
|
|
)
|
|
const data = await readDocusignJson(response, 'DocuSign recipients response')
|
|
|
|
if (!response.ok) {
|
|
return NextResponse.json(
|
|
{ success: false, error: docusignError(data, 'Failed to list recipients') },
|
|
{ status: response.status }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(data)
|
|
}
|