@@ -826,15 +826,12 @@ export class GatewayManager extends EventEmitter {
826826 reject ( err ) ;
827827 } ;
828828
829- this . ws . on ( 'open' , async ( ) => {
830- logger . debug ( 'Gateway WebSocket opened, sending connect handshake' ) ;
831-
832- // Re-fetch token here before generating payload just in case it updated while connecting
829+ // Sends the connect frame using the server-issued challenge nonce.
830+ const sendConnectHandshake = async ( challengeNonce : string ) => {
831+ logger . debug ( 'Sending connect handshake with challenge nonce' ) ;
832+
833833 const currentToken = await getSetting ( 'gatewayToken' ) ;
834-
835- // Send proper connect handshake as required by OpenClaw Gateway protocol
836- // The Gateway expects: { type: "req", id: "...", method: "connect", params: ConnectParams }
837- // Since 2026.2.15, scopes are only granted when a signed device identity is included.
834+
838835 connectId = `connect-${ Date . now ( ) } ` ;
839836 const role = 'operator' ;
840837 const scopes = [ 'operator.admin' ] ;
@@ -844,7 +841,7 @@ export class GatewayManager extends EventEmitter {
844841
845842 const device = ( ( ) => {
846843 if ( ! this . deviceIdentity ) return undefined ;
847-
844+
848845 const payload = buildDeviceAuthPayload ( {
849846 deviceId : this . deviceIdentity . deviceId ,
850847 clientId,
@@ -853,13 +850,15 @@ export class GatewayManager extends EventEmitter {
853850 scopes,
854851 signedAtMs,
855852 token : currentToken ?? null ,
853+ nonce : challengeNonce ,
856854 } ) ;
857855 const signature = signDevicePayload ( this . deviceIdentity . privateKeyPem , payload ) ;
858856 return {
859857 id : this . deviceIdentity . deviceId ,
860858 publicKey : publicKeyRawBase64UrlFromPem ( this . deviceIdentity . publicKeyPem ) ,
861859 signature,
862860 signedAt : signedAtMs ,
861+ nonce : challengeNonce ,
863862 } ;
864863 } ) ( ) ;
865864
@@ -886,10 +885,9 @@ export class GatewayManager extends EventEmitter {
886885 device,
887886 } ,
888887 } ;
889-
888+
890889 this . ws ?. send ( JSON . stringify ( connectFrame ) ) ;
891-
892- // Store pending connect request
890+
893891 const requestTimeout = setTimeout ( ( ) => {
894892 if ( ! handshakeComplete ) {
895893 logger . error ( 'Gateway connect handshake timed out' ) ;
@@ -917,11 +915,35 @@ export class GatewayManager extends EventEmitter {
917915 } ,
918916 timeout : requestTimeout ,
919917 } ) ;
918+ } ;
919+
920+ this . ws . on ( 'open' , ( ) => {
921+ logger . debug ( 'Gateway WebSocket opened, waiting for connect.challenge...' ) ;
920922 } ) ;
921923
924+ let challengeReceived = false ;
925+
922926 this . ws . on ( 'message' , ( data ) => {
923927 try {
924928 const message = JSON . parse ( data . toString ( ) ) ;
929+
930+ // Intercept the connect.challenge event before the general handler
931+ if (
932+ ! challengeReceived &&
933+ typeof message === 'object' && message !== null &&
934+ message . type === 'event' && message . event === 'connect.challenge'
935+ ) {
936+ challengeReceived = true ;
937+ const nonce = message . payload ?. nonce as string | undefined ;
938+ if ( ! nonce ) {
939+ rejectOnce ( new Error ( 'Gateway connect.challenge missing nonce' ) ) ;
940+ return ;
941+ }
942+ logger . debug ( 'Received connect.challenge, sending handshake' ) ;
943+ sendConnectHandshake ( nonce ) ;
944+ return ;
945+ }
946+
925947 this . handleMessage ( message ) ;
926948 } catch ( error ) {
927949 logger . debug ( 'Failed to parse Gateway WebSocket message:' , error ) ;
0 commit comments