@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +607 -0
  2. package/README.md +400 -0
  3. package/app-config.yaml +25 -0
  4. package/config.d.ts +142 -0
  5. package/dist/database/migration.cjs.js +19 -0
  6. package/dist/database/migration.cjs.js.map +1 -0
  7. package/dist/index.cjs.js +12 -0
  8. package/dist/index.cjs.js.map +1 -0
  9. package/dist/index.d.ts +73 -0
  10. package/dist/plugin.cjs.js +95 -0
  11. package/dist/plugin.cjs.js.map +1 -0
  12. package/dist/service/constant.cjs.js +107 -0
  13. package/dist/service/constant.cjs.js.map +1 -0
  14. package/dist/service/mcp-server-store.cjs.js +107 -0
  15. package/dist/service/mcp-server-store.cjs.js.map +1 -0
  16. package/dist/service/mcp-server-validator.cjs.js +215 -0
  17. package/dist/service/mcp-server-validator.cjs.js.map +1 -0
  18. package/dist/service/middleware/createPermissionMiddleware.cjs.js +24 -0
  19. package/dist/service/middleware/createPermissionMiddleware.cjs.js.map +1 -0
  20. package/dist/service/middleware/createRateLimitMiddleware.cjs.js +52 -0
  21. package/dist/service/middleware/createRateLimitMiddleware.cjs.js.map +1 -0
  22. package/dist/service/middleware/getIdentity.cjs.js +39 -0
  23. package/dist/service/middleware/getIdentity.cjs.js.map +1 -0
  24. package/dist/service/notebooks/VectorStoresOperator.cjs.js +347 -0
  25. package/dist/service/notebooks/VectorStoresOperator.cjs.js.map +1 -0
  26. package/dist/service/notebooks/documents/documentHelpers.cjs.js +36 -0
  27. package/dist/service/notebooks/documents/documentHelpers.cjs.js.map +1 -0
  28. package/dist/service/notebooks/documents/documentService.cjs.js +189 -0
  29. package/dist/service/notebooks/documents/documentService.cjs.js.map +1 -0
  30. package/dist/service/notebooks/documents/fileParser.cjs.js +139 -0
  31. package/dist/service/notebooks/documents/fileParser.cjs.js.map +1 -0
  32. package/dist/service/notebooks/notebooksRouters.cjs.js +477 -0
  33. package/dist/service/notebooks/notebooksRouters.cjs.js.map +1 -0
  34. package/dist/service/notebooks/sessions/sessionService.cjs.js +226 -0
  35. package/dist/service/notebooks/sessions/sessionService.cjs.js.map +1 -0
  36. package/dist/service/notebooks/types/notebooksTypes.cjs.js +40 -0
  37. package/dist/service/notebooks/types/notebooksTypes.cjs.js.map +1 -0
  38. package/dist/service/notebooks/utils.cjs.js +70 -0
  39. package/dist/service/notebooks/utils.cjs.js.map +1 -0
  40. package/dist/service/permission.cjs.js +26 -0
  41. package/dist/service/permission.cjs.js.map +1 -0
  42. package/dist/service/router.cjs.js +638 -0
  43. package/dist/service/router.cjs.js.map +1 -0
  44. package/dist/service/token-encryption.cjs.js +101 -0
  45. package/dist/service/token-encryption.cjs.js.map +1 -0
  46. package/dist/service/utils.cjs.js +40 -0
  47. package/dist/service/utils.cjs.js.map +1 -0
  48. package/dist/service/validation.cjs.js +45 -0
  49. package/dist/service/validation.cjs.js.map +1 -0
  50. package/migrations/20260302120000_add_mcp_servers.js +41 -0
  51. package/package.json +109 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';\nimport type {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { NotAllowedError } from '@backstage/errors';\nimport type { BasicPermission } from '@backstage/plugin-permission-common';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\n\nimport express, { Router } from 'express';\nimport { createProxyMiddleware } from 'http-proxy-middleware';\n\nimport {\n lightspeedChatCreatePermission,\n lightspeedChatDeletePermission,\n lightspeedChatReadPermission,\n lightspeedMcpManagePermission,\n lightspeedMcpReadPermission,\n lightspeedPermissions,\n} from '@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common';\n\nimport { Readable } from 'node:stream';\n\nimport {\n DEFAULT_LIGHTSPEED_SERVICE_HOST,\n DEFAULT_LIGHTSPEED_SERVICE_PORT,\n EXPRESS_JSON_BODY_LIMIT,\n} from './constant';\nimport { McpUserSettingsStore } from './mcp-server-store';\nimport {\n McpServerAuth,\n McpServerResponse,\n McpServerStatus,\n McpValidationResult,\n} from './mcp-server-types';\nimport { McpServerValidator } from './mcp-server-validator';\nimport { createPermissionMiddleware } from './middleware/createPermissionMiddleware';\nimport { createRateLimitMiddleware } from './middleware/createRateLimitMiddleware';\nimport {\n createIdentityMiddleware,\n getIdentity,\n} from './middleware/getIdentity';\nimport { VectorStoresOperator } from './notebooks/VectorStoresOperator';\nimport { userPermissionAuthorization } from './permission';\nimport { createTokenEncryptor } from './token-encryption';\nimport { QueryRequestBody, RouterOptions } from './types';\nimport { handleLCSFetchError, rewriteProxyPath } from './utils';\nimport { validateCompletionsRequest } from './validation';\n\n/**\n * MCP Server authentication modes for lightspeed.mcpServers entries.\n *\n * - `'dcr'` — Dynamic Client Registration. The Lightspeed backend mints a\n * short-lived Backstage plugin-request token carrying the current user's\n * identity. This token is forwarded to the MCP server via LCORE's\n * MCP-HEADERS, and the MCP Actions backend validates it against the\n * Backstage Permission Framework (RBAC). Use this for Backstage-internal\n * MCP servers (e.g. `@backstage/plugin-mcp-actions-backend`).\n * A static `token` field is **ignored** when `auth: dcr` is set.\n *\n * - (absent / undefined) — Legacy static-token mode. The token is resolved\n * from the user's DB override first, then from the `token` field in\n * app-config. If neither exists, the server is omitted from MCP-HEADERS.\n * Users can also set their own tokens via the Lightspeed UI.\n */\ninterface StaticMcpServer {\n name: string;\n token?: string;\n auth?: McpServerAuth;\n}\n\n/**\n * Build MCP-HEADERS for LCS. Format matches the LCS \"client\"/\"oauth\" auth\n * model: `{ \"server-name\": { \"Authorization\": \"<token>\" } }`\n *\n * For each admin-configured server:\n *\n * 1. If `auth: 'dcr'`, a Backstage plugin-request token is minted on behalf\n * of the authenticated user. Any static token is ignored.\n *\n * 2. Otherwise, the user's personal token (DB) takes precedence over the\n * admin default from app-config. If neither exists, the server is\n * excluded from MCP-HEADERS (LCORE will skip it or return 401).\n *\n * Servers the user has disabled are always excluded.\n */\nasync function buildMcpHeaders(\n servers: StaticMcpServer[],\n store: McpUserSettingsStore,\n userEntityRef: string,\n options?: {\n dcrAuth?: { authService: AuthService; credentials: BackstageCredentials };\n logger?: LoggerService;\n },\n): Promise<string> {\n const headers: Record<string, { Authorization: string }> = {};\n const userSettings = await store.listByUser(userEntityRef);\n const settingsMap = new Map(userSettings.map(s => [s.server_name, s]));\n\n for (const server of servers) {\n const setting = settingsMap.get(server.name);\n const enabled = setting ? Boolean(setting.enabled) : true;\n if (!enabled) continue;\n\n if (server.auth === 'dcr') {\n if (options?.dcrAuth) {\n try {\n const { token } =\n await options.dcrAuth.authService.getPluginRequestToken({\n onBehalfOf: options.dcrAuth.credentials,\n targetPluginId: 'mcp-actions',\n });\n headers[server.name] = { Authorization: `${token}` };\n } catch (err) {\n options?.logger?.error(\n `Failed to mint DCR token for MCP server '${server.name}': ${err}`,\n );\n }\n } else {\n options?.logger?.warn(\n `MCP server '${server.name}' has auth: dcr but AuthService is not available; skipping`,\n );\n }\n } else {\n // Static token: user's personal token (DB) > admin default (app-config).\n // If neither exists, the server is excluded from MCP-HEADERS.\n const token = setting?.token || server.token;\n if (token) {\n headers[server.name] = { Authorization: `${token}` };\n }\n }\n }\n\n return Object.keys(headers).length > 0 ? JSON.stringify(headers) : '';\n}\n\n/**\n * @public\n * The lightspeed backend router\n */\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, config, database, httpAuth, auth, userInfo, permissions } =\n options;\n\n const router = Router();\n router.use(express.json());\n\n const port =\n config.getOptionalNumber('intelligent-assistant.servicePort') ??\n DEFAULT_LIGHTSPEED_SERVICE_PORT;\n const system_prompt = config.getOptionalString(\n 'intelligent-assistant.systemPrompt',\n );\n const lcsBaseUrl = `http://${DEFAULT_LIGHTSPEED_SERVICE_HOST}:${port}`;\n\n const apiProxy = createProxyMiddleware({\n target: lcsBaseUrl,\n changeOrigin: true,\n pathRewrite: (path, req) => {\n const userEntityRef = (req as any).userEntityRef;\n const newPath = rewriteProxyPath(path, userEntityRef);\n\n if (newPath !== path) {\n logger.info(`Rewriting path from ${path} to ${newPath}`);\n }\n return newPath;\n },\n });\n\n const vectorStoresOperator = VectorStoresOperator.getInstance(\n lcsBaseUrl,\n logger,\n );\n // Parse admin-configured MCP servers from app-config.\n //\n // Each entry supports:\n // name (required) — unique identifier, must match the name in lightspeed-stack.yaml\n // auth (optional) — authentication mode:\n // 'dcr' = the backend mints a per-user Backstage token (for\n // Backstage-internal MCP servers with DCR/OAuth enabled)\n // absent = legacy static-token mode (see `token` below)\n // token (optional) — static fallback token from app-config. Ignored when auth: dcr.\n // Users can also set personal tokens via the Lightspeed UI.\n //\n // URLs come from LCS (GET /v1/mcp-servers), not from app-config.\n const mcpServersConfig = config.getOptionalConfigArray(\n 'intelligent-assistant.mcpServers',\n );\n const staticServers: StaticMcpServer[] = [];\n if (mcpServersConfig) {\n for (const mcpServer of mcpServersConfig) {\n const authValue = mcpServer.getOptionalString('auth')?.toLowerCase();\n if (authValue && authValue !== 'dcr') {\n logger.warn(\n `MCP server '${mcpServer.getString('name')}' has unsupported auth value '${authValue}'; ` +\n `only 'dcr' is supported — falling back to static-token mode`,\n );\n }\n staticServers.push({\n name: mcpServer.getString('name'),\n token: mcpServer.getOptionalString('token'),\n auth: authValue === 'dcr' ? 'dcr' : undefined,\n });\n }\n }\n\n // Initialize database-backed store for per-user preferences and validator\n const dbClient = await database.getClient();\n const encryptor = createTokenEncryptor(config, logger);\n const settingsStore = new McpUserSettingsStore(dbClient, encryptor, logger);\n const mcpValidator = new McpServerValidator(logger);\n\n // URL cache populated from LCS GET /v1/mcp-servers.\n // The canonical URL for each MCP server lives in LCS config, not app-config.\n const lcsUrlCache = new Map<string, string>();\n\n async function refreshLcsUrlCache(): Promise<void> {\n try {\n const response = await fetch(`${lcsBaseUrl}/v1/mcp-servers`, {\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.warn(\n `Failed to fetch MCP server URLs from LCS: HTTP ${response.status}`,\n );\n return;\n }\n const data = (await response.json()) as {\n servers: Array<{ name: string; url: string }>;\n };\n for (const s of data.servers) {\n lcsUrlCache.set(s.name, s.url);\n }\n logger.info(`Cached ${lcsUrlCache.size} MCP server URL(s) from LCS`);\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n logger.warn(`Failed to fetch MCP server URLs from LCS: ${msg}`);\n }\n }\n\n function resolveServerUrl(serverName: string): string | undefined {\n return lcsUrlCache.get(serverName);\n }\n\n // Best-effort URL cache on startup (non-blocking)\n refreshLcsUrlCache().catch(() => {});\n\n router.get('/health', (_, response) => {\n response.json({ status: 'ok' });\n });\n\n const permissionIntegrationRouter = createPermissionIntegrationRouter({\n permissions: lightspeedPermissions,\n });\n router.use(permissionIntegrationRouter);\n\n const identityMiddleware = createIdentityMiddleware(\n httpAuth,\n userInfo,\n logger,\n );\n router.use((req, res, next) => {\n // Middleware mounts to notebooks router independently\n // so we skip it here\n if (req.path.startsWith('/notebooks')) {\n return next();\n }\n return identityMiddleware(req, res, next);\n });\n\n const authorizer = userPermissionAuthorization(permissions);\n\n function requirePermission(permission: BasicPermission) {\n return createPermissionMiddleware(authorizer, permission, logger);\n }\n\n const expensiveRateLimiter = createRateLimitMiddleware(\n config,\n 'expensive',\n logger,\n );\n const generalRateLimiter = createRateLimitMiddleware(\n config,\n 'general',\n logger,\n );\n\n // ─── MCP Server Management Endpoints ────────────────────────────────\n // All MCP servers are admin-configured (static). Users can view the\n // list, toggle servers on/off, and provide personal access tokens.\n\n router.get(\n '/mcp-servers',\n generalRateLimiter,\n requirePermission(lightspeedMcpReadPermission),\n async (req, res) => {\n try {\n const { userEntityRef } = getIdentity(req);\n\n const userSettings = await settingsStore.listByUser(userEntityRef);\n const settingsMap = new Map(userSettings.map(s => [s.server_name, s]));\n\n const hasAllUrls = staticServers.every(s => lcsUrlCache.has(s.name));\n if (!hasAllUrls) {\n await refreshLcsUrlCache();\n }\n\n const servers: McpServerResponse[] = staticServers.map(server => {\n const setting = settingsMap.get(server.name);\n return {\n name: server.name,\n url: resolveServerUrl(server.name),\n enabled: setting ? Boolean(setting.enabled) : true,\n status: setting?.status ?? 'unknown',\n toolCount: setting?.tool_count ?? 0,\n // DCR servers always have a token (minted per-request); static servers check DB + config.\n hasToken:\n server.auth === 'dcr' || !!(setting?.token || server.token),\n hasUserToken: !!setting?.token,\n auth: server.auth,\n };\n });\n\n res.json({ servers });\n } catch (error) {\n logger.error(`Error listing MCP servers: ${error}`);\n res.status(500).json({ error: 'Failed to list MCP servers' });\n }\n },\n );\n\n router.post(\n '/mcp-servers/validate',\n generalRateLimiter,\n requirePermission(lightspeedMcpReadPermission),\n async (req, res) => {\n try {\n const { url, token } = req.body;\n if (!url || !token) {\n res.status(400).json({ error: 'url and token are required' });\n return;\n }\n\n // SSRF protection: only allow URLs registered in LCS\n let knownUrls = new Set(lcsUrlCache.values());\n if (!knownUrls.has(url)) {\n await refreshLcsUrlCache();\n knownUrls = new Set(lcsUrlCache.values());\n }\n if (!knownUrls.has(url)) {\n res.status(400).json({\n error:\n 'URL not recognized — only MCP server URLs registered in LCS are allowed',\n });\n return;\n }\n\n const result = await mcpValidator.validate(url, token);\n res.json(result);\n } catch (error) {\n logger.error(`Error validating MCP credentials: ${error}`);\n res.status(500).json({ error: 'Validation failed' });\n }\n },\n );\n\n router.post(\n '/mcp-servers/:name/validate',\n generalRateLimiter,\n requirePermission(lightspeedMcpManagePermission),\n async (req, res) => {\n try {\n const { userEntityRef, credentials } = getIdentity(req);\n\n const { name } = req.params;\n const server = staticServers.find(s => s.name === name);\n if (!server) {\n res.status(404).json({\n error: `MCP server '${name}' is not configured — it must be defined in the Lightspeed Stack config and listed under intelligent-assistant.mcpServers in app-config`,\n });\n return;\n }\n // Resolve URL: LCS cache → fresh LCS fetch\n let serverUrl = resolveServerUrl(server.name);\n if (!serverUrl) {\n await refreshLcsUrlCache();\n serverUrl = resolveServerUrl(server.name);\n }\n if (!serverUrl) {\n res\n .status(400)\n .json({ error: 'Server has no URL — not found in LCS or config' });\n return;\n }\n\n let effectiveToken: string | undefined;\n if (server.auth === 'dcr') {\n try {\n const { token: dcrToken } = await auth.getPluginRequestToken({\n onBehalfOf: credentials,\n targetPluginId: 'mcp-actions',\n });\n effectiveToken = dcrToken;\n } catch (err) {\n logger.error(\n `Failed to mint DCR token for server '${name}': ${err}`,\n );\n res.status(502).json({\n error: `Failed to mint authentication token for DCR server '${name}' — check Backstage auth configuration`,\n });\n return;\n }\n } else {\n const setting = await settingsStore.get(name, userEntityRef);\n effectiveToken = setting?.token || server.token;\n }\n if (!effectiveToken) {\n res\n .status(400)\n .json({ error: 'No token available — provide one first' });\n return;\n }\n\n const validation = await mcpValidator.validate(\n serverUrl,\n effectiveToken,\n );\n const status: McpServerStatus = validation.valid\n ? 'connected'\n : 'error';\n\n await settingsStore.updateStatus(\n name,\n userEntityRef,\n status,\n validation.toolCount,\n );\n\n res.json({\n name,\n status,\n toolCount: validation.toolCount,\n validation,\n });\n } catch (error) {\n logger.error(`Error validating MCP server: ${error}`);\n res.status(500).json({ error: 'Validation failed' });\n }\n },\n );\n\n router.patch(\n '/mcp-servers/:name',\n generalRateLimiter,\n requirePermission(lightspeedMcpManagePermission),\n async (req, res) => {\n try {\n const { userEntityRef } = getIdentity(req);\n\n const { name } = req.params;\n const server = staticServers.find(s => s.name === name);\n if (!server) {\n res.status(404).json({\n error: `MCP server '${name}' is not configured — it must be defined in the Lightspeed Stack config and listed under intelligent-assistant.mcpServers in app-config`,\n });\n return;\n }\n\n const body = (req.body ?? {}) as {\n enabled?: boolean;\n token?: string | null;\n };\n const hasEnabledField = Object.prototype.hasOwnProperty.call(\n body,\n 'enabled',\n );\n const hasTokenField = Object.prototype.hasOwnProperty.call(\n body,\n 'token',\n );\n const { enabled, token } = body;\n if (!hasEnabledField && !hasTokenField) {\n res.status(400).json({\n error: 'At least one of enabled or token must be provided',\n });\n return;\n }\n\n const setting = await settingsStore.upsert(name, userEntityRef, {\n enabled: hasEnabledField ? enabled : undefined,\n token: hasTokenField ? token : undefined,\n });\n\n let validation: McpValidationResult | undefined;\n let serverUrl = resolveServerUrl(server.name);\n if (token && !serverUrl) {\n await refreshLcsUrlCache();\n serverUrl = resolveServerUrl(server.name);\n }\n if (token && serverUrl) {\n validation = await mcpValidator.validate(serverUrl, token);\n const newStatus: McpServerStatus = validation.valid\n ? 'connected'\n : 'error';\n await settingsStore.updateStatus(\n name,\n userEntityRef,\n newStatus,\n validation.toolCount,\n );\n setting.status = newStatus;\n setting.tool_count = validation.toolCount;\n }\n\n const result: Record<string, unknown> = {\n server: {\n name: server.name,\n url: resolveServerUrl(server.name),\n enabled: Boolean(setting.enabled),\n status: setting.status,\n toolCount: setting.tool_count,\n hasToken:\n server.auth === 'dcr' || !!(setting.token || server.token),\n hasUserToken: !!setting.token,\n auth: server.auth,\n },\n };\n if (validation) result.validation = validation;\n res.json(result);\n } catch (error) {\n logger.error(`Error updating MCP server settings: ${error}`);\n res.status(500).json({ error: 'Failed to update MCP server settings' });\n }\n },\n );\n\n // Returns conversation IDs associated with notebook sessions for filtering\n router.get(\n '/notebook-conversation-ids',\n generalRateLimiter,\n async (req, res) => {\n try {\n const { userEntityRef } = getIdentity(req);\n\n const vectorStoresPage = await vectorStoresOperator.vectorStores.list();\n const vectorStores = vectorStoresPage.data || [];\n\n const conversationIds: string[] = [];\n\n for (const store of vectorStores) {\n const sessionUserId = store.metadata?.user_id as string;\n const conversationId = store.metadata?.conversation_id as\n | string\n | null;\n\n // Only include this user's sessions with a conversation_id\n if (sessionUserId === userEntityRef && conversationId) {\n conversationIds.push(conversationId);\n }\n }\n\n res.json({\n conversation_ids: conversationIds,\n });\n } catch (error) {\n const errormsg = `Error fetching notebook conversation IDs: ${error}`;\n logger.error(errormsg);\n\n if (error instanceof NotAllowedError) {\n res.status(403).json({ error: error.message });\n } else {\n res.status(500).json({ error: errormsg });\n }\n }\n },\n );\n\n // ─── Proxy Routes ───────────────────────────────────────────────────\n\n router.get(\n '/v1/models',\n generalRateLimiter,\n requirePermission(lightspeedChatReadPermission),\n apiProxy,\n );\n router.get(\n '/v1/shields',\n generalRateLimiter,\n requirePermission(lightspeedChatReadPermission),\n apiProxy,\n );\n router.get(\n '/v2/conversations',\n generalRateLimiter,\n requirePermission(lightspeedChatReadPermission),\n apiProxy,\n );\n router.get(\n '/v2/conversations/:conversation_id',\n generalRateLimiter,\n requirePermission(lightspeedChatReadPermission),\n apiProxy,\n );\n router.delete(\n '/v2/conversations/:conversation_id',\n generalRateLimiter,\n requirePermission(lightspeedChatDeletePermission),\n apiProxy,\n );\n router.get(\n '/v1/feedback/status',\n generalRateLimiter,\n requirePermission(lightspeedChatReadPermission),\n apiProxy,\n );\n\n router.post(\n '/v1/feedback',\n generalRateLimiter,\n requirePermission(lightspeedChatCreatePermission),\n async (request, response) => {\n try {\n const { userEntityRef } = getIdentity(request);\n\n logger.info(`/v1/feedback receives call from user: ${userEntityRef}`);\n\n const userQueryParam = `user_id=${encodeURIComponent(userEntityRef)}`;\n const requestBody = JSON.stringify(request.body);\n const fetchResponse = await fetch(\n `${lcsBaseUrl}/v1/feedback?${userQueryParam}`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: requestBody,\n },\n );\n\n if (!fetchResponse.ok) {\n await handleLCSFetchError(\n fetchResponse,\n logger,\n 'sending feedback',\n response,\n );\n return;\n }\n\n const data = await fetchResponse.json();\n response.status(fetchResponse.status).json(data);\n } catch (error) {\n const errormsg = `Error while sending feedback: ${error}`;\n logger.error(errormsg);\n response.status(500).json({ error: errormsg });\n }\n },\n );\n\n router.post(\n '/v1/query/interrupt',\n generalRateLimiter,\n requirePermission(lightspeedChatCreatePermission),\n async (request, response) => {\n try {\n const { userEntityRef } = getIdentity(request);\n const userQueryParam = `user_id=${encodeURIComponent(userEntityRef)}`;\n const requestBody = JSON.stringify(request.body);\n const fetchResponse = await fetch(\n `${lcsBaseUrl}/v1/streaming_query/interrupt?${userQueryParam}`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: requestBody,\n },\n );\n if (!fetchResponse.ok) {\n await handleLCSFetchError(\n fetchResponse,\n logger,\n 'interrupting query',\n response,\n );\n return;\n }\n response.status(fetchResponse.status).json(await fetchResponse.json());\n } catch (error) {\n const errormsg = `Error while interrupting query: ${error}`;\n logger.error(errormsg);\n response.status(500).json({ error: errormsg });\n }\n },\n );\n\n router.post(\n '/v1/query',\n express.json({ limit: EXPRESS_JSON_BODY_LIMIT }),\n expensiveRateLimiter,\n validateCompletionsRequest,\n requirePermission(lightspeedChatCreatePermission),\n async (request, response) => {\n const { provider }: Pick<QueryRequestBody, 'provider'> = request.body;\n try {\n const { userEntityRef, credentials } = getIdentity(request);\n\n logger.info(`/v1/query receives call from user: ${userEntityRef}`);\n\n const userQueryParam = `user_id=${encodeURIComponent(userEntityRef)}`;\n request.body.media_type = 'application/json'; // set media_type to receive start and end event\n // if system_prompt is defined in lightspeed config\n // set system_prompt to override the default rhdh system prompt\n if (system_prompt && system_prompt.trim().length > 0) {\n request.body.system_prompt = system_prompt;\n }\n\n const requestBody = JSON.stringify(request.body);\n\n // Build MCP headers from config servers + this user's preferences.\n // For servers with auth: dcr, a per-user Backstage token is minted.\n const mcpHeadersValue = await buildMcpHeaders(\n staticServers,\n settingsStore,\n userEntityRef,\n { dcrAuth: { authService: auth, credentials }, logger },\n );\n\n const abortController = new AbortController();\n\n const fetchResponse = await fetch(\n `${lcsBaseUrl}/v1/streaming_query?${userQueryParam}`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'MCP-HEADERS': mcpHeadersValue,\n },\n body: requestBody,\n signal: abortController.signal,\n },\n );\n\n if (!fetchResponse.ok) {\n await handleLCSFetchError(\n fetchResponse,\n logger,\n 'processing query',\n response,\n );\n return;\n }\n\n // Pipe the response back to the original response\n if (fetchResponse.body) {\n const nodeStream = Readable.fromWeb(fetchResponse.body as any);\n\n nodeStream.on('error', (error: Error) => {\n logger.error(\n `Upstream stream error while processing query: ${error}`,\n );\n if (response.headersSent) {\n response.destroy();\n } else {\n response.status(500).json({ error: 'Stream error occurred' });\n }\n abortController.abort();\n });\n\n response.on('close', () => {\n if (!response.writableFinished) {\n logger.warn('Client disconnected while processing query');\n nodeStream.destroy();\n abortController.abort();\n }\n });\n\n nodeStream.pipe(response);\n }\n } catch (error) {\n const errormsg = `Error fetching completions from ${provider}: ${error}`;\n logger.error(errormsg);\n response.status(500).json({ error: errormsg });\n }\n },\n );\n\n router.put(\n '/v2/conversations/:conversation_id',\n generalRateLimiter,\n requirePermission(lightspeedChatCreatePermission),\n async (request, response) => {\n try {\n const { userEntityRef } = getIdentity(request);\n const conversation_id = request.params.conversation_id;\n\n const requestBody = JSON.stringify(request.body);\n const userQueryParam = `user_id=${encodeURIComponent(userEntityRef)}`;\n const fetchResponse = await fetch(\n `${lcsBaseUrl}/v2/conversations/${conversation_id}?${userQueryParam}`,\n {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: requestBody,\n },\n );\n if (!fetchResponse.ok) {\n await handleLCSFetchError(\n fetchResponse,\n logger,\n 'updating conversation',\n response,\n );\n return;\n }\n\n const data = await fetchResponse.json();\n response.status(fetchResponse.status).json(data);\n } catch (error) {\n const errormsg = `Error while updating topic summary: ${error}`;\n logger.error(errormsg);\n response.status(500).json({ error: errormsg });\n }\n },\n );\n\n router.use((_request, response) => {\n response.status(404).json({ error: 'Requested path is not available' });\n });\n\n const middleware = MiddlewareFactory.create({ logger, config });\n\n router.use(middleware.error());\n return router;\n}\n"],"names":["Router","express","DEFAULT_LIGHTSPEED_SERVICE_PORT","DEFAULT_LIGHTSPEED_SERVICE_HOST","createProxyMiddleware","rewriteProxyPath","VectorStoresOperator","createTokenEncryptor","McpUserSettingsStore","McpServerValidator","createPermissionIntegrationRouter","lightspeedPermissions","createIdentityMiddleware","userPermissionAuthorization","createPermissionMiddleware","createRateLimitMiddleware","lightspeedMcpReadPermission","getIdentity","lightspeedMcpManagePermission","NotAllowedError","lightspeedChatReadPermission","lightspeedChatDeletePermission","lightspeedChatCreatePermission","handleLCSFetchError","EXPRESS_JSON_BODY_LIMIT","validateCompletionsRequest","Readable","MiddlewareFactory"],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ var node_crypto = require('node:crypto');
4
+
5
+ const ALGORITHM = "aes-256-gcm";
6
+ const IV_LENGTH = 12;
7
+ const AUTH_TAG_LENGTH = 16;
8
+ const ENCRYPTED_PREFIX = "enc:";
9
+ function deriveKey(secret) {
10
+ return node_crypto.createHash("sha256").update(secret).digest();
11
+ }
12
+ function decryptWithKey(key, stored) {
13
+ const combined = Buffer.from(stored.slice(ENCRYPTED_PREFIX.length), "base64");
14
+ const iv = combined.subarray(0, IV_LENGTH);
15
+ const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
16
+ const encrypted = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
17
+ const decipher = node_crypto.createDecipheriv(ALGORITHM, key, iv, {
18
+ authTagLength: AUTH_TAG_LENGTH
19
+ });
20
+ decipher.setAuthTag(authTag);
21
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString(
22
+ "utf8"
23
+ );
24
+ }
25
+ class AesGcmEncryptor {
26
+ enabled = true;
27
+ primaryKey;
28
+ allKeys;
29
+ logger;
30
+ constructor(secrets, logger) {
31
+ this.primaryKey = deriveKey(secrets[0]);
32
+ this.allKeys = secrets.map(deriveKey);
33
+ this.logger = logger;
34
+ }
35
+ encrypt(plaintext) {
36
+ const iv = node_crypto.randomBytes(IV_LENGTH);
37
+ const cipher = node_crypto.createCipheriv(ALGORITHM, this.primaryKey, iv, {
38
+ authTagLength: AUTH_TAG_LENGTH
39
+ });
40
+ const encrypted = Buffer.concat([
41
+ cipher.update(plaintext, "utf8"),
42
+ cipher.final()
43
+ ]);
44
+ const authTag = cipher.getAuthTag();
45
+ const combined = Buffer.concat([iv, authTag, encrypted]);
46
+ return `${ENCRYPTED_PREFIX}${combined.toString("base64")}`;
47
+ }
48
+ decrypt(stored) {
49
+ if (!stored.startsWith(ENCRYPTED_PREFIX)) {
50
+ this.logger.warn(
51
+ "Found legacy plaintext token in database \u2014 it will be re-encrypted with the current key"
52
+ );
53
+ return { plaintext: stored, needsReEncrypt: true };
54
+ }
55
+ for (let i = 0; i < this.allKeys.length; i++) {
56
+ try {
57
+ const plaintext = decryptWithKey(this.allKeys[i], stored);
58
+ if (i > 0) {
59
+ this.logger.info(
60
+ `Decrypted token using non-primary key at index ${i} \u2014 it will be re-encrypted with the primary key`
61
+ );
62
+ }
63
+ return { plaintext, needsReEncrypt: i > 0 };
64
+ } catch {
65
+ }
66
+ }
67
+ this.logger.error(
68
+ "Failed to decrypt token with any configured key \u2014 token will be treated as missing"
69
+ );
70
+ return { plaintext: null, needsReEncrypt: false };
71
+ }
72
+ }
73
+ class PlaintextPassthrough {
74
+ enabled = false;
75
+ encrypt(plaintext) {
76
+ return plaintext;
77
+ }
78
+ decrypt(stored) {
79
+ if (stored.startsWith(ENCRYPTED_PREFIX)) {
80
+ return { plaintext: null, needsReEncrypt: false };
81
+ }
82
+ return { plaintext: stored, needsReEncrypt: false };
83
+ }
84
+ }
85
+ function createTokenEncryptor(config, logger) {
86
+ const keys = config.getOptionalConfigArray("backend.auth.keys");
87
+ const secrets = keys?.map((k) => k.getOptionalString("secret")).filter((s) => !!s);
88
+ if (secrets && secrets.length > 0) {
89
+ logger.info(
90
+ `Token encryption enabled \u2014 MCP user tokens will be encrypted at rest using backend.auth.keys (${secrets.length} key(s) configured)`
91
+ );
92
+ return new AesGcmEncryptor(secrets, logger);
93
+ }
94
+ logger.warn(
95
+ "Token encryption disabled \u2014 backend.auth.keys not configured. MCP user tokens will be stored as plaintext. Configure backend.auth.keys for production deployments."
96
+ );
97
+ return new PlaintextPassthrough();
98
+ }
99
+
100
+ exports.createTokenEncryptor = createTokenEncryptor;
101
+ //# sourceMappingURL=token-encryption.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-encryption.cjs.js","sources":["../../src/service/token-encryption.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { LoggerService } from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\n\nimport {\n createCipheriv,\n createDecipheriv,\n createHash,\n randomBytes,\n} from 'node:crypto';\n\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12;\nconst AUTH_TAG_LENGTH = 16;\nconst ENCRYPTED_PREFIX = 'enc:';\n\nexport interface DecryptResult {\n plaintext: string | null;\n needsReEncrypt: boolean;\n}\n\nexport interface TokenEncryptor {\n encrypt(plaintext: string): string;\n decrypt(stored: string): DecryptResult;\n readonly enabled: boolean;\n}\n\nfunction deriveKey(secret: string): Buffer {\n return createHash('sha256').update(secret).digest();\n}\n\nfunction decryptWithKey(key: Buffer, stored: string): string {\n const combined = Buffer.from(stored.slice(ENCRYPTED_PREFIX.length), 'base64');\n const iv = combined.subarray(0, IV_LENGTH);\n const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);\n const encrypted = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);\n const decipher = createDecipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH,\n });\n decipher.setAuthTag(authTag);\n return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString(\n 'utf8',\n );\n}\n\nclass AesGcmEncryptor implements TokenEncryptor {\n readonly enabled = true;\n private readonly primaryKey: Buffer;\n private readonly allKeys: Buffer[];\n private readonly logger: LoggerService;\n\n constructor(secrets: string[], logger: LoggerService) {\n this.primaryKey = deriveKey(secrets[0]);\n this.allKeys = secrets.map(deriveKey);\n this.logger = logger;\n }\n\n encrypt(plaintext: string): string {\n const iv = randomBytes(IV_LENGTH);\n const cipher = createCipheriv(ALGORITHM, this.primaryKey, iv, {\n authTagLength: AUTH_TAG_LENGTH,\n });\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n const combined = Buffer.concat([iv, authTag, encrypted]);\n return `${ENCRYPTED_PREFIX}${combined.toString('base64')}`;\n }\n\n decrypt(stored: string): DecryptResult {\n if (!stored.startsWith(ENCRYPTED_PREFIX)) {\n this.logger.warn(\n 'Found legacy plaintext token in database — it will be re-encrypted with the current key',\n );\n return { plaintext: stored, needsReEncrypt: true };\n }\n for (let i = 0; i < this.allKeys.length; i++) {\n try {\n const plaintext = decryptWithKey(this.allKeys[i], stored);\n if (i > 0) {\n this.logger.info(\n `Decrypted token using non-primary key at index ${i} — it will be re-encrypted with the primary key`,\n );\n }\n return { plaintext, needsReEncrypt: i > 0 };\n } catch {\n // Try the next key\n }\n }\n this.logger.error(\n 'Failed to decrypt token with any configured key — token will be treated as missing',\n );\n return { plaintext: null, needsReEncrypt: false };\n }\n}\n\nclass PlaintextPassthrough implements TokenEncryptor {\n readonly enabled = false;\n\n encrypt(plaintext: string): string {\n return plaintext;\n }\n\n decrypt(stored: string): DecryptResult {\n if (stored.startsWith(ENCRYPTED_PREFIX)) {\n return { plaintext: null, needsReEncrypt: false };\n }\n return { plaintext: stored, needsReEncrypt: false };\n }\n}\n\nexport function createTokenEncryptor(\n config: Config,\n logger: LoggerService,\n): TokenEncryptor {\n const keys = config.getOptionalConfigArray('backend.auth.keys');\n const secrets = keys\n ?.map(k => k.getOptionalString('secret'))\n .filter((s): s is string => !!s);\n\n if (secrets && secrets.length > 0) {\n logger.info(\n `Token encryption enabled — MCP user tokens will be encrypted at rest using backend.auth.keys (${secrets.length} key(s) configured)`,\n );\n return new AesGcmEncryptor(secrets, logger);\n }\n\n logger.warn(\n 'Token encryption disabled — backend.auth.keys not configured. ' +\n 'MCP user tokens will be stored as plaintext. ' +\n 'Configure backend.auth.keys for production deployments.',\n );\n return new PlaintextPassthrough();\n}\n"],"names":["createHash","createDecipheriv","randomBytes","createCipheriv"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ var constant = require('./constant.cjs.js');
4
+
5
+ function sanitizeLCSError(errorBody, logger, context) {
6
+ logger.error(
7
+ `Error from lightspeed-core server while ${context}: ${JSON.stringify(errorBody)}`
8
+ );
9
+ return `Error from lightspeed-core server while ${context}`;
10
+ }
11
+ async function handleLCSFetchError(fetchResponse, logger, context, response) {
12
+ let errorBody;
13
+ try {
14
+ errorBody = await fetchResponse.json();
15
+ } catch {
16
+ errorBody = {};
17
+ }
18
+ const sanitizedError = sanitizeLCSError(errorBody, logger, context);
19
+ response.status(fetchResponse.status).json({ error: sanitizedError });
20
+ }
21
+ function rewriteProxyPath(path, userEntityRef) {
22
+ const isSkippable = Array.from(constant.SKIP_USER_ID_ENDPOINTS).some(
23
+ (endpoint) => path.startsWith(endpoint)
24
+ );
25
+ if (isSkippable) {
26
+ return path;
27
+ }
28
+ const userQueryParam = `user_id=${encodeURIComponent(userEntityRef)}`;
29
+ let newPath = path.includes("?") ? `${path}&${userQueryParam}` : `${path}?${userQueryParam}`;
30
+ if (!path.includes("history_length") && path.includes("conversation_id")) {
31
+ const historyLengthQuery = `history_length=${constant.DEFAULT_HISTORY_LENGTH}`;
32
+ newPath = newPath.includes("?") ? `${newPath}&${historyLengthQuery}` : `${newPath}?${historyLengthQuery}`;
33
+ }
34
+ return newPath;
35
+ }
36
+
37
+ exports.handleLCSFetchError = handleLCSFetchError;
38
+ exports.rewriteProxyPath = rewriteProxyPath;
39
+ exports.sanitizeLCSError = sanitizeLCSError;
40
+ //# sourceMappingURL=utils.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.cjs.js","sources":["../../src/service/utils.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nimport { DEFAULT_HISTORY_LENGTH, SKIP_USER_ID_ENDPOINTS } from './constant';\n\n/**\n * Interface for Lightspeed Core Service error response structure\n */\nexport interface LCSErrorResponse {\n error?: {\n message?: string;\n };\n detail?: {\n cause?: string;\n };\n}\n\n/**\n * Sanitizes Lightspeed Core Service (LCS) error responses to prevent\n * information disclosure. Logs full error details server-side for debugging\n * while returning only generic messages to clients.\n *\n * @param errorBody - The error response body from LCS\n * @param logger - Logger instance for server-side logging\n * @param context - Context string describing the operation (e.g., \"sending feedback\")\n * @returns Generic error message safe to return to clients\n */\nexport function sanitizeLCSError(\n errorBody: LCSErrorResponse,\n logger: LoggerService,\n context: string,\n): string {\n // Log full error details server-side for debugging\n logger.error(\n `Error from lightspeed-core server while ${context}: ${JSON.stringify(errorBody)}`,\n );\n\n // Return only generic message to client (no internal LCS details)\n return `Error from lightspeed-core server while ${context}`;\n}\n\n/**\n * Handles LCS fetch errors by sanitizing the error response and sending it to the client.\n * This helper eliminates code duplication across multiple endpoints.\n *\n * @param fetchResponse - The failed fetch response from LCS\n * @param logger - Logger instance for server-side logging\n * @param context - Context string describing the operation\n * @param response - Express response object\n */\nexport async function handleLCSFetchError(\n fetchResponse: Response,\n logger: LoggerService,\n context: string,\n response: { status: (code: number) => { json: (body: any) => void } },\n): Promise<void> {\n let errorBody: LCSErrorResponse;\n try {\n errorBody = await fetchResponse.json();\n } catch {\n // If LCS doesn't return JSON or doesn't have a body, use empty object\n errorBody = {};\n }\n const sanitizedError = sanitizeLCSError(errorBody, logger, context);\n response.status(fetchResponse.status).json({ error: sanitizedError });\n}\n\nexport function rewriteProxyPath(path: string, userEntityRef: string): string {\n const isSkippable = Array.from(SKIP_USER_ID_ENDPOINTS).some(endpoint =>\n path.startsWith(endpoint),\n );\n if (isSkippable) {\n return path;\n }\n\n const userQueryParam = `user_id=${encodeURIComponent(userEntityRef)}`;\n let newPath = path.includes('?')\n ? `${path}&${userQueryParam}`\n : `${path}?${userQueryParam}`;\n\n if (!path.includes('history_length') && path.includes('conversation_id')) {\n const historyLengthQuery = `history_length=${DEFAULT_HISTORY_LENGTH}`;\n newPath = newPath.includes('?')\n ? `${newPath}&${historyLengthQuery}`\n : `${newPath}?${historyLengthQuery}`;\n }\n\n return newPath;\n}\n"],"names":["SKIP_USER_ID_ENDPOINTS","DEFAULT_HISTORY_LENGTH"],"mappings":";;;;AA0CO,SAAS,gBAAA,CACd,SAAA,EACA,MAAA,EACA,OAAA,EACQ;AAER,EAAA,MAAA,CAAO,KAAA;AAAA,IACL,2CAA2C,OAAO,CAAA,EAAA,EAAK,IAAA,CAAK,SAAA,CAAU,SAAS,CAAC,CAAA;AAAA,GAClF;AAGA,EAAA,OAAO,2CAA2C,OAAO,CAAA,CAAA;AAC3D;AAWA,eAAsB,mBAAA,CACpB,aAAA,EACA,MAAA,EACA,OAAA,EACA,QAAA,EACe;AACf,EAAA,IAAI,SAAA;AACJ,EAAA,IAAI;AACF,IAAA,SAAA,GAAY,MAAM,cAAc,IAAA,EAAK;AAAA,EACvC,CAAA,CAAA,MAAQ;AAEN,IAAA,SAAA,GAAY,EAAC;AAAA,EACf;AACA,EAAA,MAAM,cAAA,GAAiB,gBAAA,CAAiB,SAAA,EAAW,MAAA,EAAQ,OAAO,CAAA;AAClE,EAAA,QAAA,CAAS,MAAA,CAAO,cAAc,MAAM,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,gBAAgB,CAAA;AACtE;AAEO,SAAS,gBAAA,CAAiB,MAAc,aAAA,EAA+B;AAC5E,EAAA,MAAM,WAAA,GAAc,KAAA,CAAM,IAAA,CAAKA,+BAAsB,CAAA,CAAE,IAAA;AAAA,IAAK,CAAA,QAAA,KAC1D,IAAA,CAAK,UAAA,CAAW,QAAQ;AAAA,GAC1B;AACA,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,cAAA,GAAiB,CAAA,QAAA,EAAW,kBAAA,CAAmB,aAAa,CAAC,CAAA,CAAA;AACnE,EAAA,IAAI,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,GAC3B,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,cAAc,CAAA,CAAA,GACzB,CAAA,EAAG,IAAI,IAAI,cAAc,CAAA,CAAA;AAE7B,EAAA,IAAI,CAAC,KAAK,QAAA,CAAS,gBAAgB,KAAK,IAAA,CAAK,QAAA,CAAS,iBAAiB,CAAA,EAAG;AACxE,IAAA,MAAM,kBAAA,GAAqB,kBAAkBC,+BAAsB,CAAA,CAAA;AACnE,IAAA,OAAA,GAAU,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,GAC1B,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,kBAAkB,CAAA,CAAA,GAChC,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,kBAAkB,CAAA,CAAA;AAAA,EACtC;AAEA,EAAA,OAAO,OAAA;AACT;;;;;;"}
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ var constant = require('./constant.cjs.js');
4
+
5
+ function validateAttachments(attachments) {
6
+ let totalSize = 0;
7
+ for (const attachment of attachments) {
8
+ const attachmentSize = attachment.content ? Buffer.byteLength(attachment.content, "utf8") : 0;
9
+ if (attachmentSize > constant.MAX_ATTACHMENT_SIZE_BYTES) {
10
+ return `Attachment "${attachment.name}" exceeds maximum size of ${constant.MAX_ATTACHMENT_SIZE_BYTES / (1024 * 1024)}MB`;
11
+ }
12
+ totalSize += attachmentSize;
13
+ }
14
+ if (totalSize > constant.MAX_TOTAL_ATTACHMENTS_SIZE_BYTES) {
15
+ return `Total attachments size exceeds maximum of ${constant.MAX_TOTAL_ATTACHMENTS_SIZE_BYTES / (1024 * 1024)}MB`;
16
+ }
17
+ return null;
18
+ }
19
+ const validateCompletionsRequest = (req, res, next) => {
20
+ const reqData = req.body;
21
+ if (typeof reqData.model !== "string" || reqData.model.trim() === "") {
22
+ return res.status(400).json({ error: "model is required and must be a non-empty string" });
23
+ }
24
+ if (typeof reqData.provider !== "string" || reqData.provider.trim() === "") {
25
+ return res.status(400).json({ error: "provider is required and must be a non-empty string" });
26
+ }
27
+ if (typeof reqData.query !== "string" || reqData.query.trim() === "") {
28
+ return res.status(400).json({ error: "query is required and must be a non-empty string" });
29
+ }
30
+ if (reqData.query.length > constant.MAX_QUERY_LENGTH) {
31
+ return res.status(400).json({
32
+ error: `query exceeds maximum length of ${constant.MAX_QUERY_LENGTH} characters`
33
+ });
34
+ }
35
+ if (reqData.attachments && Array.isArray(reqData.attachments)) {
36
+ const attachmentError = validateAttachments(reqData.attachments);
37
+ if (attachmentError) {
38
+ return res.status(400).json({ error: attachmentError });
39
+ }
40
+ }
41
+ return next();
42
+ };
43
+
44
+ exports.validateCompletionsRequest = validateCompletionsRequest;
45
+ //# sourceMappingURL=validation.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.cjs.js","sources":["../../src/service/validation.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { NextFunction, Request, Response } from 'express';\n\nimport {\n MAX_ATTACHMENT_SIZE_BYTES,\n MAX_QUERY_LENGTH,\n MAX_TOTAL_ATTACHMENTS_SIZE_BYTES,\n} from './constant';\nimport { QueryRequestBody } from './types';\n\nfunction validateAttachments(\n attachments: Array<{ name: string; content?: string }>,\n): string | null {\n let totalSize = 0;\n\n for (const attachment of attachments) {\n const attachmentSize = attachment.content\n ? Buffer.byteLength(attachment.content, 'utf8')\n : 0;\n\n if (attachmentSize > MAX_ATTACHMENT_SIZE_BYTES) {\n return `Attachment \"${attachment.name}\" exceeds maximum size of ${MAX_ATTACHMENT_SIZE_BYTES / (1024 * 1024)}MB`;\n }\n\n totalSize += attachmentSize;\n }\n\n if (totalSize > MAX_TOTAL_ATTACHMENTS_SIZE_BYTES) {\n return `Total attachments size exceeds maximum of ${MAX_TOTAL_ATTACHMENTS_SIZE_BYTES / (1024 * 1024)}MB`;\n }\n\n return null;\n}\n\nexport const validateCompletionsRequest = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => {\n const reqData: QueryRequestBody = req.body;\n\n if (typeof reqData.model !== 'string' || reqData.model.trim() === '') {\n return res\n .status(400)\n .json({ error: 'model is required and must be a non-empty string' });\n }\n\n if (typeof reqData.provider !== 'string' || reqData.provider.trim() === '') {\n return res\n .status(400)\n .json({ error: 'provider is required and must be a non-empty string' });\n }\n\n if (typeof reqData.query !== 'string' || reqData.query.trim() === '') {\n return res\n .status(400)\n .json({ error: 'query is required and must be a non-empty string' });\n }\n\n if (reqData.query.length > MAX_QUERY_LENGTH) {\n return res.status(400).json({\n error: `query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`,\n });\n }\n\n if (reqData.attachments && Array.isArray(reqData.attachments)) {\n const attachmentError = validateAttachments(reqData.attachments);\n if (attachmentError) {\n return res.status(400).json({ error: attachmentError });\n }\n }\n\n return next();\n};\n\nexport const validateLoadHistoryRequest = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => {\n const historyLength = Number(req.query.historyLength);\n\n if (historyLength && !Number.isInteger(historyLength)) {\n return res.status(400).send('historyLength has to be a valid integer');\n }\n\n // TODO: Need to extract out the user_id from conversation_id, and verify with the login user entity\n\n return next();\n};\n"],"names":["MAX_ATTACHMENT_SIZE_BYTES","MAX_TOTAL_ATTACHMENTS_SIZE_BYTES","MAX_QUERY_LENGTH"],"mappings":";;;;AAyBA,SAAS,oBACP,WAAA,EACe;AACf,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,KAAA,MAAW,cAAc,WAAA,EAAa;AACpC,IAAA,MAAM,cAAA,GAAiB,WAAW,OAAA,GAC9B,MAAA,CAAO,WAAW,UAAA,CAAW,OAAA,EAAS,MAAM,CAAA,GAC5C,CAAA;AAEJ,IAAA,IAAI,iBAAiBA,kCAAA,EAA2B;AAC9C,MAAA,OAAO,eAAe,UAAA,CAAW,IAAI,CAAA,0BAAA,EAA6BA,kCAAA,IAA6B,OAAO,IAAA,CAAK,CAAA,EAAA,CAAA;AAAA,IAC7G;AAEA,IAAA,SAAA,IAAa,cAAA;AAAA,EACf;AAEA,EAAA,IAAI,YAAYC,yCAAA,EAAkC;AAChD,IAAA,OAAO,CAAA,0CAAA,EAA6CA,yCAAA,IAAoC,IAAA,GAAO,IAAA,CAAK,CAAA,EAAA,CAAA;AAAA,EACtG;AAEA,EAAA,OAAO,IAAA;AACT;AAEO,MAAM,0BAAA,GAA6B,CACxC,GAAA,EACA,GAAA,EACA,IAAA,KACG;AACH,EAAA,MAAM,UAA4B,GAAA,CAAI,IAAA;AAEtC,EAAA,IAAI,OAAO,QAAQ,KAAA,KAAU,QAAA,IAAY,QAAQ,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AACpE,IAAA,OAAO,GAAA,CACJ,OAAO,GAAG,CAAA,CACV,KAAK,EAAE,KAAA,EAAO,oDAAoD,CAAA;AAAA,EACvE;AAEA,EAAA,IAAI,OAAO,QAAQ,QAAA,KAAa,QAAA,IAAY,QAAQ,QAAA,CAAS,IAAA,OAAW,EAAA,EAAI;AAC1E,IAAA,OAAO,GAAA,CACJ,OAAO,GAAG,CAAA,CACV,KAAK,EAAE,KAAA,EAAO,uDAAuD,CAAA;AAAA,EAC1E;AAEA,EAAA,IAAI,OAAO,QAAQ,KAAA,KAAU,QAAA,IAAY,QAAQ,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AACpE,IAAA,OAAO,GAAA,CACJ,OAAO,GAAG,CAAA,CACV,KAAK,EAAE,KAAA,EAAO,oDAAoD,CAAA;AAAA,EACvE;AAEA,EAAA,IAAI,OAAA,CAAQ,KAAA,CAAM,MAAA,GAASC,yBAAA,EAAkB;AAC3C,IAAA,OAAO,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK;AAAA,MAC1B,KAAA,EAAO,mCAAmCA,yBAAgB,CAAA,WAAA;AAAA,KAC3D,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,QAAQ,WAAA,IAAe,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,WAAW,CAAA,EAAG;AAC7D,IAAA,MAAM,eAAA,GAAkB,mBAAA,CAAoB,OAAA,CAAQ,WAAW,CAAA;AAC/D,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,OAAO,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,iBAAiB,CAAA;AAAA,IACxD;AAAA,EACF;AAEA,EAAA,OAAO,IAAA,EAAK;AACd;;;;"}
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright Red Hat, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * @param {import('knex').Knex} knex
19
+ */
20
+ exports.up = async function up(knex) {
21
+ await knex.schema.createTable('lightspeed_mcp_user_settings', table => {
22
+ table.string('id').primary().notNullable();
23
+ table.string('server_name').notNullable();
24
+ table.string('user_entity_ref').notNullable();
25
+ table.boolean('enabled').notNullable().defaultTo(true);
26
+ table.text('token'); // nullable — user override for admin default
27
+ table.string('status').notNullable().defaultTo('unknown');
28
+ table.integer('tool_count').notNullable().defaultTo(0);
29
+ table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
30
+ table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
31
+
32
+ table.unique(['server_name', 'user_entity_ref']);
33
+ });
34
+ };
35
+
36
+ /**
37
+ * @param {import('knex').Knex} knex
38
+ */
39
+ exports.down = async function down(knex) {
40
+ await knex.schema.dropTableIfExists('lightspeed_mcp_user_settings');
41
+ };
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend",
3
+ "version": "3.0.0",
4
+ "main": "./dist/index.cjs.js",
5
+ "types": "./dist/index.d.ts",
6
+ "license": "Apache-2.0",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "backstage": {
11
+ "role": "backend-plugin",
12
+ "pluginId": "intelligent-assistant",
13
+ "pluginPackage": "@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend",
14
+ "pluginPackages": [
15
+ "@red-hat-developer-hub/backstage-plugin-intelligent-assistant",
16
+ "@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend",
17
+ "@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common"
18
+ ],
19
+ "features": {
20
+ ".": "@backstage/BackendFeature"
21
+ }
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "backstage": "@backstage/BackendFeature",
26
+ "require": "./dist/index.cjs.js",
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.cjs.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "typesVersions": {
33
+ "*": {
34
+ "package.json": [
35
+ "package.json"
36
+ ]
37
+ }
38
+ },
39
+ "scripts": {
40
+ "build": "backstage-cli package build",
41
+ "clean": "backstage-cli package clean",
42
+ "lint:check": "backstage-cli package lint",
43
+ "lint:fix": "backstage-cli package lint --fix",
44
+ "postpack": "backstage-cli package postpack",
45
+ "prepack": "backstage-cli package prepack",
46
+ "start": "backstage-cli package start",
47
+ "test": "backstage-cli package test --passWithNoTests --coverage",
48
+ "tsc": "tsc",
49
+ "prettier:check": "prettier --ignore-unknown --check .",
50
+ "prettier:fix": "prettier --ignore-unknown --write ."
51
+ },
52
+ "dependencies": {
53
+ "@backstage/backend-defaults": "^0.17.4",
54
+ "@backstage/backend-plugin-api": "^1.9.2",
55
+ "@backstage/config": "^1.3.8",
56
+ "@backstage/errors": "^1.3.1",
57
+ "@backstage/plugin-permission-common": "^0.9.9",
58
+ "@backstage/plugin-permission-node": "^0.11.1",
59
+ "@langchain/core": "^0.3.80",
60
+ "@langchain/openai": "^0.6.0",
61
+ "@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common": "^3.0.0",
62
+ "express": "^4.21.1",
63
+ "express-rate-limit": "^8.2.2",
64
+ "form-data": "^4.0.5",
65
+ "htmlparser2": "^9.1.0",
66
+ "http-proxy-middleware": "^3.0.2",
67
+ "js-yaml": "^4.1.1",
68
+ "knex": "^3.1.0",
69
+ "multer": "^2.1.1",
70
+ "pdfjs-dist": "^4.10.38"
71
+ },
72
+ "devDependencies": {
73
+ "@backstage/backend-test-utils": "^1.11.4",
74
+ "@backstage/cli": "^0.36.3",
75
+ "@ianvs/prettier-plugin-sort-imports": "^4.4.0",
76
+ "@spotify/prettier-config": "^15.0.0",
77
+ "@types/express": "4.17.25",
78
+ "@types/js-yaml": "^4",
79
+ "@types/multer": "^1.4.12",
80
+ "@types/supertest": "2.0.16",
81
+ "msw": "2.14.6",
82
+ "prettier": "3.8.4",
83
+ "supertest": "6.3.4"
84
+ },
85
+ "resolutions": {
86
+ "langsmith": "^0.7.0"
87
+ },
88
+ "files": [
89
+ "dist",
90
+ "config.d.ts",
91
+ "app-config.yaml",
92
+ "migrations"
93
+ ],
94
+ "configSchema": "config.d.ts",
95
+ "repository": {
96
+ "type": "git",
97
+ "url": "git+https://github.com/redhat-developer/rhdh-plugins.git",
98
+ "directory": "workspaces/intelligent-assistant/plugins/intelligent-assistant-backend"
99
+ },
100
+ "keywords": [
101
+ "backstage",
102
+ "plugin"
103
+ ],
104
+ "homepage": "https://red.ht/rhdh",
105
+ "bugs": "https://github.com/redhat-developer/rhdh-plugins/issues",
106
+ "maintainers": [
107
+ "@yangcao77"
108
+ ]
109
+ }