@@ -4,27 +4,50 @@ import { genericToolResponseSchema } from '@/lib/api/contracts/tools/shared'
44import { defineRouteContract } from '@/lib/api/contracts/types'
55
66/**
7- * Kusto cluster host suffixes Sim will talk to. A cluster URI is user-supplied,
8- * so the proxy is pinned to the documented Azure Data Explorer and Fabric
9- * Eventhouse service domains rather than trusting any HTTPS host. Apex hosts
10- * are allowed too, because the documented token audiences
11- * (`https://api.kusto.windows.net`, `https://kusto.fabric.microsoft.com`) sit
12- * at the apex.
7+ * Kusto service domains Sim will talk to, each paired with the Microsoft Entra
8+ * authority that issues tokens for it.
9+ *
10+ * A cluster URI is user-supplied, so the proxy is pinned to the documented Azure
11+ * Data Explorer and Fabric Eventhouse domains rather than trusting any HTTPS
12+ * host. Host and authority are declared together on purpose: a sovereign cloud
13+ * authenticates against its own isolated Entra instance, so accepting a cluster
14+ * host without its authority would pass validation and then fail to get a token.
15+ *
16+ * Apex hosts match as well as subdomains, because the documented token
17+ * audiences (`https://api.kusto.windows.net`,
18+ * `https://kusto.fabric.microsoft.com`) sit at the apex.
1319 */
14- const ALLOWED_CLUSTER_HOST_SUFFIXES = [
15- 'kusto.windows.net' ,
16- 'kustomfa.windows.net' ,
17- 'kusto.chinacloudapi.cn' ,
18- 'kusto.usgovcloudapi.net' ,
19- 'kusto.fabric.microsoft.com' ,
20+ const KUSTO_CLOUDS = [
21+ { hostSuffix : 'kusto.windows.net' , authority : 'https://login.microsoftonline.com' } ,
22+ { hostSuffix : 'kustomfa.windows.net' , authority : 'https://login.microsoftonline.com' } ,
23+ { hostSuffix : 'kusto.fabric.microsoft.com' , authority : 'https://login.microsoftonline.com' } ,
24+ { hostSuffix : 'kusto.usgovcloudapi.net' , authority : 'https://login.microsoftonline.us' } ,
25+ { hostSuffix : 'kusto.chinacloudapi.cn' , authority : 'https://login.partner.microsoftonline.cn' } ,
2026] as const
2127
22- function isAllowedKustoHost ( host : string ) : boolean {
23- return ALLOWED_CLUSTER_HOST_SUFFIXES . some (
24- ( suffix ) => host === suffix || host . endsWith ( `.${ suffix } ` )
28+ const ALLOWED_CLUSTER_HOSTS = KUSTO_CLOUDS . map ( ( cloud ) => cloud . hostSuffix ) . join ( ', ' )
29+
30+ function matchKustoCloud ( host : string ) : ( typeof KUSTO_CLOUDS ) [ number ] | null {
31+ return (
32+ KUSTO_CLOUDS . find (
33+ ( cloud ) => host === cloud . hostSuffix || host . endsWith ( `.${ cloud . hostSuffix } ` )
34+ ) ?? null
2535 )
2636}
2737
38+ /**
39+ * Resolves the Entra authority that issues tokens for a cluster host. Callers
40+ * pass a host already accepted by {@link checkAzureDataExplorerClusterUri}, so
41+ * an unmatched host here means the two fell out of sync and is a bug, not input.
42+ */
43+ export function resolveEntraAuthority ( clusterHost : string ) : string {
44+ const cloud = matchKustoCloud ( clusterHost . toLowerCase ( ) )
45+ if ( ! cloud ) {
46+ throw new Error ( `No Microsoft Entra authority is configured for cluster host ${ clusterHost } ` )
47+ }
48+ return cloud . authority
49+ }
50+
2851export function checkAzureDataExplorerClusterUri (
2952 rawUrl : string ,
3053 label = 'clusterUri'
@@ -45,10 +68,10 @@ export function checkAzureDataExplorerClusterUri(
4568 if ( isPrivateIpHost ( host ) ) {
4669 return { ok : false , message : `${ label } host is not allowed (private/loopback range)` }
4770 }
48- if ( ! isAllowedKustoHost ( host ) ) {
71+ if ( ! matchKustoCloud ( host ) ) {
4972 return {
5073 ok : false ,
51- message : `${ label } host must be an Azure Data Explorer or Fabric Eventhouse endpoint (${ ALLOWED_CLUSTER_HOST_SUFFIXES . join ( ', ' ) } )` ,
74+ message : `${ label } host must be an Azure Data Explorer or Fabric Eventhouse endpoint (${ ALLOWED_CLUSTER_HOSTS } )` ,
5275 }
5376 }
5477 return { ok : true , url : parsed }
0 commit comments