@@ -12,20 +12,53 @@ type MulterFile = {
1212
1313type ExpressMulterRequest = { file ?: MulterFile } ;
1414
15+ const currentPageContextSchema = z . object ( {
16+ path : z . string ( ) ,
17+ fullPath : z . string ( ) ,
18+ title : z . string ( ) ,
19+ url : z . string ( ) ,
20+ } ) . strict ( ) satisfies z . ZodType < CurrentPageContext > ;
21+
1522const agentResponseBodySchema = z . object ( {
1623 message : z . string ( ) ,
1724 sessionId : z . string ( ) ,
1825 mode : z . string ( ) . nullish ( ) ,
1926 timeZone : z . string ( ) . optional ( ) ,
20- currentPage : z . custom < CurrentPageContext > ( ) . optional ( ) ,
27+ currentPage : currentPageContextSchema . optional ( ) ,
2128} ) . strict ( ) ;
2229
2330const agentApprovalBodySchema = z . object ( {
2431 sessionId : z . string ( ) ,
2532 decision : z . enum ( [ "approve" , "reject" ] ) ,
2633} ) . strict ( ) ;
2734
28- const agentSpeechResponseBodySchema = agentResponseBodySchema . omit ( { message : true } ) ;
35+ // Sent as multipart/form-data (via multer), so every field arrives as a plain string on the wire.
36+ // `currentPage` is JSON.stringify'd by the client into a form field and must be parsed back into
37+ // an object. Zod -> JSON-Schema conversion cannot represent `.transform()` at all (it throws), so
38+ // `agentSpeechResponseShapeSchema` (plain strings, no transform) is what's passed as request_schema
39+ // for the AJV pre-check, while `agentSpeechResponseBodySchema` (with the transform) is used via an
40+ // inline safeParse in the handler to actually parse `currentPage` into an object.
41+ const agentSpeechResponseShapeSchema = z . object ( {
42+ sessionId : z . string ( ) ,
43+ mode : z . string ( ) . nullish ( ) ,
44+ timeZone : z . string ( ) . optional ( ) ,
45+ currentPage : z . string ( ) . optional ( ) ,
46+ } ) . strict ( ) ;
47+
48+ const agentSpeechResponseBodySchema = agentSpeechResponseShapeSchema . extend ( {
49+ currentPage : z . string ( ) . optional ( ) . transform ( ( value , ctx ) => {
50+ if ( value === undefined ) return undefined ;
51+ try {
52+ return currentPageContextSchema . parse ( JSON . parse ( value ) ) ;
53+ } catch ( err ) {
54+ ctx . addIssue ( {
55+ code : z . ZodIssueCode . custom ,
56+ message : `currentPage must be a JSON-encoded object matching CurrentPageContext: ${ err instanceof Error ? err . message : String ( err ) } ` ,
57+ } ) ;
58+ return z . NEVER ;
59+ }
60+ } ) ,
61+ } ) ;
2962
3063export function setupCoreEndpoints ( ctx : CoreEndpointsContext , server : IHttpServer ) {
3164 server . endpoint ( {
@@ -52,9 +85,9 @@ export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServe
5285 server . endpoint ( {
5386 method : 'POST' ,
5487 path : `/agent/response` ,
88+ request_schema : agentResponseBodySchema ,
5589 handler : async ( { body, adminUser, response, _raw_express_res, abortSignal } ) => {
56- const data = ctx . parseBody ( agentResponseBodySchema , body , response ) ;
57- if ( ! data ) return ;
90+ const data = body as z . infer < typeof agentResponseBodySchema > ;
5891 const emit = createSseEventEmitter ( _raw_express_res , {
5992 vercelAiUiMessageStream : true ,
6093 closeActiveBlockOnToolStart : true ,
@@ -79,9 +112,9 @@ export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServe
79112 server . endpoint ( {
80113 method : 'POST' ,
81114 path : `/agent/approval` ,
115+ request_schema : agentApprovalBodySchema ,
82116 handler : async ( { body, adminUser, response, _raw_express_res, abortSignal } ) => {
83- const data = ctx . parseBody ( agentApprovalBodySchema , body , response ) ;
84- if ( ! data ) return ;
117+ const data = body as z . infer < typeof agentApprovalBodySchema > ;
85118 const emit = createSseEventEmitter ( _raw_express_res , {
86119 vercelAiUiMessageStream : true ,
87120 closeActiveBlockOnToolStart : true ,
@@ -105,15 +138,20 @@ export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServe
105138 method : 'POST' ,
106139 path : `/agent/speech-response` ,
107140 target : 'upload' ,
141+ request_schema : agentSpeechResponseShapeSchema ,
108142 handler : async ( { body, adminUser, response, _raw_express_req, _raw_express_res, abortSignal } ) => {
109143 const req = _raw_express_req as ExpressMulterRequest ;
110144 const audioAdapter = ctx . options . audioAdapter ;
111145 if ( ! audioAdapter ) {
112146 response . setStatus ( 400 , "Audio adapter is not configured for AdminForth Agent" ) ;
113147 return { error : "Audio adapter is not configured for AdminForth Agent" } ;
114148 }
115- const data = ctx . parseBody ( agentSpeechResponseBodySchema , body , response ) ;
116- if ( ! data ) return ;
149+ const parsed = agentSpeechResponseBodySchema . safeParse ( body ) ;
150+ if ( ! parsed . success ) {
151+ response . setStatus ( 400 , 'Request body validation failed' ) ;
152+ return { error : 'Request body validation failed' , details : parsed . error . issues } ;
153+ }
154+ const data = parsed . data ;
117155 if ( ! req . file ) {
118156 response . setStatus ( 400 , "Audio file is required" ) ;
119157 return { error : "Audio file is required" } ;
0 commit comments