@@ -33,6 +33,7 @@ wit_bindgen::generate!({
3333} ) ;
3434
3535use serde:: { Deserialize , Serialize } ;
36+ use subtle:: ConstantTimeEq ;
3637
3738// Re-export generated types
3839use exports:: near:: agent:: channel:: {
@@ -104,6 +105,10 @@ struct FeishuEventHeader {
104105 /// Tenant key.
105106 #[ serde( default ) ]
106107 tenant_key : Option < String > ,
108+
109+ /// Verification token for v2 event payloads.
110+ #[ serde( default ) ]
111+ token : Option < String > ,
107112}
108113
109114/// Message receive event payload (im.message.receive_v1).
@@ -305,11 +310,8 @@ impl Guest for FeishuChannel {
305310 if let Some ( ref app_secret) = config. app_secret {
306311 let _ = channel_host:: workspace_write ( APP_SECRET_PATH , app_secret) ;
307312 }
308- if let Some ( ref verification_token) = config. verification_token {
309- let _ = channel_host:: workspace_write ( VERIFICATION_TOKEN_PATH , verification_token) ;
310- } else {
311- let _ = channel_host:: workspace_write ( VERIFICATION_TOKEN_PATH , "" ) ;
312- }
313+ let verification_token = config. verification_token . as_deref ( ) . unwrap_or ( "" ) ;
314+ let _ = channel_host:: workspace_write ( VERIFICATION_TOKEN_PATH , verification_token) ;
313315
314316 if let Some ( owner_id) = & config. owner_id {
315317 let _ = channel_host:: workspace_write ( OWNER_ID_PATH , owner_id) ;
@@ -391,7 +393,7 @@ impl Guest for FeishuChannel {
391393 if !is_authenticated_webhook (
392394 req. secret_validated ,
393395 configured_token. as_deref ( ) ,
394- event . token . as_deref ( ) ,
396+ request_verification_token ( & event ) ,
395397 ) {
396398 channel_host:: log (
397399 channel_host:: LogLevel :: Warn ,
@@ -876,11 +878,21 @@ fn is_authenticated_webhook(
876878 }
877879
878880 match ( configured_token, request_token) {
879- ( Some ( expected) , Some ( provided) ) => expected == provided,
881+ ( Some ( expected) , Some ( provided) ) => {
882+ bool:: from ( expected. as_bytes ( ) . ct_eq ( provided. as_bytes ( ) ) )
883+ }
880884 _ => false ,
881885 }
882886}
883887
888+ fn request_verification_token ( event : & FeishuEvent ) -> Option < & str > {
889+ event
890+ . header
891+ . as_ref ( )
892+ . and_then ( |header| header. token . as_deref ( ) )
893+ . or ( event. token . as_deref ( ) )
894+ }
895+
884896#[ cfg( test) ]
885897mod tests {
886898 use super :: * ;
@@ -967,4 +979,36 @@ mod tests {
967979 "host authentication should take precedence over body token checks"
968980 ) ;
969981 }
982+
983+ #[ test]
984+ fn request_verification_token_prefers_v2_header_token ( ) {
985+ let event: FeishuEvent = serde_json:: from_str (
986+ r#"{
987+ "schema": "2.0",
988+ "header": {
989+ "event_id": "evt_123",
990+ "event_type": "im.message.receive_v1",
991+ "token": "header-token"
992+ },
993+ "event": {}
994+ }"# ,
995+ )
996+ . unwrap ( ) ;
997+
998+ assert_eq ! ( request_verification_token( & event) , Some ( "header-token" ) ) ;
999+ }
1000+
1001+ #[ test]
1002+ fn request_verification_token_falls_back_to_top_level_token ( ) {
1003+ let event: FeishuEvent = serde_json:: from_str (
1004+ r#"{
1005+ "type": "url_verification",
1006+ "challenge": "abc",
1007+ "token": "top-level-token"
1008+ }"# ,
1009+ )
1010+ . unwrap ( ) ;
1011+
1012+ assert_eq ! ( request_verification_token( & event) , Some ( "top-level-token" ) ) ;
1013+ }
9701014}
0 commit comments