22import { randomUUID } from "node:crypto" ;
33import type {
44 ConnectParams ,
5+ ErrorShape ,
56 EventFrame ,
67 HelloOk ,
78 RequestFrame ,
8- ResponseFrame ,
99} from "@openclaw/gateway-protocol" ;
1010import {
1111 GATEWAY_CLIENT_MODES ,
@@ -21,6 +21,10 @@ import {
2121 readPairingConnectErrorDetails ,
2222 type ConnectErrorRecoveryAdvice ,
2323} from "@openclaw/gateway-protocol/connect-error-details" ;
24+ import {
25+ isGatewayEventFrame ,
26+ isGatewayResponseFrame ,
27+ } from "@openclaw/gateway-protocol/frame-guards" ;
2428import { resolveGatewayStartupRetryAfterMs } from "@openclaw/gateway-protocol/startup-unavailable" ;
2529import { MIN_CLIENT_PROTOCOL_VERSION , PROTOCOL_VERSION } from "@openclaw/gateway-protocol/version" ;
2630import ipaddr from "ipaddr.js" ;
@@ -78,63 +82,6 @@ function normalizeOptionalString(value: unknown): string | undefined {
7882 return trimmed || undefined ;
7983}
8084
81- function isRecord ( value : unknown ) : value is Record < string , unknown > {
82- return Boolean ( value ) && typeof value === "object" && ! Array . isArray ( value ) ;
83- }
84-
85- function isNonEmptyString ( value : unknown ) : value is string {
86- return typeof value === "string" && value . length > 0 ;
87- }
88-
89- function isNonNegativeInteger ( value : unknown ) : value is number {
90- return typeof value === "number" && Number . isInteger ( value ) && value >= 0 ;
91- }
92-
93- function isGatewayClientErrorShape ( value : unknown ) : boolean {
94- if ( ! isRecord ( value ) ) {
95- return false ;
96- }
97- if ( ! isNonEmptyString ( value . code ) || ! isNonEmptyString ( value . message ) ) {
98- return false ;
99- }
100- if ( value . retryable !== undefined && typeof value . retryable !== "boolean" ) {
101- return false ;
102- }
103- if ( value . retryAfterMs !== undefined && ! isNonNegativeInteger ( value . retryAfterMs ) ) {
104- return false ;
105- }
106- return true ;
107- }
108-
109- function isGatewayEventFrame ( value : unknown ) : value is EventFrame {
110- if ( ! isRecord ( value ) || value . type !== "event" || ! isNonEmptyString ( value . event ) ) {
111- return false ;
112- }
113- return value . seq === undefined || isNonNegativeInteger ( value . seq ) ;
114- }
115-
116- function isGatewayResponseFrame ( value : unknown ) : value is ResponseFrame {
117- if (
118- ! isRecord ( value ) ||
119- value . type !== "res" ||
120- ! isNonEmptyString ( value . id ) ||
121- typeof value . ok !== "boolean"
122- ) {
123- return false ;
124- }
125- return value . error === undefined || isGatewayClientErrorShape ( value . error ) ;
126- }
127-
128- function validateClientRequestFrame ( frame : RequestFrame ) : string | null {
129- if ( ! isNonEmptyString ( frame . id ) ) {
130- return "id must be a non-empty string" ;
131- }
132- if ( ! isNonEmptyString ( frame . method ) ) {
133- return "method must be a non-empty string" ;
134- }
135- return null ;
136- }
137-
13885function normalizeLowercaseStringOrEmpty ( value : unknown ) : string {
13986 return typeof value === "string" ? value . trim ( ) . toLowerCase ( ) : "" ;
14087}
@@ -314,14 +261,6 @@ export type GatewayClientRequestOptions = {
314261 onAccepted ?: ( payload : unknown ) => void ;
315262} ;
316263
317- type GatewayClientErrorShape = {
318- code ?: string ;
319- message ?: string ;
320- details ?: unknown ;
321- retryable ?: boolean ;
322- retryAfterMs ?: number ;
323- } ;
324-
325264type SelectedConnectAuth = {
326265 authToken ?: string ;
327266 authBootstrapToken ?: string ;
@@ -376,7 +315,7 @@ export class GatewayClientRequestError extends Error {
376315 readonly retryable : boolean ;
377316 readonly retryAfterMs ?: number ;
378317
379- constructor ( error : GatewayClientErrorShape ) {
318+ constructor ( error : Partial < ErrorShape > ) {
380319 super ( formatConnectErrorMessage ( { message : error . message , details : error . details } ) ) ;
381320 this . name = "GatewayClientRequestError" ;
382321 this . gatewayCode = error . code ?? "UNAVAILABLE" ;
@@ -461,6 +400,13 @@ export type GatewayClientOptions = {
461400 onGap ?: ( info : { expected : number ; received : number } ) => void ;
462401} ;
463402
403+ export type GatewayClientConnectionMetadata = {
404+ clientName ?: GatewayClientName ;
405+ hasDeviceIdentity : boolean ;
406+ mode ?: GatewayClientMode ;
407+ preauthHandshakeTimeoutMs ?: number ;
408+ } ;
409+
464410export const GATEWAY_CLOSE_CODE_HINTS : Readonly < Record < number , string > > = {
465411 1000 : "normal closure" ,
466412 1006 : "abnormal closure (no close frame)" ,
@@ -595,6 +541,15 @@ export class GatewayClient {
595541 : 30_000 ;
596542 }
597543
544+ getConnectionMetadata ( ) : GatewayClientConnectionMetadata {
545+ return {
546+ clientName : this . opts . clientName ,
547+ hasDeviceIdentity : Boolean ( this . opts . deviceIdentity ) ,
548+ mode : this . opts . mode ,
549+ preauthHandshakeTimeoutMs : this . opts . preauthHandshakeTimeoutMs ,
550+ } ;
551+ }
552+
598553 start ( ) {
599554 if ( this . closed ) {
600555 return ;
@@ -1639,12 +1594,11 @@ export class GatewayClient {
16391594 if ( opts ?. signal ?. aborted ) {
16401595 throw createGatewayRequestAbortError ( method ) ;
16411596 }
1597+ if ( typeof method !== "string" || method . length === 0 ) {
1598+ throw new Error ( "invalid request frame: method must be a non-empty string" ) ;
1599+ }
16421600 const id = randomUUID ( ) ;
16431601 const frame : RequestFrame = { type : "req" , id, method, params } ;
1644- const requestFrameError = validateClientRequestFrame ( frame ) ;
1645- if ( requestFrameError ) {
1646- throw new Error ( `invalid request frame: ${ requestFrameError } ` ) ;
1647- }
16481602 const expectFinal = opts ?. expectFinal === true ;
16491603 const timeoutMs =
16501604 opts ?. timeoutMs === null
0 commit comments