11import express from 'express' ;
2+ import rateLimit from 'express-rate-limit' ;
23import { TokenManager } from './TokenManager' ;
34import { pipeline } from 'stream/promises' ;
45import { Readable } from 'stream' ;
@@ -10,51 +11,79 @@ const config = require('../config.js');
1011const tokenManager = new TokenManager ( ) ;
1112const COMPANION_API_BASE_URL = `${ config . features ?. KYMA_COMPANION ?. config
1213 ?. apiBaseUrl ?? '' } /api/conversations/`;
14+ const SKIP_AUTH = config . features ?. KYMA_COMPANION ?. config ?. skipAuth ?? false ;
1315const router = express . Router ( ) ;
1416
17+ // Rate limiter: Max 200 requests per 1 minutes per IP
18+ const companionRateLimiter = rateLimit ( {
19+ windowMs : 1 * 60 * 1000 ,
20+ max : 200 ,
21+ message : 'Too many requests, please try again later.' ,
22+ standardHeaders : true ,
23+ legacyHeaders : false ,
24+ } ) ;
25+
1526router . use ( express . json ( ) ) ;
1627
28+ function extractAuthHeaders ( req ) {
29+ return {
30+ clusterUrl : req . headers [ 'x-cluster-url' ] ,
31+ certificateAuthorityData :
32+ req . headers [ 'x-cluster-certificate-authority-data' ] ,
33+ clusterToken : req . headers [ 'x-k8s-authorization' ] ?. replace (
34+ / ^ B e a r e r \s + / i,
35+ '' ,
36+ ) ,
37+ clientCertificateData : req . headers [ 'x-client-certificate-data' ] ,
38+ clientKeyData : req . headers [ 'x-client-key-data' ] ,
39+ sessionId : req . headers [ 'session-id' ] ,
40+ } ;
41+ }
42+
43+ async function buildApiHeaders ( authData , contentType = 'application/json' ) {
44+ const headers = {
45+ Accept : contentType ,
46+ 'Content-Type' : 'application/json' ,
47+ 'X-Cluster-Certificate-Authority-Data' : authData . certificateAuthorityData ,
48+ 'X-Cluster-Url' : authData . clusterUrl ,
49+ } ;
50+
51+ if ( ! SKIP_AUTH ) {
52+ const AUTH_TOKEN = await tokenManager . getToken ( ) ;
53+ headers . Authorization = `Bearer ${ AUTH_TOKEN } ` ;
54+ }
55+
56+ if ( authData . sessionId ) {
57+ headers [ 'Session-Id' ] = authData . sessionId ;
58+ }
59+
60+ if ( authData . clusterToken ) {
61+ headers [ 'X-K8s-Authorization' ] = authData . clusterToken ;
62+ } else if ( authData . clientCertificateData && authData . clientKeyData ) {
63+ headers [ 'X-Client-Certificate-Data' ] = authData . clientCertificateData ;
64+ headers [ 'X-Client-Key-Data' ] = authData . clientKeyData ;
65+ } else {
66+ throw new Error ( 'Missing authentication credentials' ) ;
67+ }
68+
69+ return headers ;
70+ }
71+
1772async function handlePromptSuggestions ( req , res ) {
1873 const { namespace, resourceType, groupVersion, resourceName } = JSON . parse (
1974 req . body . toString ( ) ,
2075 ) ;
21- const clusterUrl = req . headers [ 'x-cluster-url' ] ;
22- const certificateAuthorityData =
23- req . headers [ 'x-cluster-certificate-authority-data' ] ;
24- const clusterToken = req . headers [ 'x-k8s-authorization' ] ?. replace (
25- / ^ B e a r e r \s + / i,
26- '' ,
27- ) ;
28- const clientCertificateData = req . headers [ 'x-client-certificate-data' ] ;
29- const clientKeyData = req . headers [ 'x-client-key-data' ] ;
76+ const authData = extractAuthHeaders ( req ) ;
77+ const endpointUrl = COMPANION_API_BASE_URL ;
78+ const payload = {
79+ resource_kind : resourceType ,
80+ resource_api_version : groupVersion ,
81+ resource_name : resourceName ,
82+ namespace : namespace ,
83+ } ;
3084
3185 try {
32- const endpointUrl = COMPANION_API_BASE_URL ;
33- const payload = {
34- resource_kind : resourceType ,
35- resource_api_version : groupVersion ,
36- resource_name : resourceName ,
37- namespace : namespace ,
38- } ;
39-
40- const AUTH_TOKEN = await tokenManager . getToken ( ) ;
41-
42- const headers = {
43- Accept : 'application/json' ,
44- 'Content-Type' : 'application/json' ,
45- Authorization : `Bearer ${ AUTH_TOKEN } ` ,
46- 'X-Cluster-Certificate-Authority-Data' : certificateAuthorityData ,
47- 'X-Cluster-Url' : clusterUrl ,
48- } ;
49-
50- if ( clusterToken ) {
51- headers [ 'X-K8s-Authorization' ] = clusterToken ;
52- } else if ( clientCertificateData && clientKeyData ) {
53- headers [ 'X-Client-Certificate-Data' ] = clientCertificateData ;
54- headers [ 'X-Client-Key-Data' ] = clientKeyData ;
55- } else {
56- throw new Error ( 'Missing authentication credentials' ) ;
57- }
86+ const headers = await buildApiHeaders ( authData ) ;
5887
5988 const response = await fetch ( endpointUrl , {
6089 method : 'POST' ,
@@ -85,61 +114,33 @@ async function handleChatMessage(req, res) {
85114 resourceName,
86115 } = JSON . parse ( req . body . toString ( ) ) ;
87116
88- const clusterUrl = req . headers [ 'x-cluster-url' ] ;
89- const certificateAuthorityData =
90- req . headers [ 'x-cluster-certificate-authority-data' ] ;
91- const clusterToken = req . headers [ 'x-k8s-authorization' ] ?. replace (
92- / ^ B e a r e r \s + / i ,
93- '' ,
117+ const authData = extractAuthHeaders ( req ) ;
118+ const conversationId = authData . sessionId ;
119+
120+ const endpointUrl = new URL (
121+ ` ${ encodeURIComponent ( conversationId ) } /messages` ,
122+ COMPANION_API_BASE_URL ,
94123 ) ;
95- const clientCertificateData = req . headers [ 'x-client-certificate-data' ] ;
96- const clientKeyData = req . headers [ 'x-client-key-data' ] ;
97- const sessionId = req . headers [ 'session-id' ] ;
98- const conversationId = sessionId ;
124+
125+ const payload = {
126+ query,
127+ resource_kind : resourceType ,
128+ resource_api_version : groupVersion ,
129+ resource_name : resourceName ,
130+ namespace : namespace ,
131+ } ;
99132
100133 try {
101134 const uuidPattern = / ^ [ a - f 0 - 9 ] { 32 } $ / i;
102135 if ( ! uuidPattern . test ( conversationId ) ) {
103136 throw new Error ( 'Invalid session ID ' ) ;
104137 }
105-
106- const endpointUrl = new URL (
107- `${ encodeURIComponent ( conversationId ) } /messages` ,
108- COMPANION_API_BASE_URL ,
109- ) ;
110-
111- const payload = {
112- query,
113- resource_kind : resourceType ,
114- resource_api_version : groupVersion ,
115- resource_name : resourceName ,
116- namespace : namespace ,
117- } ;
118-
119- const AUTH_TOKEN = await tokenManager . getToken ( ) ;
120-
121138 // Set up headers for streaming response
122139 res . setHeader ( 'Content-Type' , 'text/event-stream' ) ;
123140 res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
124141 res . setHeader ( 'Connection' , 'keep-alive' ) ;
125142
126- const headers = {
127- Accept : 'text/event-stream' ,
128- 'Content-Type' : 'application/json' ,
129- Authorization : `Bearer ${ AUTH_TOKEN } ` ,
130- 'X-Cluster-Certificate-Authority-Data' : certificateAuthorityData ,
131- 'X-Cluster-Url' : clusterUrl ,
132- 'Session-Id' : sessionId ,
133- } ;
134-
135- if ( clusterToken ) {
136- headers [ 'X-K8s-Authorization' ] = clusterToken ;
137- } else if ( clientCertificateData && clientKeyData ) {
138- headers [ 'X-Client-Certificate-Data' ] = clientCertificateData ;
139- headers [ 'X-Client-Key-Data' ] = clientKeyData ;
140- } else {
141- throw new Error ( 'Missing authentication credentials' ) ;
142- }
143+ const headers = await buildApiHeaders ( authData , 'text/event-stream' ) ;
143144
144145 const response = await fetch ( endpointUrl , {
145146 method : 'POST' ,
@@ -190,43 +191,16 @@ async function handleChatMessage(req, res) {
190191}
191192
192193async function handleFollowUpSuggestions ( req , res ) {
193- const clusterUrl = req . headers [ 'x-cluster-url' ] ;
194- const certificateAuthorityData =
195- req . headers [ 'x-cluster-certificate-authority-data' ] ;
196- const clusterToken = req . headers [ 'x-k8s-authorization' ] ?. replace (
197- / ^ B e a r e r \s + / i ,
198- '' ,
194+ const authData = extractAuthHeaders ( req ) ;
195+ const conversationId = authData . sessionId ;
196+
197+ const endpointUrl = new URL (
198+ ` ${ encodeURIComponent ( conversationId ) } /questions` ,
199+ COMPANION_API_BASE_URL ,
199200 ) ;
200- const clientCertificateData = req . headers [ 'x-client-certificate-data' ] ;
201- const clientKeyData = req . headers [ 'x-client-key-data' ] ;
202- const sessionId = req . headers [ 'session-id' ] ;
203- const conversationId = sessionId ;
204201
205202 try {
206- const endpointUrl = new URL (
207- `${ encodeURIComponent ( conversationId ) } /questions` ,
208- COMPANION_API_BASE_URL ,
209- ) ;
210-
211- const AUTH_TOKEN = await tokenManager . getToken ( ) ;
212-
213- const headers = {
214- Accept : 'application/json' ,
215- 'Content-Type' : 'application/json' ,
216- Authorization : `Bearer ${ AUTH_TOKEN } ` ,
217- 'X-Cluster-Certificate-Authority-Data' : certificateAuthorityData ,
218- 'X-Cluster-Url' : clusterUrl ,
219- 'Session-Id' : sessionId ,
220- } ;
221-
222- if ( clusterToken ) {
223- headers [ 'X-K8s-Authorization' ] = clusterToken ;
224- } else if ( clientCertificateData && clientKeyData ) {
225- headers [ 'X-Client-Certificate-Data' ] = clientCertificateData ;
226- headers [ 'X-Client-Key-Data' ] = clientKeyData ;
227- } else {
228- throw new Error ( 'Missing authentication credentials' ) ;
229- }
203+ const headers = await buildApiHeaders ( authData ) ;
230204
231205 const response = await fetch ( endpointUrl , {
232206 method : 'GET' ,
@@ -243,8 +217,16 @@ async function handleFollowUpSuggestions(req, res) {
243217 }
244218}
245219
246- router . post ( '/suggestions' , addLogger ( handlePromptSuggestions ) ) ;
247- router . post ( '/messages' , addLogger ( handleChatMessage ) ) ;
248- router . post ( '/followup' , addLogger ( handleFollowUpSuggestions ) ) ;
220+ router . post (
221+ '/suggestions' ,
222+ companionRateLimiter ,
223+ addLogger ( handlePromptSuggestions ) ,
224+ ) ;
225+ router . post ( '/messages' , companionRateLimiter , addLogger ( handleChatMessage ) ) ;
226+ router . post (
227+ '/followup' ,
228+ companionRateLimiter ,
229+ addLogger ( handleFollowUpSuggestions ) ,
230+ ) ;
249231
250232export default router ;
0 commit comments