@open-mercato/ai-assistant 0.6.5-develop.5099.1.85c0aff78c → 0.6.5-develop.5116.1.f0af9e5080

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.
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 211 entry points
1
+ [build:ai-assistant] found 212 entry points
2
2
  [build:ai-assistant] built successfully
@@ -7,6 +7,7 @@ import { executeTool } from "./tool-executor.js";
7
7
  import { loadAllModuleTools, indexToolsForSearch } from "./tool-loader.js";
8
8
  import { extractApiKeyFromHeaders, hasRequiredFeatures } from "./auth.js";
9
9
  import { jsonSchemaToZod } from "./schema-utils.js";
10
+ import { redactSecretForLog, deriveApiKeySessionId } from "./log-redaction.js";
10
11
  import { findApiKeyBySecret, findSessionApiKeyWithSecret } from "@open-mercato/core/modules/api_keys/services/apiKeyService";
11
12
  async function resolveSessionContext(sessionToken, baseContext, debug) {
12
13
  try {
@@ -15,7 +16,7 @@ async function resolveSessionContext(sessionToken, baseContext, debug) {
15
16
  const sessionResult = await findSessionApiKeyWithSecret(em, sessionToken);
16
17
  if (!sessionResult) {
17
18
  if (debug) {
18
- console.error(`[MCP HTTP] Session token not found, expired, or secret unavailable: ${sessionToken}`);
19
+ console.error(`[MCP HTTP] Session token not found, expired, or secret unavailable: ${redactSecretForLog(sessionToken)}`);
19
20
  }
20
21
  return null;
21
22
  }
@@ -190,7 +191,7 @@ function createMcpServerForRequest(config, toolContext, apiKeyRecord) {
190
191
  if (!effectiveContext.sessionId && effectiveContext.apiKeySecret) {
191
192
  effectiveContext = {
192
193
  ...effectiveContext,
193
- sessionId: "apikey_" + effectiveContext.apiKeySecret.slice(0, 16)
194
+ sessionId: deriveApiKeySessionId(effectiveContext.apiKeySecret)
194
195
  };
195
196
  }
196
197
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/http-server.ts"],
4
- "sourcesContent": ["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z, type ZodType } from 'zod'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'\nimport { jsonSchemaToZod, toSafeZodSchema } from './schema-utils'\nimport type { McpServerConfig, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'\nimport { findApiKeyBySecret, findSessionApiKeyWithSecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\n\n/**\n * Options for the HTTP MCP server.\n */\nexport type McpHttpServerOptions = {\n config: McpServerConfig\n container: AwilixContainer\n port: number\n}\n\n/**\n * Resolve user context from session token.\n * Returns null if session token is invalid or expired.\n * Includes the decrypted API key secret for making authenticated API calls.\n */\nasync function resolveSessionContext(\n sessionToken: string,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const em = baseContext.container.resolve<EntityManager>('em')\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n\n // Look up ephemeral key by session token with decrypted secret\n const sessionResult = await findSessionApiKeyWithSecret(em, sessionToken)\n if (!sessionResult) {\n if (debug) {\n console.error(`[MCP HTTP] Session token not found, expired, or secret unavailable: ${sessionToken}`)\n }\n return null\n }\n\n const { key: sessionKey, secret: sessionSecret } = sessionResult\n\n // Load ACL for the session user\n const userId = sessionKey.sessionUserId || sessionKey.createdBy\n if (!userId) {\n if (debug) {\n console.error(`[MCP HTTP] Session key has no associated user`)\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${sessionKey.id}`, {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n })\n\n if (debug) {\n console.error(`[MCP HTTP] Session context resolved for user ${userId}:`, {\n tenantId: sessionKey.tenantId,\n organizationId: sessionKey.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n hasSessionSecret: !!sessionSecret,\n })\n }\n\n return {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n // Use the decrypted session secret for API calls (not the MCP server key)\n apiKeySecret: sessionSecret,\n }\n } catch (error) {\n if (debug) {\n console.error(`[MCP HTTP] Error resolving session context:`, error)\n }\n return null\n }\n}\n\n/**\n * Resolve user context from the server-level API key (header-based auth fallback).\n * Used when no session token is provided \u2014 loads the API key's ACL for RBAC.\n */\nasync function resolveApiKeyContext(\n apiKeyRecord: ApiKey,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n const userId = apiKeyRecord.sessionUserId ?? apiKeyRecord.createdBy\n if (!userId) {\n if (debug) {\n console.error(`[MCP HTTP] API key has no associated user`)\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${apiKeyRecord.id}`, {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n })\n\n if (debug) {\n console.error(`[MCP HTTP] API key context resolved for user ${userId}:`, {\n tenantId: apiKeyRecord.tenantId,\n organizationId: apiKeyRecord.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n })\n }\n\n return {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n apiKeySecret: baseContext.apiKeySecret,\n }\n } catch (error) {\n if (debug) {\n console.error(`[MCP HTTP] Error resolving API key context:`, error)\n }\n return null\n }\n}\n\n/**\n * Create a stateless MCP server instance for a single request.\n * Tools are registered without pre-filtering - permission checks happen at execution time\n * based on the session token provided in each tool call.\n */\nfunction createMcpServerForRequest(\n config: McpServerConfig,\n toolContext: McpToolContext,\n apiKeyRecord: ApiKey\n): McpServer {\n const server = new McpServer(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n if (config.debug) {\n console.error(`[MCP HTTP] Registering ${tools.length} tools (ACL checked per-call via session token)`)\n }\n\n // Register ALL tools - permission checks happen at execution time via session token\n for (const tool of tools) {\n if (config.debug) {\n console.error(`[MCP HTTP] Registering tool: ${tool.name}`)\n }\n\n // Convert Zod schema to a \"safe\" schema without Date types\n // This uses JSON Schema round-trip to avoid issues with MCP SDK's internal conversion\n // Also inject _sessionToken as an optional parameter so the AI knows to pass it\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n // Convert to JSON Schema first\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n\n // Inject _sessionToken into the JSON schema properties\n const properties = (jsonSchema.properties ?? {}) as Record<string, unknown>\n properties._sessionToken = {\n type: 'string',\n description: 'Session authorization token. If omitted, the server API key roles are used instead.',\n }\n jsonSchema.properties = properties\n\n // Convert back to Zod with passthrough to allow extra properties\n const converted = jsonSchemaToZod(jsonSchema)\n // Use type assertion since we know it's an object schema (we added properties above)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (config.debug) {\n console.error(\n `[MCP HTTP] Skipping tool ${tool.name} - schema conversion failed:`,\n error instanceof Error ? error.message : error\n )\n }\n continue\n }\n } else {\n // If no schema, create one with just _sessionToken\n safeSchema = z.object({\n _sessionToken: z\n .string()\n .optional()\n .describe('Session authorization token (REQUIRED for all tool calls)'),\n })\n }\n\n // Wrap in try/catch to handle any remaining edge cases\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n // Extract session token from args\n const sessionToken = toolArgs._sessionToken as string | undefined\n delete toolArgs._sessionToken // Remove before passing to tool handler\n\n // Always log tool calls for debugging\n console.error(`[MCP HTTP] \u25B6 Tool call: ${tool.name}`, {\n hasSessionToken: !!sessionToken,\n args: JSON.stringify(toolArgs).slice(0, 200),\n })\n\n // Resolve user context from session token\n let effectiveContext = toolContext\n if (sessionToken) {\n const sessionContext = await resolveSessionContext(sessionToken, toolContext, config.debug)\n if (sessionContext) {\n // Session context includes the decrypted API key secret + session ID for memory layer\n effectiveContext = { ...sessionContext, sessionId: sessionToken }\n } else {\n // Session token expired - return user-friendly error for AI to relay\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Your chat session has expired. Please close and reopen the chat window to continue.',\n code: 'SESSION_EXPIRED',\n }),\n },\n ],\n isError: true,\n }\n }\n } else {\n // No session token \u2014 fall back to header API key auth\n const apiKeyContext = await resolveApiKeyContext(apiKeyRecord, toolContext, config.debug)\n if (apiKeyContext) {\n effectiveContext = apiKeyContext\n } else if (!effectiveContext.userId && effectiveContext.userFeatures.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Authentication failed: provide a session token (_sessionToken) or a valid API key with assigned roles',\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n\n // Derive a fallback sessionId from the API key so all tool calls\n // within the same MCP connection share a session memory cache\n if (!effectiveContext.sessionId && effectiveContext.apiKeySecret) {\n effectiveContext = {\n ...effectiveContext,\n sessionId: 'apikey_' + effectiveContext.apiKeySecret.slice(0, 16),\n }\n }\n }\n\n // Check if user has required permissions for this tool\n if (tool.requiredFeatures?.length) {\n const rbacService = effectiveContext.container.resolve<RbacService>('rbacService')\n const hasAccess = hasRequiredFeatures(\n tool.requiredFeatures,\n effectiveContext.userFeatures,\n effectiveContext.isSuperAdmin,\n rbacService\n )\n if (!hasAccess) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: `Insufficient permissions for tool \"${tool.name}\". Required: ${tool.requiredFeatures.join(', ')}`,\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n }\n\n try {\n const result = await executeTool(tool.name, toolArgs, effectiveContext)\n\n if (!result.success) {\n console.error(`[MCP HTTP] \u2717 Tool error: ${tool.name}`, { error: result.error, code: result.errorCode })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n console.error(`[MCP HTTP] \u2713 Tool success: ${tool.name}`, {\n resultPreview: JSON.stringify(result.result).slice(0, 200)\n })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n } catch (err) {\n console.error(`[MCP HTTP] \u2717 Tool exception: ${tool.name}`, err)\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error', code: 'EXCEPTION' }),\n },\n ],\n isError: true,\n }\n }\n }\n )\n } catch (error) {\n // Skip tools with schemas that can't be registered\n if (config.debug) {\n console.error(\n `[MCP HTTP] Skipping tool ${tool.name} - registration failed:`,\n error instanceof Error ? error.message : error\n )\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Maximum request body size (1MB).\n * Prevents memory exhaustion from oversized payloads.\n */\nconst MAX_BODY_SIZE = 1 * 1024 * 1024\n\n/**\n * Parse JSON body from request with size limit.\n */\nasync function parseJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let totalSize = 0\n\n req.on('data', (chunk: Buffer) => {\n totalSize += chunk.length\n if (totalSize > MAX_BODY_SIZE) {\n req.destroy()\n reject(new Error('Request payload too large'))\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8')\n resolve(body ? JSON.parse(body) : undefined)\n } catch (error) {\n reject(error)\n }\n })\n req.on('error', reject)\n })\n}\n\n/**\n * Run MCP server with HTTP transport (stateless mode).\n *\n * Each request creates a new MCP server instance and transport.\n * The server authenticates requests using API keys from the x-api-key header.\n */\nexport async function runMcpHttpServer(options: McpHttpServerOptions): Promise<void> {\n const { config, container, port } = options\n\n await loadAllModuleTools()\n\n // Generate and cache entity graph for understand_entity tool\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n console.error(`[MCP HTTP] Entity graph: ${graph.nodes.length} entities, ${graph.edges.length} relationships`)\n } catch (error) {\n console.error('[MCP HTTP] Entity graph generation skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache rich OpenAPI spec for Code Mode search tool (prefers runtime module registry over static JSON)\n try {\n const { loadRichOpenApiSpec } = await import('./api-endpoint-index')\n const spec = await loadRichOpenApiSpec()\n if (spec) {\n console.error('[MCP HTTP] Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n console.error('[MCP HTTP] OpenAPI spec not available')\n }\n } catch (error) {\n console.error('[MCP HTTP] OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Index tools and entity schemas for hybrid search discovery (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n\n // Index MCP tools\n await indexToolsForSearch(searchService)\n\n // Index entity schemas for hybrid search\n try {\n const { getCachedEntityGraph } = await import('./entity-graph')\n const { indexEntitiesForSearch } = await import('./entity-index')\n const graph = getCachedEntityGraph()\n if (graph) {\n const { count } = await indexEntitiesForSearch(searchService, graph)\n if (count > 0) {\n console.error(`[MCP HTTP] Indexed ${count} entity schemas for hybrid search`)\n }\n }\n } catch (entityError) {\n console.error('[MCP HTTP] Entity schema indexing skipped:', entityError instanceof Error ? entityError.message : entityError)\n }\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n console.error('[MCP HTTP] Search indexing skipped (search service not available):', error)\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '/', `http://localhost:${port}`)\n\n // Health check endpoint\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({\n status: 'ok',\n tools: getToolRegistry().listToolNames().length,\n timestamp: new Date().toISOString(),\n }))\n return\n }\n\n if (url.pathname !== '/mcp') {\n res.writeHead(404, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Not found' }))\n return\n }\n\n console.error(`[MCP HTTP] \u2190 Request: ${req.method} ${url.pathname}`)\n\n // Extract headers\n const headers: Record<string, string | undefined> = {}\n for (const [key, value] of Object.entries(req.headers)) {\n headers[key] = Array.isArray(value) ? value[0] : value\n }\n\n // Server-level authentication via database lookup\n const providedApiKey = extractApiKeyFromHeaders(headers)\n if (!providedApiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))\n return\n }\n\n // Validate API key against database (prefix lookup + bcrypt verify + expiry check)\n const em = container.resolve<EntityManager>('em')\n const apiKeyRecord = await findApiKeyBySecret(em, providedApiKey)\n if (!apiKeyRecord) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid or expired API key' }))\n return\n }\n\n if (config.debug) {\n console.error(`[MCP HTTP] Server-level auth passed (${req.method}) - API key: ${apiKeyRecord.keyPrefix}...`)\n }\n\n // Create base tool context using API key's tenant/org scope\n // Session tokens can override with user-specific permissions\n const toolContext: McpToolContext = {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId: apiKeyRecord.createdBy ?? null,\n container,\n userFeatures: [],\n isSuperAdmin: false,\n apiKeySecret: providedApiKey,\n }\n\n try {\n // Create stateless transport (no session ID generator = stateless)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create new server for this request\n const mcpServer = createMcpServerForRequest(config, toolContext, apiKeyRecord)\n\n if (config.debug) {\n // Check registered tools on the server\n const registeredTools = (mcpServer as any)._registeredTools || {}\n console.error(`[MCP HTTP] Registered tools in McpServer:`, Object.keys(registeredTools))\n console.error(`[MCP HTTP] Tool handlers initialized:`, (mcpServer as any)._toolHandlersInitialized)\n }\n\n // Connect server to transport\n await mcpServer.connect(transport)\n\n // Handle the request\n if (req.method === 'POST') {\n const body = await parseJsonBody(req)\n await transport.handleRequest(req, res, body)\n } else {\n await transport.handleRequest(req, res)\n }\n\n // Cleanup after response finishes\n res.on('finish', () => {\n transport.close()\n mcpServer.close()\n if (config.debug) {\n console.error(`[MCP HTTP] Request completed, cleaned up`)\n }\n })\n } catch (error) {\n console.error('[MCP HTTP] Error handling request:', error)\n if (!res.headersSent) {\n // Handle payload too large error\n if (error instanceof Error && error.message === 'Request payload too large') {\n res.writeHead(413, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))\n return\n }\n\n res.writeHead(500, { 'Content-Type': 'application/json' })\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,\n },\n id: null,\n })\n )\n }\n }\n })\n\n const toolCount = getToolRegistry().listToolNames().length\n\n console.error(`[MCP HTTP] Starting ${config.name} v${config.version}`)\n console.error(`[MCP HTTP] Endpoint: http://localhost:${port}/mcp`)\n console.error(`[MCP HTTP] Health: http://localhost:${port}/health`)\n console.error(`[MCP HTTP] Tools registered: ${toolCount}`)\n console.error(`[MCP HTTP] Mode: Stateless (new server per request)`)\n console.error(`[MCP HTTP] Server Auth: API key validated against database (x-api-key header)`)\n console.error(`[MCP HTTP] User Auth: Session token (_sessionToken) preferred, falls back to API key roles`)\n\n // Return a Promise that keeps the process alive until shutdown\n return new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n console.error(`[MCP HTTP] Server listening on port ${port}`)\n })\n\n const shutdown = async () => {\n console.error('[MCP HTTP] Shutting down...')\n httpServer.close(() => {\n console.error('[MCP HTTP] Server closed')\n resolve()\n })\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z, type ZodType } from 'zod'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'\nimport { jsonSchemaToZod, toSafeZodSchema } from './schema-utils'\nimport { redactSecretForLog, deriveApiKeySessionId } from './log-redaction'\nimport type { McpServerConfig, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'\nimport { findApiKeyBySecret, findSessionApiKeyWithSecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\n\n/**\n * Options for the HTTP MCP server.\n */\nexport type McpHttpServerOptions = {\n config: McpServerConfig\n container: AwilixContainer\n port: number\n}\n\n/**\n * Resolve user context from session token.\n * Returns null if session token is invalid or expired.\n * Includes the decrypted API key secret for making authenticated API calls.\n */\nasync function resolveSessionContext(\n sessionToken: string,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const em = baseContext.container.resolve<EntityManager>('em')\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n\n // Look up ephemeral key by session token with decrypted secret\n const sessionResult = await findSessionApiKeyWithSecret(em, sessionToken)\n if (!sessionResult) {\n if (debug) {\n console.error(`[MCP HTTP] Session token not found, expired, or secret unavailable: ${redactSecretForLog(sessionToken)}`)\n }\n return null\n }\n\n const { key: sessionKey, secret: sessionSecret } = sessionResult\n\n // Load ACL for the session user\n const userId = sessionKey.sessionUserId || sessionKey.createdBy\n if (!userId) {\n if (debug) {\n console.error(`[MCP HTTP] Session key has no associated user`)\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${sessionKey.id}`, {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n })\n\n if (debug) {\n console.error(`[MCP HTTP] Session context resolved for user ${userId}:`, {\n tenantId: sessionKey.tenantId,\n organizationId: sessionKey.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n hasSessionSecret: !!sessionSecret,\n })\n }\n\n return {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n // Use the decrypted session secret for API calls (not the MCP server key)\n apiKeySecret: sessionSecret,\n }\n } catch (error) {\n if (debug) {\n console.error(`[MCP HTTP] Error resolving session context:`, error)\n }\n return null\n }\n}\n\n/**\n * Resolve user context from the server-level API key (header-based auth fallback).\n * Used when no session token is provided \u2014 loads the API key's ACL for RBAC.\n */\nasync function resolveApiKeyContext(\n apiKeyRecord: ApiKey,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n const userId = apiKeyRecord.sessionUserId ?? apiKeyRecord.createdBy\n if (!userId) {\n if (debug) {\n console.error(`[MCP HTTP] API key has no associated user`)\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${apiKeyRecord.id}`, {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n })\n\n if (debug) {\n console.error(`[MCP HTTP] API key context resolved for user ${userId}:`, {\n tenantId: apiKeyRecord.tenantId,\n organizationId: apiKeyRecord.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n })\n }\n\n return {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n apiKeySecret: baseContext.apiKeySecret,\n }\n } catch (error) {\n if (debug) {\n console.error(`[MCP HTTP] Error resolving API key context:`, error)\n }\n return null\n }\n}\n\n/**\n * Create a stateless MCP server instance for a single request.\n * Tools are registered without pre-filtering - permission checks happen at execution time\n * based on the session token provided in each tool call.\n */\nfunction createMcpServerForRequest(\n config: McpServerConfig,\n toolContext: McpToolContext,\n apiKeyRecord: ApiKey\n): McpServer {\n const server = new McpServer(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n if (config.debug) {\n console.error(`[MCP HTTP] Registering ${tools.length} tools (ACL checked per-call via session token)`)\n }\n\n // Register ALL tools - permission checks happen at execution time via session token\n for (const tool of tools) {\n if (config.debug) {\n console.error(`[MCP HTTP] Registering tool: ${tool.name}`)\n }\n\n // Convert Zod schema to a \"safe\" schema without Date types\n // This uses JSON Schema round-trip to avoid issues with MCP SDK's internal conversion\n // Also inject _sessionToken as an optional parameter so the AI knows to pass it\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n // Convert to JSON Schema first\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n\n // Inject _sessionToken into the JSON schema properties\n const properties = (jsonSchema.properties ?? {}) as Record<string, unknown>\n properties._sessionToken = {\n type: 'string',\n description: 'Session authorization token. If omitted, the server API key roles are used instead.',\n }\n jsonSchema.properties = properties\n\n // Convert back to Zod with passthrough to allow extra properties\n const converted = jsonSchemaToZod(jsonSchema)\n // Use type assertion since we know it's an object schema (we added properties above)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (config.debug) {\n console.error(\n `[MCP HTTP] Skipping tool ${tool.name} - schema conversion failed:`,\n error instanceof Error ? error.message : error\n )\n }\n continue\n }\n } else {\n // If no schema, create one with just _sessionToken\n safeSchema = z.object({\n _sessionToken: z\n .string()\n .optional()\n .describe('Session authorization token (REQUIRED for all tool calls)'),\n })\n }\n\n // Wrap in try/catch to handle any remaining edge cases\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n // Extract session token from args\n const sessionToken = toolArgs._sessionToken as string | undefined\n delete toolArgs._sessionToken // Remove before passing to tool handler\n\n // Always log tool calls for debugging\n console.error(`[MCP HTTP] \u25B6 Tool call: ${tool.name}`, {\n hasSessionToken: !!sessionToken,\n args: JSON.stringify(toolArgs).slice(0, 200),\n })\n\n // Resolve user context from session token\n let effectiveContext = toolContext\n if (sessionToken) {\n const sessionContext = await resolveSessionContext(sessionToken, toolContext, config.debug)\n if (sessionContext) {\n // Session context includes the decrypted API key secret + session ID for memory layer\n effectiveContext = { ...sessionContext, sessionId: sessionToken }\n } else {\n // Session token expired - return user-friendly error for AI to relay\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Your chat session has expired. Please close and reopen the chat window to continue.',\n code: 'SESSION_EXPIRED',\n }),\n },\n ],\n isError: true,\n }\n }\n } else {\n // No session token \u2014 fall back to header API key auth\n const apiKeyContext = await resolveApiKeyContext(apiKeyRecord, toolContext, config.debug)\n if (apiKeyContext) {\n effectiveContext = apiKeyContext\n } else if (!effectiveContext.userId && effectiveContext.userFeatures.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Authentication failed: provide a session token (_sessionToken) or a valid API key with assigned roles',\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n\n // Derive a fallback sessionId from the API key so all tool calls\n // within the same MCP connection share a session memory cache\n if (!effectiveContext.sessionId && effectiveContext.apiKeySecret) {\n effectiveContext = {\n ...effectiveContext,\n sessionId: deriveApiKeySessionId(effectiveContext.apiKeySecret),\n }\n }\n }\n\n // Check if user has required permissions for this tool\n if (tool.requiredFeatures?.length) {\n const rbacService = effectiveContext.container.resolve<RbacService>('rbacService')\n const hasAccess = hasRequiredFeatures(\n tool.requiredFeatures,\n effectiveContext.userFeatures,\n effectiveContext.isSuperAdmin,\n rbacService\n )\n if (!hasAccess) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: `Insufficient permissions for tool \"${tool.name}\". Required: ${tool.requiredFeatures.join(', ')}`,\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n }\n\n try {\n const result = await executeTool(tool.name, toolArgs, effectiveContext)\n\n if (!result.success) {\n console.error(`[MCP HTTP] \u2717 Tool error: ${tool.name}`, { error: result.error, code: result.errorCode })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n console.error(`[MCP HTTP] \u2713 Tool success: ${tool.name}`, {\n resultPreview: JSON.stringify(result.result).slice(0, 200)\n })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n } catch (err) {\n console.error(`[MCP HTTP] \u2717 Tool exception: ${tool.name}`, err)\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error', code: 'EXCEPTION' }),\n },\n ],\n isError: true,\n }\n }\n }\n )\n } catch (error) {\n // Skip tools with schemas that can't be registered\n if (config.debug) {\n console.error(\n `[MCP HTTP] Skipping tool ${tool.name} - registration failed:`,\n error instanceof Error ? error.message : error\n )\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Maximum request body size (1MB).\n * Prevents memory exhaustion from oversized payloads.\n */\nconst MAX_BODY_SIZE = 1 * 1024 * 1024\n\n/**\n * Parse JSON body from request with size limit.\n */\nasync function parseJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let totalSize = 0\n\n req.on('data', (chunk: Buffer) => {\n totalSize += chunk.length\n if (totalSize > MAX_BODY_SIZE) {\n req.destroy()\n reject(new Error('Request payload too large'))\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8')\n resolve(body ? JSON.parse(body) : undefined)\n } catch (error) {\n reject(error)\n }\n })\n req.on('error', reject)\n })\n}\n\n/**\n * Run MCP server with HTTP transport (stateless mode).\n *\n * Each request creates a new MCP server instance and transport.\n * The server authenticates requests using API keys from the x-api-key header.\n */\nexport async function runMcpHttpServer(options: McpHttpServerOptions): Promise<void> {\n const { config, container, port } = options\n\n await loadAllModuleTools()\n\n // Generate and cache entity graph for understand_entity tool\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n console.error(`[MCP HTTP] Entity graph: ${graph.nodes.length} entities, ${graph.edges.length} relationships`)\n } catch (error) {\n console.error('[MCP HTTP] Entity graph generation skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache rich OpenAPI spec for Code Mode search tool (prefers runtime module registry over static JSON)\n try {\n const { loadRichOpenApiSpec } = await import('./api-endpoint-index')\n const spec = await loadRichOpenApiSpec()\n if (spec) {\n console.error('[MCP HTTP] Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n console.error('[MCP HTTP] OpenAPI spec not available')\n }\n } catch (error) {\n console.error('[MCP HTTP] OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Index tools and entity schemas for hybrid search discovery (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n\n // Index MCP tools\n await indexToolsForSearch(searchService)\n\n // Index entity schemas for hybrid search\n try {\n const { getCachedEntityGraph } = await import('./entity-graph')\n const { indexEntitiesForSearch } = await import('./entity-index')\n const graph = getCachedEntityGraph()\n if (graph) {\n const { count } = await indexEntitiesForSearch(searchService, graph)\n if (count > 0) {\n console.error(`[MCP HTTP] Indexed ${count} entity schemas for hybrid search`)\n }\n }\n } catch (entityError) {\n console.error('[MCP HTTP] Entity schema indexing skipped:', entityError instanceof Error ? entityError.message : entityError)\n }\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n console.error('[MCP HTTP] Search indexing skipped (search service not available):', error)\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '/', `http://localhost:${port}`)\n\n // Health check endpoint\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({\n status: 'ok',\n tools: getToolRegistry().listToolNames().length,\n timestamp: new Date().toISOString(),\n }))\n return\n }\n\n if (url.pathname !== '/mcp') {\n res.writeHead(404, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Not found' }))\n return\n }\n\n console.error(`[MCP HTTP] \u2190 Request: ${req.method} ${url.pathname}`)\n\n // Extract headers\n const headers: Record<string, string | undefined> = {}\n for (const [key, value] of Object.entries(req.headers)) {\n headers[key] = Array.isArray(value) ? value[0] : value\n }\n\n // Server-level authentication via database lookup\n const providedApiKey = extractApiKeyFromHeaders(headers)\n if (!providedApiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))\n return\n }\n\n // Validate API key against database (prefix lookup + bcrypt verify + expiry check)\n const em = container.resolve<EntityManager>('em')\n const apiKeyRecord = await findApiKeyBySecret(em, providedApiKey)\n if (!apiKeyRecord) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid or expired API key' }))\n return\n }\n\n if (config.debug) {\n console.error(`[MCP HTTP] Server-level auth passed (${req.method}) - API key: ${apiKeyRecord.keyPrefix}...`)\n }\n\n // Create base tool context using API key's tenant/org scope\n // Session tokens can override with user-specific permissions\n const toolContext: McpToolContext = {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId: apiKeyRecord.createdBy ?? null,\n container,\n userFeatures: [],\n isSuperAdmin: false,\n apiKeySecret: providedApiKey,\n }\n\n try {\n // Create stateless transport (no session ID generator = stateless)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create new server for this request\n const mcpServer = createMcpServerForRequest(config, toolContext, apiKeyRecord)\n\n if (config.debug) {\n // Check registered tools on the server\n const registeredTools = (mcpServer as any)._registeredTools || {}\n console.error(`[MCP HTTP] Registered tools in McpServer:`, Object.keys(registeredTools))\n console.error(`[MCP HTTP] Tool handlers initialized:`, (mcpServer as any)._toolHandlersInitialized)\n }\n\n // Connect server to transport\n await mcpServer.connect(transport)\n\n // Handle the request\n if (req.method === 'POST') {\n const body = await parseJsonBody(req)\n await transport.handleRequest(req, res, body)\n } else {\n await transport.handleRequest(req, res)\n }\n\n // Cleanup after response finishes\n res.on('finish', () => {\n transport.close()\n mcpServer.close()\n if (config.debug) {\n console.error(`[MCP HTTP] Request completed, cleaned up`)\n }\n })\n } catch (error) {\n console.error('[MCP HTTP] Error handling request:', error)\n if (!res.headersSent) {\n // Handle payload too large error\n if (error instanceof Error && error.message === 'Request payload too large') {\n res.writeHead(413, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))\n return\n }\n\n res.writeHead(500, { 'Content-Type': 'application/json' })\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,\n },\n id: null,\n })\n )\n }\n }\n })\n\n const toolCount = getToolRegistry().listToolNames().length\n\n console.error(`[MCP HTTP] Starting ${config.name} v${config.version}`)\n console.error(`[MCP HTTP] Endpoint: http://localhost:${port}/mcp`)\n console.error(`[MCP HTTP] Health: http://localhost:${port}/health`)\n console.error(`[MCP HTTP] Tools registered: ${toolCount}`)\n console.error(`[MCP HTTP] Mode: Stateless (new server per request)`)\n console.error(`[MCP HTTP] Server Auth: API key validated against database (x-api-key header)`)\n console.error(`[MCP HTTP] User Auth: Session token (_sessionToken) preferred, falls back to API key roles`)\n\n // Return a Promise that keeps the process alive until shutdown\n return new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n console.error(`[MCP HTTP] Server listening on port ${port}`)\n })\n\n const shutdown = async () => {\n console.error('[MCP HTTP] Shutting down...')\n httpServer.close(() => {\n console.error('[MCP HTTP] Server closed')\n resolve()\n })\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,16 @@
1
+ import { createHash } from "node:crypto";
2
+ function redactSecretForLog(value) {
3
+ if (typeof value !== "string" || value.length === 0) return "<redacted>";
4
+ const prefixLength = Math.min(12, Math.floor(value.length / 2));
5
+ if (prefixLength <= 0) return "<redacted>";
6
+ return `${value.slice(0, prefixLength)}...`;
7
+ }
8
+ function deriveApiKeySessionId(apiKeySecret) {
9
+ const digest = createHash("sha256").update(apiKeySecret).digest("hex");
10
+ return `apikey_${digest.slice(0, 16)}`;
11
+ }
12
+ export {
13
+ deriveApiKeySessionId,
14
+ redactSecretForLog
15
+ };
16
+ //# sourceMappingURL=log-redaction.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/log-redaction.ts"],
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\n\n/**\n * Redact a bearer-style secret (session token, API key) for safe logging.\n * Reveals at most a short leading fingerprint and never more than half of the\n * value, so durable logs never carry a replayable credential.\n */\nexport function redactSecretForLog(value: unknown): string {\n if (typeof value !== 'string' || value.length === 0) return '<redacted>'\n const prefixLength = Math.min(12, Math.floor(value.length / 2))\n if (prefixLength <= 0) return '<redacted>'\n return `${value.slice(0, prefixLength)}...`\n}\n\n/**\n * Derive a stable, non-reversible session-memory id from an API key secret.\n * The same secret always maps to the same id (so tool calls on one MCP\n * connection share a memory cache), but no secret material is exposed because\n * the id is a truncated SHA-256 digest rather than a slice of the secret.\n */\nexport function deriveApiKeySessionId(apiKeySecret: string): string {\n const digest = createHash('sha256').update(apiKeySecret).digest('hex')\n return `apikey_${digest.slice(0, 16)}`\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAOpB,SAAS,mBAAmB,OAAwB;AACzD,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,QAAM,eAAe,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM,SAAS,CAAC,CAAC;AAC9D,MAAI,gBAAgB,EAAG,QAAO;AAC9B,SAAO,GAAG,MAAM,MAAM,GAAG,YAAY,CAAC;AACxC;AAQO,SAAS,sBAAsB,cAA8B;AAClE,QAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,YAAY,EAAE,OAAO,KAAK;AACrE,SAAO,UAAU,OAAO,MAAM,GAAG,EAAE,CAAC;AACtC;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.5-develop.5099.1.85c0aff78c",
3
+ "version": "0.6.5-develop.5116.1.f0af9e5080",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -98,16 +98,16 @@
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.5-develop.5099.1.85c0aff78c",
102
- "@open-mercato/ui": "0.6.5-develop.5099.1.85c0aff78c",
101
+ "@open-mercato/shared": "0.6.5-develop.5116.1.f0af9e5080",
102
+ "@open-mercato/ui": "0.6.5-develop.5116.1.f0af9e5080",
103
103
  "react": "^19.0.0",
104
104
  "react-dom": "^19.0.0",
105
105
  "zod": ">=3.23.0"
106
106
  },
107
107
  "devDependencies": {
108
- "@open-mercato/cli": "0.6.5-develop.5099.1.85c0aff78c",
109
- "@open-mercato/shared": "0.6.5-develop.5099.1.85c0aff78c",
110
- "@open-mercato/ui": "0.6.5-develop.5099.1.85c0aff78c",
108
+ "@open-mercato/cli": "0.6.5-develop.5116.1.f0af9e5080",
109
+ "@open-mercato/shared": "0.6.5-develop.5116.1.f0af9e5080",
110
+ "@open-mercato/ui": "0.6.5-develop.5116.1.f0af9e5080",
111
111
  "@types/react": "^19.2.16",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.7",
@@ -0,0 +1,56 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { redactSecretForLog, deriveApiKeySessionId } from '../log-redaction'
3
+
4
+ describe('redactSecretForLog', () => {
5
+ it('never emits the full session token', () => {
6
+ const token = `sess_${'a'.repeat(32)}`
7
+ const redacted = redactSecretForLog(token)
8
+ expect(redacted).not.toBe(token)
9
+ expect(redacted).not.toContain(token)
10
+ expect(token.startsWith(redacted.replace(/\.\.\.$/, ''))).toBe(true)
11
+ })
12
+
13
+ it('reveals at most a short leading fingerprint and never more than half', () => {
14
+ const token = `sess_${'b'.repeat(32)}`
15
+ const redacted = redactSecretForLog(token)
16
+ const visible = redacted.replace(/\.\.\.$/, '')
17
+ expect(redacted.endsWith('...')).toBe(true)
18
+ expect(visible.length).toBeLessThanOrEqual(12)
19
+ expect(visible.length).toBeLessThanOrEqual(Math.floor(token.length / 2))
20
+ expect(token.startsWith(visible)).toBe(true)
21
+ })
22
+
23
+ it('does not leak short values', () => {
24
+ expect(redactSecretForLog('abcd')).toBe('ab...')
25
+ })
26
+
27
+ it('returns a placeholder for empty or non-string input', () => {
28
+ expect(redactSecretForLog('')).toBe('<redacted>')
29
+ expect(redactSecretForLog(undefined)).toBe('<redacted>')
30
+ expect(redactSecretForLog(null)).toBe('<redacted>')
31
+ expect(redactSecretForLog(12345)).toBe('<redacted>')
32
+ })
33
+ })
34
+
35
+ describe('deriveApiKeySessionId', () => {
36
+ it('does not embed any slice of the secret', () => {
37
+ const secret = 'omk_publicprefix_secretbodythatmustnotleak'
38
+ const sessionId = deriveApiKeySessionId(secret)
39
+ expect(sessionId.startsWith('apikey_')).toBe(true)
40
+ expect(sessionId).not.toContain(secret.slice(0, 16))
41
+ expect(sessionId).not.toContain('secretbody')
42
+ const digestPart = sessionId.slice('apikey_'.length)
43
+ expect(secret).not.toContain(digestPart)
44
+ })
45
+
46
+ it('is a stable truncated sha-256 digest of the secret', () => {
47
+ const secret = 'omk_test_secret_value'
48
+ const expected = `apikey_${createHash('sha256').update(secret).digest('hex').slice(0, 16)}`
49
+ expect(deriveApiKeySessionId(secret)).toBe(expected)
50
+ expect(deriveApiKeySessionId(secret)).toBe(deriveApiKeySessionId(secret))
51
+ })
52
+
53
+ it('maps distinct secrets to distinct ids', () => {
54
+ expect(deriveApiKeySessionId('secret-one')).not.toBe(deriveApiKeySessionId('secret-two'))
55
+ })
56
+ })
@@ -9,6 +9,7 @@ import { executeTool } from './tool-executor'
9
9
  import { loadAllModuleTools, indexToolsForSearch } from './tool-loader'
10
10
  import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'
11
11
  import { jsonSchemaToZod, toSafeZodSchema } from './schema-utils'
12
+ import { redactSecretForLog, deriveApiKeySessionId } from './log-redaction'
12
13
  import type { McpServerConfig, McpToolContext } from './types'
13
14
  import type { SearchService } from '@open-mercato/search/service'
14
15
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
@@ -42,7 +43,7 @@ async function resolveSessionContext(
42
43
  const sessionResult = await findSessionApiKeyWithSecret(em, sessionToken)
43
44
  if (!sessionResult) {
44
45
  if (debug) {
45
- console.error(`[MCP HTTP] Session token not found, expired, or secret unavailable: ${sessionToken}`)
46
+ console.error(`[MCP HTTP] Session token not found, expired, or secret unavailable: ${redactSecretForLog(sessionToken)}`)
46
47
  }
47
48
  return null
48
49
  }
@@ -277,7 +278,7 @@ function createMcpServerForRequest(
277
278
  if (!effectiveContext.sessionId && effectiveContext.apiKeySecret) {
278
279
  effectiveContext = {
279
280
  ...effectiveContext,
280
- sessionId: 'apikey_' + effectiveContext.apiKeySecret.slice(0, 16),
281
+ sessionId: deriveApiKeySessionId(effectiveContext.apiKeySecret),
281
282
  }
282
283
  }
283
284
  }
@@ -0,0 +1,24 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ /**
4
+ * Redact a bearer-style secret (session token, API key) for safe logging.
5
+ * Reveals at most a short leading fingerprint and never more than half of the
6
+ * value, so durable logs never carry a replayable credential.
7
+ */
8
+ export function redactSecretForLog(value: unknown): string {
9
+ if (typeof value !== 'string' || value.length === 0) return '<redacted>'
10
+ const prefixLength = Math.min(12, Math.floor(value.length / 2))
11
+ if (prefixLength <= 0) return '<redacted>'
12
+ return `${value.slice(0, prefixLength)}...`
13
+ }
14
+
15
+ /**
16
+ * Derive a stable, non-reversible session-memory id from an API key secret.
17
+ * The same secret always maps to the same id (so tool calls on one MCP
18
+ * connection share a memory cache), but no secret material is exposed because
19
+ * the id is a truncated SHA-256 digest rather than a slice of the secret.
20
+ */
21
+ export function deriveApiKeySessionId(apiKeySecret: string): string {
22
+ const digest = createHash('sha256').update(apiKeySecret).digest('hex')
23
+ return `apikey_${digest.slice(0, 16)}`
24
+ }