@@ -22,7 +22,9 @@ import { assertToolFileAccess } from '@/app/api/files/authorization'
2222
2323const logger = createLogger ( 'DocuSignAPI' )
2424const MAX_DOCUSIGN_DOCUMENT_BYTES = 25 * 1024 * 1024
25+ const MAX_LEGACY_INLINE_DOCUMENT_BYTES = 7 * 1024 * 1024
2526const MAX_DOCUSIGN_JSON_BYTES = 2 * 1024 * 1024
27+ const DOCUSIGN_FETCH_TIMEOUT_MS = 30_000
2628
2729interface DocuSignAccountInfo {
2830 accountId : string
@@ -47,14 +49,41 @@ function docusignError(data: Record<string, unknown>, fallback: string): string
4749 )
4850}
4951
52+ async function fetchDocusign (
53+ input : string ,
54+ init : RequestInit = { } ,
55+ parentSignal ?: AbortSignal
56+ ) : Promise < Response > {
57+ const controller = new AbortController ( )
58+ const timeout = setTimeout ( ( ) => {
59+ controller . abort ( new Error ( 'DocuSign request timed out' ) )
60+ } , DOCUSIGN_FETCH_TIMEOUT_MS )
61+ const abort = ( ) => controller . abort ( parentSignal ?. reason ?? new Error ( 'Request aborted' ) )
62+ parentSignal ?. addEventListener ( 'abort' , abort , { once : true } )
63+
64+ try {
65+ return await fetch ( input , { ...init , signal : controller . signal } )
66+ } finally {
67+ clearTimeout ( timeout )
68+ parentSignal ?. removeEventListener ( 'abort' , abort )
69+ }
70+ }
71+
5072/**
5173 * Resolves the user's DocuSign account info from their access token
5274 * by calling the DocuSign userinfo endpoint.
5375 */
54- async function resolveAccount ( accessToken : string ) : Promise < DocuSignAccountInfo > {
55- const response = await fetch ( 'https://account-d.docusign.com/oauth/userinfo' , {
56- headers : { Authorization : `Bearer ${ accessToken } ` } ,
57- } )
76+ async function resolveAccount (
77+ accessToken : string ,
78+ signal ?: AbortSignal
79+ ) : Promise < DocuSignAccountInfo > {
80+ const response = await fetchDocusign (
81+ 'https://account-d.docusign.com/oauth/userinfo' ,
82+ {
83+ headers : { Authorization : `Bearer ${ accessToken } ` } ,
84+ } ,
85+ signal
86+ )
5887
5988 if ( ! response . ok ) {
6089 const errorText = await readResponseTextWithLimit ( response , {
@@ -120,7 +149,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
120149 const { accessToken, operation, ...params } = parsed . data . body
121150
122151 try {
123- const account = await resolveAccount ( accessToken )
152+ const account = await resolveAccount ( accessToken , request . signal )
124153 const apiBase = `${ account . baseUri } /restapi/v2.1/accounts/${ account . accountId } `
125154 const headers : Record < string , string > = {
126155 Authorization : `Bearer ${ accessToken } ` ,
@@ -129,21 +158,27 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
129158
130159 switch ( operation ) {
131160 case 'send_envelope' :
132- return await handleSendEnvelope ( apiBase , headers , params , authResult . userId )
161+ return await handleSendEnvelope ( apiBase , headers , params , authResult . userId , request . signal )
133162 case 'create_from_template' :
134- return await handleCreateFromTemplate ( apiBase , headers , params )
163+ return await handleCreateFromTemplate ( apiBase , headers , params , request . signal )
135164 case 'get_envelope' :
136- return await handleGetEnvelope ( apiBase , headers , params )
165+ return await handleGetEnvelope ( apiBase , headers , params , request . signal )
137166 case 'list_envelopes' :
138- return await handleListEnvelopes ( apiBase , headers , params )
167+ return await handleListEnvelopes ( apiBase , headers , params , request . signal )
139168 case 'void_envelope' :
140- return await handleVoidEnvelope ( apiBase , headers , params )
169+ return await handleVoidEnvelope ( apiBase , headers , params , request . signal )
141170 case 'download_document' :
142- return await handleDownloadDocument ( apiBase , headers , params , authResult . userId )
171+ return await handleDownloadDocument (
172+ apiBase ,
173+ headers ,
174+ params ,
175+ authResult . userId ,
176+ request . signal
177+ )
143178 case 'list_templates' :
144- return await handleListTemplates ( apiBase , headers , params )
179+ return await handleListTemplates ( apiBase , headers , params , request . signal )
145180 case 'list_recipients' :
146- return await handleListRecipients ( apiBase , headers , params )
181+ return await handleListRecipients ( apiBase , headers , params , request . signal )
147182 default :
148183 return NextResponse . json (
149184 { success : false , error : `Unknown operation: ${ operation } ` } ,
@@ -164,7 +199,8 @@ async function handleSendEnvelope(
164199 apiBase : string ,
165200 headers : Record < string , string > ,
166201 params : Record < string , unknown > ,
167- userId : string
202+ userId : string ,
203+ signal ?: AbortSignal
168204) {
169205 const { signerEmail, signerName, emailSubject, emailBody, ccEmail, ccName, file, status } = params
170206
@@ -276,11 +312,15 @@ async function handleSendEnvelope(
276312 )
277313 }
278314
279- const response = await fetch ( `${ apiBase } /envelopes` , {
280- method : 'POST' ,
281- headers,
282- body : JSON . stringify ( envelopeBody ) ,
283- } )
315+ const response = await fetchDocusign (
316+ `${ apiBase } /envelopes` ,
317+ {
318+ method : 'POST' ,
319+ headers,
320+ body : JSON . stringify ( envelopeBody ) ,
321+ } ,
322+ signal
323+ )
284324
285325 const data = await readDocusignJson ( response , 'DocuSign send envelope response' )
286326 if ( ! response . ok ) {
@@ -297,7 +337,8 @@ async function handleSendEnvelope(
297337async function handleCreateFromTemplate (
298338 apiBase : string ,
299339 headers : Record < string , string > ,
300- params : Record < string , unknown >
340+ params : Record < string , unknown > ,
341+ signal ?: AbortSignal
301342) {
302343 const { templateId, emailSubject, emailBody, templateRoles, status } = params
303344
@@ -330,11 +371,15 @@ async function handleCreateFromTemplate(
330371 if ( emailSubject ) envelopeBody . emailSubject = emailSubject
331372 if ( emailBody ) envelopeBody . emailBlurb = emailBody
332373
333- const response = await fetch ( `${ apiBase } /envelopes` , {
334- method : 'POST' ,
335- headers,
336- body : JSON . stringify ( envelopeBody ) ,
337- } )
374+ const response = await fetchDocusign (
375+ `${ apiBase } /envelopes` ,
376+ {
377+ method : 'POST' ,
378+ headers,
379+ body : JSON . stringify ( envelopeBody ) ,
380+ } ,
381+ signal
382+ )
338383
339384 const data = await readDocusignJson ( response , 'DocuSign create from template response' )
340385 if ( ! response . ok ) {
@@ -354,16 +399,18 @@ async function handleCreateFromTemplate(
354399async function handleGetEnvelope (
355400 apiBase : string ,
356401 headers : Record < string , string > ,
357- params : Record < string , unknown >
402+ params : Record < string , unknown > ,
403+ signal ?: AbortSignal
358404) {
359405 const { envelopeId } = params
360406 if ( ! envelopeId ) {
361407 return NextResponse . json ( { success : false , error : 'envelopeId is required' } , { status : 400 } )
362408 }
363409
364- const response = await fetch (
410+ const response = await fetchDocusign (
365411 `${ apiBase } /envelopes/${ ( envelopeId as string ) . trim ( ) } ?include=recipients,documents` ,
366- { headers }
412+ { headers } ,
413+ signal
367414 )
368415 const data = await readDocusignJson ( response , 'DocuSign envelope response' )
369416
@@ -380,7 +427,8 @@ async function handleGetEnvelope(
380427async function handleListEnvelopes (
381428 apiBase : string ,
382429 headers : Record < string , string > ,
383- params : Record < string , unknown >
430+ params : Record < string , unknown > ,
431+ signal ?: AbortSignal
384432) {
385433 const queryParams = new URLSearchParams ( )
386434
@@ -398,7 +446,7 @@ async function handleListEnvelopes(
398446 if ( params . searchText ) queryParams . append ( 'search_text' , params . searchText as string )
399447 if ( params . count ) queryParams . append ( 'count' , params . count as string )
400448
401- const response = await fetch ( `${ apiBase } /envelopes?${ queryParams } ` , { headers } )
449+ const response = await fetchDocusign ( `${ apiBase } /envelopes?${ queryParams } ` , { headers } , signal )
402450 const data = await readDocusignJson ( response , 'DocuSign envelope list response' )
403451
404452 if ( ! response . ok ) {
@@ -414,7 +462,8 @@ async function handleListEnvelopes(
414462async function handleVoidEnvelope (
415463 apiBase : string ,
416464 headers : Record < string , string > ,
417- params : Record < string , unknown >
465+ params : Record < string , unknown > ,
466+ signal ?: AbortSignal
418467) {
419468 const { envelopeId, voidedReason } = params
420469 if ( ! envelopeId ) {
@@ -424,11 +473,15 @@ async function handleVoidEnvelope(
424473 return NextResponse . json ( { success : false , error : 'voidedReason is required' } , { status : 400 } )
425474 }
426475
427- const response = await fetch ( `${ apiBase } /envelopes/${ ( envelopeId as string ) . trim ( ) } ` , {
428- method : 'PUT' ,
429- headers,
430- body : JSON . stringify ( { status : 'voided' , voidedReason } ) ,
431- } )
476+ const response = await fetchDocusign (
477+ `${ apiBase } /envelopes/${ ( envelopeId as string ) . trim ( ) } ` ,
478+ {
479+ method : 'PUT' ,
480+ headers,
481+ body : JSON . stringify ( { status : 'voided' , voidedReason } ) ,
482+ } ,
483+ signal
484+ )
432485
433486 const data = await readDocusignJson ( response , 'DocuSign void envelope response' )
434487 if ( ! response . ok ) {
@@ -445,7 +498,8 @@ async function handleDownloadDocument(
445498 apiBase : string ,
446499 headers : Record < string , string > ,
447500 params : Record < string , unknown > ,
448- userId : string
501+ userId : string ,
502+ signal ?: AbortSignal
449503) {
450504 const { envelopeId, documentId } = params
451505 if ( ! envelopeId ) {
@@ -454,11 +508,12 @@ async function handleDownloadDocument(
454508
455509 const docId = ( documentId as string ) || 'combined'
456510
457- const response = await fetch (
511+ const response = await fetchDocusign (
458512 `${ apiBase } /envelopes/${ ( envelopeId as string ) . trim ( ) } /documents/${ docId } ` ,
459513 {
460514 headers : { Authorization : headers . Authorization } ,
461- }
515+ } ,
516+ signal
462517 )
463518
464519 if ( ! response . ok ) {
@@ -494,6 +549,10 @@ async function handleDownloadDocument(
494549 const workspaceId = typeof params . workspaceId === 'string' ? params . workspaceId : undefined
495550 const workflowId = typeof params . workflowId === 'string' ? params . workflowId : undefined
496551 const executionId = typeof params . executionId === 'string' ? params . executionId : undefined
552+ const legacyInlineContent =
553+ buffer . length <= MAX_LEGACY_INLINE_DOCUMENT_BYTES
554+ ? { base64Content : buffer . toString ( 'base64' ) }
555+ : { }
497556
498557 if ( workspaceId && workflowId && executionId ) {
499558 const file = await uploadExecutionFile (
@@ -506,6 +565,7 @@ async function handleDownloadDocument(
506565 file,
507566 mimeType : contentType ,
508567 fileName,
568+ ...legacyInlineContent ,
509569 } )
510570 }
511571
@@ -516,13 +576,14 @@ async function handleDownloadDocument(
516576 userId,
517577 } )
518578
519- return NextResponse . json ( { file, mimeType : contentType , fileName } )
579+ return NextResponse . json ( { file, mimeType : contentType , fileName, ... legacyInlineContent } )
520580}
521581
522582async function handleListTemplates (
523583 apiBase : string ,
524584 headers : Record < string , string > ,
525- params : Record < string , unknown >
585+ params : Record < string , unknown > ,
586+ signal ?: AbortSignal
526587) {
527588 const queryParams = new URLSearchParams ( )
528589 if ( params . searchText ) queryParams . append ( 'search_text' , params . searchText as string )
@@ -531,7 +592,7 @@ async function handleListTemplates(
531592 const queryString = queryParams . toString ( )
532593 const url = queryString ? `${ apiBase } /templates?${ queryString } ` : `${ apiBase } /templates`
533594
534- const response = await fetch ( url , { headers } )
595+ const response = await fetchDocusign ( url , { headers } , signal )
535596 const data = await readDocusignJson ( response , 'DocuSign template list response' )
536597
537598 if ( ! response . ok ) {
@@ -547,16 +608,21 @@ async function handleListTemplates(
547608async function handleListRecipients (
548609 apiBase : string ,
549610 headers : Record < string , string > ,
550- params : Record < string , unknown >
611+ params : Record < string , unknown > ,
612+ signal ?: AbortSignal
551613) {
552614 const { envelopeId } = params
553615 if ( ! envelopeId ) {
554616 return NextResponse . json ( { success : false , error : 'envelopeId is required' } , { status : 400 } )
555617 }
556618
557- const response = await fetch ( `${ apiBase } /envelopes/${ ( envelopeId as string ) . trim ( ) } /recipients` , {
558- headers,
559- } )
619+ const response = await fetchDocusign (
620+ `${ apiBase } /envelopes/${ ( envelopeId as string ) . trim ( ) } /recipients` ,
621+ {
622+ headers,
623+ } ,
624+ signal
625+ )
560626 const data = await readDocusignJson ( response , 'DocuSign recipients response' )
561627
562628 if ( ! response . ok ) {
0 commit comments