1- import { isVariableSimLinkUrl , parseHttpUrl , SIM_LINK_VARIABLE } from '@/lib/sim-links/validation'
1+ import {
2+ isVariableSimLinkUrl ,
3+ parseHttpUrl ,
4+ SIM_LINK_VARIABLE ,
5+ simLinkOrigin ,
6+ } from '@/lib/sim-links/validation'
27
38interface SimLinkDestination {
49 url : string
@@ -13,34 +18,45 @@ function encodeSegments(values: readonly string[]): string {
1318 return values . map ( encodeURIComponent ) . join ( '/' )
1419}
1520
16- function redirectTo ( url : string ) : SimLinkTarget {
21+ /** Values fill only what follows a closed authority, so leaving `origin` is a broken invariant. */
22+ function redirectWithin ( origin : string , url : string ) : SimLinkTarget {
1723 const parsed = parseHttpUrl ( url )
18- if ( ! parsed ) throw new Error ( 'Sim link destination is not an http(s) URL ' )
24+ if ( parsed ?. origin !== origin ) throw new Error ( 'Sim link resolved outside its fixed origin ' )
1925 return { kind : 'redirect' , url : parsed . toString ( ) }
2026}
2127
2228/**
2329 * Where `sim/<name>/<values…>` leads. A plain link appends the values to its path.
2430 * A variable link fills each `{*}` in order, the last taking every remaining value,
25- * and opens its default destination when given none.
31+ * and opens its default destination when given none. Every redirect stays on the
32+ * stored destination's origin.
2633 */
2734export function resolveSimLinkTarget (
2835 link : SimLinkDestination ,
2936 values : readonly string [ ]
3037) : SimLinkTarget {
38+ const origin = simLinkOrigin ( link . url )
39+ if ( ! origin ) throw new Error ( 'Sim link destination has no fixed http(s) origin' )
3140 if ( ! isVariableSimLinkUrl ( link . url ) ) {
32- if ( ! values . length ) return redirectTo ( link . url )
41+ if ( ! values . length ) return redirectWithin ( origin , link . url )
3342 const url = new URL ( link . url )
3443 url . pathname = `${ url . pathname . replace ( / \/ + $ / , '' ) } /${ encodeSegments ( values ) } `
35- return redirectTo ( url . toString ( ) )
44+ return redirectWithin ( origin , url . toString ( ) )
45+ }
46+ if ( ! values . length && link . defaultUrl ) {
47+ const defaultUrl = parseHttpUrl ( link . defaultUrl )
48+ if ( ! defaultUrl ) throw new Error ( 'Sim link default destination is not an http(s) URL' )
49+ return { kind : 'redirect' , url : defaultUrl . toString ( ) }
3650 }
3751 const parts = link . url . split ( SIM_LINK_VARIABLE )
3852 const slots = parts . length - 1
39- if ( ! values . length && link . defaultUrl ) return redirectTo ( link . defaultUrl )
4053 if ( values . length < slots ) return { kind : 'missing_values' , required : slots }
4154 const fills = [
4255 ...values . slice ( 0 , slots - 1 ) . map ( encodeURIComponent ) ,
4356 encodeSegments ( values . slice ( slots - 1 ) ) ,
4457 ]
45- return redirectTo ( parts . reduce ( ( url , part , index ) => url + fills [ index - 1 ] + part ) )
58+ return redirectWithin (
59+ origin ,
60+ parts . reduce ( ( url , part , index ) => url + fills [ index - 1 ] + part )
61+ )
4662}
0 commit comments