@@ -31,6 +31,7 @@ export interface RequestTrustViolation {
3131 toolId ?: string
3232 reason :
3333 | 'missing-internal-policy'
34+ | 'invalid-internal-policy'
3435 | 'internal-policy-without-internal-route'
3536 | 'mixed-route-requires-conditional-policy'
3637 | 'unsafe-internal-path-interpolation'
@@ -196,7 +197,7 @@ function isExternalUrlExpression(expression: SyntaxNode): boolean {
196197 return false
197198}
198199
199- function isInternalUrlConstruction ( node : SyntaxNode ) : boolean {
200+ function getUrlConstructionPrefix ( node : SyntaxNode ) : string | undefined {
200201 const current = unwrapExpression ( node )
201202 if (
202203 current . type !== 'NewExpression' ||
@@ -207,9 +208,18 @@ function isInternalUrlConstruction(node: SyntaxNode): boolean {
207208 current . arguments . length === 0 ||
208209 ! isSyntaxNode ( current . arguments [ 0 ] )
209210 ) {
210- return false
211+ return undefined
211212 }
212- return getStringPrefix ( current . arguments [ 0 ] ) ?. startsWith ( '/api/' ) === true
213+ return getStringPrefix ( current . arguments [ 0 ] )
214+ }
215+
216+ function isInternalUrlConstruction ( node : SyntaxNode ) : boolean {
217+ return getUrlConstructionPrefix ( node ) ?. startsWith ( '/api/' ) === true
218+ }
219+
220+ function isExternalUrlConstruction ( node : SyntaxNode ) : boolean {
221+ const prefix = getUrlConstructionPrefix ( node )
222+ return prefix !== undefined && / ^ h t t p s ? : \/ \/ / . test ( prefix )
213223}
214224
215225function functionContainsInternalRoute ( fn : SyntaxNode ) : boolean {
@@ -268,6 +278,10 @@ function functionContainsExternalRoute(fn: SyntaxNode): boolean {
268278 found = true
269279 return
270280 }
281+ if ( isExternalUrlConstruction ( node ) ) {
282+ found = true
283+ return
284+ }
271285 for ( const child of getChildNodes ( node ) ) visit ( child )
272286 }
273287 visit ( current )
@@ -498,35 +512,47 @@ export function auditToolRequestTrust(source: string, file = 'source.ts'): Reque
498512 const hasExternalRoute = functionContainsExternalRoute ( url )
499513 const hasInternalPolicy = internalProperty !== undefined
500514 const internalPolicyValue = internalProperty ?. value
501- const hasConditionalInternalPolicy =
515+ const internalPolicyType = isSyntaxNode ( internalPolicyValue )
516+ ? unwrapExpression ( internalPolicyValue ) . type
517+ : undefined
518+ const hasStaticInternalPolicy =
519+ internalPolicyType === 'BooleanLiteral' &&
502520 isSyntaxNode ( internalPolicyValue ) &&
503- ! (
504- unwrapExpression ( internalPolicyValue ) . type === 'BooleanLiteral' &&
505- unwrapExpression ( internalPolicyValue ) . value === true
506- )
521+ unwrapExpression ( internalPolicyValue ) . value === true
522+ const hasConditionalInternalPolicy =
523+ internalPolicyType === 'ArrowFunctionExpression' ||
524+ internalPolicyType === 'FunctionExpression' ||
525+ internalPolicyType === 'Identifier'
526+ const hasValidInternalPolicy = hasStaticInternalPolicy || hasConditionalInternalPolicy
507527 const hasUnsafeInternalPathInterpolation =
508528 functionContainsUnsafeInternalPathInterpolation ( url )
509529 if ( hasInternalRoute ) dynamicInternalRoutes += 1
510530 if ( hasInternalPolicy ) dynamicInternalPolicies += 1
511- if (
512- ( hasInternalRoute && ! hasInternalPolicy ) ||
513- ( hasInternalPolicy &&
514- hasExternalRoute &&
515- ! hasInternalRoute &&
516- ! hasConditionalInternalPolicy )
517- ) {
531+ if ( hasInternalPolicy && ! hasValidInternalPolicy ) {
532+ violations . push ( {
533+ file,
534+ line : internalProperty ?. loc ?. start . line ?? requestProperty . loc ?. start . line ?? 1 ,
535+ toolId : getToolId ( node ) ,
536+ reason : 'invalid-internal-policy' ,
537+ } )
538+ } else if ( hasInternalRoute && ! hasInternalPolicy ) {
539+ violations . push ( {
540+ file,
541+ line : urlProperty ?. loc ?. start . line ?? requestProperty . loc ?. start . line ?? 1 ,
542+ toolId : getToolId ( node ) ,
543+ reason : 'missing-internal-policy' ,
544+ } )
545+ } else if ( hasStaticInternalPolicy && hasExternalRoute && ! hasInternalRoute ) {
518546 const location = ( hasInternalPolicy ? internalProperty : urlProperty ) ?. loc ?. start . line
519547 violations . push ( {
520548 file,
521549 line : location ?? requestProperty . loc ?. start . line ?? 1 ,
522550 toolId : getToolId ( node ) ,
523- reason : hasInternalRoute
524- ? 'missing-internal-policy'
525- : 'internal-policy-without-internal-route' ,
551+ reason : 'internal-policy-without-internal-route' ,
526552 } )
527553 }
528554 if (
529- hasInternalPolicy &&
555+ hasValidInternalPolicy &&
530556 hasInternalRoute &&
531557 hasExternalRoute &&
532558 ! hasConditionalInternalPolicy
@@ -740,11 +766,13 @@ function main(): void {
740766 const description =
741767 violation . reason === 'missing-internal-policy'
742768 ? 'dynamic /api route is missing request.internal'
743- : violation . reason === 'internal-policy-without-internal-route'
744- ? 'request.internal is declared but the URL builder has no /api route'
745- : violation . reason === 'mixed-route-requires-conditional-policy'
746- ? 'mixed internal/external URL builder requires a predicate request.internal policy'
747- : 'dynamic /api path parameter must use encodeURIComponent'
769+ : violation . reason === 'invalid-internal-policy'
770+ ? 'request.internal must be true or a predicate function'
771+ : violation . reason === 'internal-policy-without-internal-route'
772+ ? 'request.internal is declared but the URL builder has no /api route'
773+ : violation . reason === 'mixed-route-requires-conditional-policy'
774+ ? 'mixed internal/external URL builder requires a predicate request.internal policy'
775+ : 'dynamic /api path parameter must use encodeURIComponent'
748776 console . error (
749777 ` ${ relative ( ROOT , violation . file ) } :${ violation . line } ${ violation . toolId ?? 'unknown tool' } : ${ description } `
750778 )
0 commit comments