@@ -290,28 +290,164 @@ function isHttpMethod(value: string): value is HttpMethod {
290290 return VALID_METHODS . includes ( value as HttpMethod ) ;
291291}
292292
293- function stripApiHost ( endpoint : string ) : string {
294- return endpoint . replace ( / ^ h t t p s : \/ \/ a p i \. g o d a d d y \. c o m / i, "" ) ;
293+ const ABSOLUTE_HTTP_URL_PATTERN = / ^ h t t p s ? : \/ \/ / i;
294+ const TRUSTED_API_HOSTS = new Set ( [ "api.godaddy.com" , "api.ote-godaddy.com" ] ) ;
295+
296+ interface ParsedEndpointInput {
297+ callEndpoint : string ;
298+ catalogPathCandidates : string [ ] ;
299+ absoluteUrl : URL | null ;
300+ isTrustedAbsolute : boolean ;
301+ invalidAbsoluteUrl : boolean ;
295302}
296303
297- function normalizeRelativeEndpoint ( endpoint : string ) : string {
298- const stripped = stripApiHost ( endpoint . trim ( ) ) ;
299- if ( stripped . length === 0 ) return "/" ;
300- return stripped . startsWith ( "/" ) ? stripped : `/${ stripped } ` ;
304+ function parseAbsoluteHttpUrl ( value : string ) : URL | null {
305+ try {
306+ const parsed = new URL ( value . trim ( ) ) ;
307+ if ( parsed . protocol !== "http:" && parsed . protocol !== "https:" ) {
308+ return null ;
309+ }
310+ return parsed ;
311+ } catch {
312+ return null ;
313+ }
314+ }
315+
316+ function normalizeCatalogPath ( pathValue : string ) : string {
317+ const pathOnly = pathValue . split ( / [ ? # ] / , 1 ) [ 0 ] || "/" ;
318+ const withLeadingSlash = pathOnly . startsWith ( "/" ) ? pathOnly : `/${ pathOnly } ` ;
319+
320+ if ( withLeadingSlash . length > 1 && withLeadingSlash . endsWith ( "/" ) ) {
321+ return withLeadingSlash . slice ( 0 , - 1 ) ;
322+ }
323+
324+ return withLeadingSlash ;
301325}
302326
303- function catalogPathCandidates ( endpoint : string ) : string [ ] {
304- const relative = normalizeRelativeEndpoint ( endpoint ) ;
305- const candidates = [ relative ] ;
327+ function buildCatalogPathCandidates ( pathValue : string ) : string [ ] {
328+ const normalizedPath = normalizeCatalogPath ( pathValue ) ;
329+ const candidates = [ normalizedPath ] ;
306330
307- const commercePrefixMatch = relative . match ( / ^ \/ v \d + \/ c o m m e r c e ( \/ .* ) $ / i) ;
331+ const commercePrefixMatch = normalizedPath . match ( / ^ \/ v \d + \/ c o m m e r c e ( \/ .* ) $ / i) ;
308332 if ( commercePrefixMatch ?. [ 1 ] ) {
309333 candidates . push ( commercePrefixMatch [ 1 ] ) ;
310334 }
311335
312336 return [ ...new Set ( candidates ) ] ;
313337}
314338
339+ function parseEndpointInput ( endpoint : string ) : ParsedEndpointInput {
340+ const trimmed = endpoint . trim ( ) ;
341+
342+ if ( trimmed . length === 0 ) {
343+ return {
344+ callEndpoint : "/" ,
345+ catalogPathCandidates : [ "/" ] ,
346+ absoluteUrl : null ,
347+ isTrustedAbsolute : false ,
348+ invalidAbsoluteUrl : false ,
349+ } ;
350+ }
351+
352+ if ( ABSOLUTE_HTTP_URL_PATTERN . test ( trimmed ) ) {
353+ const absoluteUrl = parseAbsoluteHttpUrl ( trimmed ) ;
354+ if ( ! absoluteUrl ) {
355+ return {
356+ callEndpoint : trimmed ,
357+ catalogPathCandidates : buildCatalogPathCandidates ( trimmed ) ,
358+ absoluteUrl : null ,
359+ isTrustedAbsolute : false ,
360+ invalidAbsoluteUrl : true ,
361+ } ;
362+ }
363+
364+ const isTrustedAbsolute =
365+ absoluteUrl . protocol === "https:" &&
366+ TRUSTED_API_HOSTS . has ( absoluteUrl . hostname . toLowerCase ( ) ) ;
367+
368+ const relativePath = `${ absoluteUrl . pathname || "/" } ${ absoluteUrl . search } ${ absoluteUrl . hash } ` ;
369+
370+ return {
371+ callEndpoint : isTrustedAbsolute ? relativePath : trimmed ,
372+ catalogPathCandidates : buildCatalogPathCandidates (
373+ absoluteUrl . pathname || "/" ,
374+ ) ,
375+ absoluteUrl,
376+ isTrustedAbsolute,
377+ invalidAbsoluteUrl : false ,
378+ } ;
379+ }
380+
381+ const callEndpoint = trimmed . startsWith ( "/" ) ? trimmed : `/${ trimmed } ` ;
382+ return {
383+ callEndpoint,
384+ catalogPathCandidates : buildCatalogPathCandidates ( callEndpoint ) ,
385+ absoluteUrl : null ,
386+ isTrustedAbsolute : false ,
387+ invalidAbsoluteUrl : false ,
388+ } ;
389+ }
390+
391+ function resolveCatalogEndpointEffect (
392+ method : HttpMethod ,
393+ methodProvided : boolean ,
394+ pathCandidates : string [ ] ,
395+ fallbackEndpoint : string ,
396+ ) : Effect . Effect < {
397+ method : HttpMethod ;
398+ endpoint : string ;
399+ catalogMatch ?: { domain : CatalogDomain ; endpoint : CatalogEndpoint } ;
400+ } > {
401+ return Effect . gen ( function * ( ) {
402+ let catalogMatch :
403+ | { domain : CatalogDomain ; endpoint : CatalogEndpoint }
404+ | undefined ;
405+ let matchedPath : string | undefined ;
406+
407+ if ( methodProvided ) {
408+ for ( const candidatePath of pathCandidates ) {
409+ const byPath = yield * findEndpointByPathEffect ( method , candidatePath ) ;
410+ if ( Option . isSome ( byPath ) ) {
411+ catalogMatch = byPath . value ;
412+ matchedPath = candidatePath ;
413+ break ;
414+ }
415+ }
416+ } else {
417+ for ( const candidatePath of pathCandidates ) {
418+ const byAnyMethod = yield * findEndpointByAnyMethodEffect ( candidatePath ) ;
419+ if ( Option . isSome ( byAnyMethod ) ) {
420+ catalogMatch = byAnyMethod . value ;
421+ matchedPath = candidatePath ;
422+ break ;
423+ }
424+ }
425+ }
426+
427+ let resolvedMethod = method ;
428+ let resolvedEndpoint = fallbackEndpoint ;
429+
430+ if ( catalogMatch ) {
431+ if ( matchedPath === catalogMatch . endpoint . path ) {
432+ resolvedEndpoint = buildCallEndpoint (
433+ catalogMatch . domain ,
434+ catalogMatch . endpoint ,
435+ ) ;
436+ }
437+
438+ if ( ! methodProvided && isHttpMethod ( catalogMatch . endpoint . method ) ) {
439+ resolvedMethod = catalogMatch . endpoint . method ;
440+ }
441+ }
442+
443+ return {
444+ method : resolvedMethod ,
445+ endpoint : resolvedEndpoint ,
446+ catalogMatch,
447+ } ;
448+ } ) ;
449+ }
450+
315451function buildCallEndpoint (
316452 domain : CatalogDomain ,
317453 endpoint : CatalogEndpoint ,
@@ -481,7 +617,8 @@ const apiDescribe = Command.make(
481617 Effect . gen ( function * ( ) {
482618 const writer = yield * EnvelopeWriter ;
483619
484- const pathCandidates = catalogPathCandidates ( endpoint ) ;
620+ const { catalogPathCandidates : pathCandidates } =
621+ parseEndpointInput ( endpoint ) ;
485622
486623 // Try exact path lookup first
487624 let result : Option . Option < {
@@ -700,50 +837,43 @@ const apiCall = Command.make(
700837 }
701838
702839 const methodProvided = Option . isSome ( config . method ) ;
703- let method : HttpMethod = methodInput ;
704- let resolvedEndpoint = config . endpoint ;
705-
706- let catalogMatch :
707- | { domain : CatalogDomain ; endpoint : CatalogEndpoint }
708- | undefined ;
709-
710- const pathCandidates = catalogPathCandidates ( config . endpoint ) ;
711- if ( methodProvided ) {
712- for ( const candidatePath of pathCandidates ) {
713- const byPath = yield * findEndpointByPathEffect ( method , candidatePath ) ;
714- if ( Option . isSome ( byPath ) ) {
715- catalogMatch = byPath . value ;
716- break ;
717- }
718- }
719- } else {
720- for ( const candidatePath of pathCandidates ) {
721- const byAnyMethod =
722- yield * findEndpointByAnyMethodEffect ( candidatePath ) ;
723- if ( Option . isSome ( byAnyMethod ) ) {
724- catalogMatch = byAnyMethod . value ;
725- break ;
726- }
727- }
840+ const parsedEndpoint = parseEndpointInput ( config . endpoint ) ;
841+
842+ if ( parsedEndpoint . invalidAbsoluteUrl ) {
843+ return yield * Effect . fail (
844+ new ValidationError ( {
845+ message : `Invalid endpoint URL: ${ config . endpoint } ` ,
846+ userMessage :
847+ "Endpoint must be a valid URL or a relative path (for example: /v1/domains)." ,
848+ } ) ,
849+ ) ;
728850 }
729851
730- if ( catalogMatch ) {
731- resolvedEndpoint = buildCallEndpoint (
732- catalogMatch . domain ,
733- catalogMatch . endpoint ,
852+ if ( parsedEndpoint . absoluteUrl && ! parsedEndpoint . isTrustedAbsolute ) {
853+ return yield * Effect . fail (
854+ new ValidationError ( {
855+ message : `Untrusted endpoint host: ${ parsedEndpoint . absoluteUrl . hostname } ` ,
856+ userMessage :
857+ "Use a relative endpoint path, or a trusted GoDaddy API URL on api.godaddy.com or api.ote-godaddy.com." ,
858+ } ) ,
734859 ) ;
860+ }
735861
736- if ( ! methodProvided && isHttpMethod ( catalogMatch . endpoint . method ) ) {
737- method = catalogMatch . endpoint . method ;
738- }
862+ const resolved = yield * resolveCatalogEndpointEffect (
863+ methodInput ,
864+ methodProvided ,
865+ parsedEndpoint . catalogPathCandidates ,
866+ parsedEndpoint . callEndpoint ,
867+ ) ;
739868
740- if ( cliConfig . verbosity >= 1 ) {
741- process . stderr . write (
742- `Resolved endpoint to ${ catalogMatch . endpoint . method } ${ resolvedEndpoint } \n` ,
743- ) ;
744- }
745- } else {
746- resolvedEndpoint = normalizeRelativeEndpoint ( config . endpoint ) ;
869+ const method = resolved . method ;
870+ const resolvedEndpoint = resolved . endpoint ;
871+ const catalogMatch = resolved . catalogMatch ;
872+
873+ if ( catalogMatch && cliConfig . verbosity >= 1 ) {
874+ process . stderr . write (
875+ `Resolved endpoint to ${ catalogMatch . endpoint . method } ${ resolvedEndpoint } \n` ,
876+ ) ;
747877 }
748878
749879 const fields = yield * parseFieldsEffect (
0 commit comments