11import * as http from "http" ;
2+ import crypto from "node:crypto" ;
23import * as Lark from "@larksuiteoapi/node-sdk" ;
34import {
45 applyBasicWebhookRequestGuards ,
6+ readJsonBodyWithLimit ,
57 type RuntimeEnv ,
68 installRequestBodyLimitGuard ,
79} from "openclaw/plugin-sdk/feishu" ;
@@ -26,6 +28,50 @@ export type MonitorTransportParams = {
2628 eventDispatcher : Lark . EventDispatcher ;
2729} ;
2830
31+ function isFeishuWebhookPayload ( value : unknown ) : value is Record < string , unknown > {
32+ return ! ! value && typeof value === "object" && ! Array . isArray ( value ) ;
33+ }
34+
35+ function buildFeishuWebhookEnvelope (
36+ req : http . IncomingMessage ,
37+ payload : Record < string , unknown > ,
38+ ) : Record < string , unknown > {
39+ return Object . assign ( Object . create ( { headers : req . headers } ) , payload ) as Record < string , unknown > ;
40+ }
41+
42+ function isFeishuWebhookSignatureValid ( params : {
43+ headers : http . IncomingHttpHeaders ;
44+ payload : Record < string , unknown > ;
45+ encryptKey ?: string ;
46+ } ) : boolean {
47+ const encryptKey = params . encryptKey ?. trim ( ) ;
48+ if ( ! encryptKey ) {
49+ return true ;
50+ }
51+
52+ const timestampHeader = params . headers [ "x-lark-request-timestamp" ] ;
53+ const nonceHeader = params . headers [ "x-lark-request-nonce" ] ;
54+ const signatureHeader = params . headers [ "x-lark-signature" ] ;
55+ const timestamp = Array . isArray ( timestampHeader ) ? timestampHeader [ 0 ] : timestampHeader ;
56+ const nonce = Array . isArray ( nonceHeader ) ? nonceHeader [ 0 ] : nonceHeader ;
57+ const signature = Array . isArray ( signatureHeader ) ? signatureHeader [ 0 ] : signatureHeader ;
58+ if ( ! timestamp || ! nonce || ! signature ) {
59+ return false ;
60+ }
61+
62+ const computedSignature = crypto
63+ . createHash ( "sha256" )
64+ . update ( timestamp + nonce + encryptKey + JSON . stringify ( params . payload ) )
65+ . digest ( "hex" ) ;
66+ return computedSignature === signature ;
67+ }
68+
69+ function respondText ( res : http . ServerResponse , statusCode : number , body : string ) : void {
70+ res . statusCode = statusCode ;
71+ res . setHeader ( "Content-Type" , "text/plain; charset=utf-8" ) ;
72+ res . end ( body ) ;
73+ }
74+
2975export async function monitorWebSocket ( {
3076 account,
3177 accountId,
@@ -88,7 +134,6 @@ export async function monitorWebhook({
88134 log ( `feishu[${ accountId } ]: starting Webhook server on ${ host } :${ port } , path ${ path } ...` ) ;
89135
90136 const server = http . createServer ( ) ;
91- const webhookHandler = Lark . adaptDefault ( path , eventDispatcher , { autoChallenge : true } ) ;
92137
93138 server . on ( "request" , ( req , res ) => {
94139 res . on ( "finish" , ( ) => {
@@ -118,15 +163,68 @@ export async function monitorWebhook({
118163 return ;
119164 }
120165
121- void Promise . resolve ( webhookHandler ( req , res ) )
122- . catch ( ( err ) => {
166+ void ( async ( ) => {
167+ try {
168+ const bodyResult = await readJsonBodyWithLimit ( req , {
169+ maxBytes : FEISHU_WEBHOOK_MAX_BODY_BYTES ,
170+ timeoutMs : FEISHU_WEBHOOK_BODY_TIMEOUT_MS ,
171+ } ) ;
172+ if ( guard . isTripped ( ) || res . writableEnded ) {
173+ return ;
174+ }
175+ if ( ! bodyResult . ok ) {
176+ if ( bodyResult . code === "INVALID_JSON" ) {
177+ respondText ( res , 400 , "Invalid JSON" ) ;
178+ }
179+ return ;
180+ }
181+ if ( ! isFeishuWebhookPayload ( bodyResult . value ) ) {
182+ respondText ( res , 400 , "Invalid JSON" ) ;
183+ return ;
184+ }
185+
186+ // Lark's default adapter drops invalid signatures as an empty 200. Reject here instead.
187+ if (
188+ ! isFeishuWebhookSignatureValid ( {
189+ headers : req . headers ,
190+ payload : bodyResult . value ,
191+ encryptKey : account . encryptKey ,
192+ } )
193+ ) {
194+ respondText ( res , 401 , "Invalid signature" ) ;
195+ return ;
196+ }
197+
198+ const { isChallenge, challenge } = Lark . generateChallenge ( bodyResult . value , {
199+ encryptKey : account . encryptKey ?? "" ,
200+ } ) ;
201+ if ( isChallenge ) {
202+ res . statusCode = 200 ;
203+ res . setHeader ( "Content-Type" , "application/json; charset=utf-8" ) ;
204+ res . end ( JSON . stringify ( challenge ) ) ;
205+ return ;
206+ }
207+
208+ const value = await eventDispatcher . invoke (
209+ buildFeishuWebhookEnvelope ( req , bodyResult . value ) ,
210+ { needCheck : false } ,
211+ ) ;
212+ if ( ! res . headersSent ) {
213+ res . statusCode = 200 ;
214+ res . setHeader ( "Content-Type" , "application/json; charset=utf-8" ) ;
215+ res . end ( JSON . stringify ( value ) ) ;
216+ }
217+ } catch ( err ) {
123218 if ( ! guard . isTripped ( ) ) {
124219 error ( `feishu[${ accountId } ]: webhook handler error: ${ String ( err ) } ` ) ;
220+ if ( ! res . headersSent ) {
221+ respondText ( res , 500 , "Internal Server Error" ) ;
222+ }
125223 }
126- } )
127- . finally ( ( ) => {
224+ } finally {
128225 guard . dispose ( ) ;
129- } ) ;
226+ }
227+ } ) ( ) ;
130228 } ) ;
131229
132230 httpServers . set ( accountId , server ) ;
0 commit comments