@@ -52,11 +52,13 @@ export interface DryRunResponse {
5252 resolvedVersion : string | null
5353}
5454
55- interface ErrorEnvelope {
56- msg ?: unknown
57- requestId ?: unknown
55+ interface PublicErrorFields {
56+ msg ?: string
57+ requestId ?: string
5858}
5959
60+ type ErrorResponseKind = 'json' | 'download'
61+
6062export class SkillHubClient {
6163 constructor (
6264 readonly registry : string ,
@@ -93,17 +95,8 @@ export class SkillHubClient {
9395 } catch {
9496 throw new CliError ( 'registry unreachable' , EXIT . network , { registry : this . registry , next : 'check network or pass --registry' } )
9597 }
96- if ( response . status === 401 ) {
97- throw new CliError ( 'authentication failed' , EXIT . auth , { registry : this . registry , next : 'run `skillhub login`' } )
98- }
99- if ( response . status === 403 ) {
100- throw await this . createAccessDeniedError ( response )
101- }
102- if ( response . status === 404 ) {
103- throw new CliError ( 'skill or version not found' , EXIT . generic , { registry : this . registry } )
104- }
10598 if ( ! response . ok ) {
106- throw new CliError ( `download failed with status ${ response . status } ` , EXIT . generic , { registry : this . registry } )
99+ throw await this . createResponseError ( response , 'download' )
107100 }
108101 return response
109102 }
@@ -159,45 +152,65 @@ export class SkillHubClient {
159152 }
160153
161154 private async handleJsonResponse < T > ( response : Response ) : Promise < T > {
162- if ( response . status === 401 ) {
163- throw new CliError ( 'authentication failed' , EXIT . auth , { registry : this . registry , next : 'run `skillhub login`' } )
164- }
165- if ( response . status === 403 ) {
166- throw await this . createAccessDeniedError ( response )
167- }
168- if ( response . status === 404 ) {
169- throw new CliError ( 'resource not found' , EXIT . generic , { registry : this . registry } )
170- }
171- // 502/503 indicate network-level failures (connection refused, service unavailable)
172- if ( response . status === 502 || response . status === 503 ) {
173- throw new CliError ( `registry returned ${ response . status } ` , EXIT . network , { registry : this . registry } )
174- }
175155 if ( ! response . ok ) {
176- const text = await response . text ( ) . catch ( ( ) => '' )
177- throw new CliError ( `registry returned ${ response . status } ` , EXIT . generic , { registry : this . registry , detail : text } )
156+ throw await this . createResponseError ( response , 'json' )
178157 }
179158 const body = await response . json ( )
180159 return body . data as T
181160 }
182161
183- private async createAccessDeniedError ( response : Response ) : Promise < CliError > {
184- const error = await this . readErrorEnvelope ( response )
185- return new CliError ( error . message ?? 'access denied' , EXIT . auth , {
186- registry : this . registry ,
187- ...( error . requestId ? { requestId : error . requestId } : { } )
188- } )
162+ private async createResponseError ( response : Response , kind : ErrorResponseKind ) : Promise < CliError > {
163+ const publicFields = await this . readPublicErrorFields ( response )
164+ const details : Record < string , unknown > = { registry : this . registry }
165+ if ( publicFields . requestId ) {
166+ details . requestId = publicFields . requestId
167+ }
168+
169+ let fallback : string
170+ let exitCode : number = EXIT . generic
171+
172+ if ( response . status === 401 ) {
173+ fallback = 'authentication failed'
174+ exitCode = EXIT . auth
175+ details . next = 'run `skillhub login`'
176+ } else if ( response . status === 403 ) {
177+ fallback = 'access denied'
178+ exitCode = EXIT . auth
179+ } else if ( response . status === 404 ) {
180+ fallback = kind === 'download' ? 'skill or version not found' : 'resource not found'
181+ } else if ( response . status === 502 || response . status === 503 ) {
182+ fallback = kind === 'download'
183+ ? `download failed with status ${ response . status } `
184+ : `registry returned ${ response . status } `
185+ exitCode = EXIT . network
186+ } else {
187+ fallback = kind === 'download'
188+ ? `download failed with status ${ response . status } `
189+ : `registry returned ${ response . status } `
190+ }
191+
192+ return new CliError ( publicFields . msg ?? fallback , exitCode , details )
189193 }
190194
191- private async readErrorEnvelope ( response : Response ) : Promise < { message ?: string ; requestId ?: string } > {
195+ private async readPublicErrorFields ( response : Response ) : Promise < PublicErrorFields > {
196+ let body : unknown
192197 try {
193- const body = await response . json ( ) as ErrorEnvelope
194- return {
195- ...( typeof body . msg === 'string' && body . msg . trim ( ) ? { message : body . msg } : { } ) ,
196- ...( typeof body . requestId === 'string' && body . requestId . trim ( ) ? { requestId : body . requestId } : { } )
197- }
198+ body = await response . json ( )
198199 } catch {
199200 return { }
200201 }
202+
203+ if ( typeof body !== 'object' || body === null || Array . isArray ( body ) ) {
204+ return { }
205+ }
206+
207+ const record = body as Record < string , unknown >
208+ const msg = typeof record . msg === 'string' ? record . msg . trim ( ) : ''
209+ const requestId = typeof record . requestId === 'string' ? record . requestId . trim ( ) : ''
210+ return {
211+ ...( msg ? { msg } : { } ) ,
212+ ...( requestId ? { requestId } : { } )
213+ }
201214 }
202215
203216 private headers ( ) : HeadersInit {
0 commit comments