@open-mercato/ai-assistant 0.6.6-develop.6465.1.019f0fb26f → 0.6.6-develop.6471.1.9ab5bdd79a

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 (199) hide show
  1. package/dist/frontend/hooks/useCommandPalette.js +16 -14
  2. package/dist/frontend/hooks/useCommandPalette.js.map +2 -2
  3. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +3 -1
  4. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +2 -2
  5. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +3 -1
  6. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +2 -2
  7. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +3 -1
  8. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +2 -2
  9. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +5 -3
  10. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +2 -2
  11. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +4 -2
  12. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
  13. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +5 -3
  14. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +2 -2
  15. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +4 -2
  16. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +2 -2
  17. package/dist/modules/ai_assistant/api/ai/agents/route.js +3 -1
  18. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
  19. package/dist/modules/ai_assistant/api/ai/chat/route.js +6 -7
  20. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  21. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +5 -3
  22. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +2 -2
  23. package/dist/modules/ai_assistant/api/ai/conversations/import/route.js +3 -1
  24. package/dist/modules/ai_assistant/api/ai/conversations/import/route.js.map +2 -2
  25. package/dist/modules/ai_assistant/api/ai/conversations/route.js +4 -2
  26. package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +2 -2
  27. package/dist/modules/ai_assistant/api/ai/run-object/route.js +3 -1
  28. package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +2 -2
  29. package/dist/modules/ai_assistant/api/chat/route.js +23 -14
  30. package/dist/modules/ai_assistant/api/chat/route.js.map +2 -2
  31. package/dist/modules/ai_assistant/api/health/route.js +3 -1
  32. package/dist/modules/ai_assistant/api/health/route.js.map +2 -2
  33. package/dist/modules/ai_assistant/api/mcp-key/route.js +3 -1
  34. package/dist/modules/ai_assistant/api/mcp-key/route.js.map +2 -2
  35. package/dist/modules/ai_assistant/api/route/route.js +11 -9
  36. package/dist/modules/ai_assistant/api/route/route.js.map +2 -2
  37. package/dist/modules/ai_assistant/api/session-key/route.js +3 -1
  38. package/dist/modules/ai_assistant/api/session-key/route.js.map +2 -2
  39. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +4 -2
  40. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
  41. package/dist/modules/ai_assistant/api/settings/route.js +6 -4
  42. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  43. package/dist/modules/ai_assistant/api/tools/execute/route.js +3 -1
  44. package/dist/modules/ai_assistant/api/tools/execute/route.js.map +2 -2
  45. package/dist/modules/ai_assistant/api/tools/route.js +3 -1
  46. package/dist/modules/ai_assistant/api/tools/route.js.map +2 -2
  47. package/dist/modules/ai_assistant/api/usage/daily/route.js +3 -1
  48. package/dist/modules/ai_assistant/api/usage/daily/route.js.map +2 -2
  49. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +3 -1
  50. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +2 -2
  51. package/dist/modules/ai_assistant/api/usage/sessions/route.js +3 -1
  52. package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +2 -2
  53. package/dist/modules/ai_assistant/cli.js +3 -0
  54. package/dist/modules/ai_assistant/cli.js.map +2 -2
  55. package/dist/modules/ai_assistant/lib/agent-policy.js +7 -5
  56. package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
  57. package/dist/modules/ai_assistant/lib/agent-registry.js +11 -13
  58. package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
  59. package/dist/modules/ai_assistant/lib/agent-runtime.js +21 -44
  60. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  61. package/dist/modules/ai_assistant/lib/agent-tools.js +5 -10
  62. package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
  63. package/dist/modules/ai_assistant/lib/ai-overrides.js +5 -9
  64. package/dist/modules/ai_assistant/lib/ai-overrides.js.map +2 -2
  65. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +22 -22
  66. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
  67. package/dist/modules/ai_assistant/lib/attachment-parts.js +10 -26
  68. package/dist/modules/ai_assistant/lib/attachment-parts.js.map +2 -2
  69. package/dist/modules/ai_assistant/lib/auth.js +3 -1
  70. package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
  71. package/dist/modules/ai_assistant/lib/codemode-tools.js +15 -17
  72. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
  73. package/dist/modules/ai_assistant/lib/entity-index.js +7 -5
  74. package/dist/modules/ai_assistant/lib/entity-index.js.map +2 -2
  75. package/dist/modules/ai_assistant/lib/generated-registry-loader.js +3 -4
  76. package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
  77. package/dist/modules/ai_assistant/lib/http-server.js +45 -48
  78. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  79. package/dist/modules/ai_assistant/lib/llm-bootstrap.js +5 -12
  80. package/dist/modules/ai_assistant/lib/llm-bootstrap.js.map +2 -2
  81. package/dist/modules/ai_assistant/lib/mcp-client.js +3 -1
  82. package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
  83. package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js +3 -1
  84. package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js.map +2 -2
  85. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +3 -1
  86. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  87. package/dist/modules/ai_assistant/lib/mcp-server.js +24 -19
  88. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  89. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +3 -1
  90. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +2 -2
  91. package/dist/modules/ai_assistant/lib/model-factory.js +4 -2
  92. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  93. package/dist/modules/ai_assistant/lib/opencode-client.js +5 -3
  94. package/dist/modules/ai_assistant/lib/opencode-client.js.map +2 -2
  95. package/dist/modules/ai_assistant/lib/opencode-handlers.js +30 -12
  96. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +2 -2
  97. package/dist/modules/ai_assistant/lib/pending-action-cancel.js +3 -1
  98. package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +2 -2
  99. package/dist/modules/ai_assistant/lib/pending-action-executor.js +3 -1
  100. package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +2 -2
  101. package/dist/modules/ai_assistant/lib/prepare-mutation.js +4 -6
  102. package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +2 -2
  103. package/dist/modules/ai_assistant/lib/schema-utils.js +3 -1
  104. package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
  105. package/dist/modules/ai_assistant/lib/token-usage-recorder.js +4 -8
  106. package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +2 -2
  107. package/dist/modules/ai_assistant/lib/tool-executor.js +3 -1
  108. package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
  109. package/dist/modules/ai_assistant/lib/tool-loader.js +18 -25
  110. package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
  111. package/dist/modules/ai_assistant/lib/tool-registry.js +3 -1
  112. package/dist/modules/ai_assistant/lib/tool-registry.js.map +2 -2
  113. package/dist/modules/ai_assistant/lib/tool-test-runner.js +3 -1
  114. package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +2 -2
  115. package/dist/modules/ai_assistant/setup.js +4 -8
  116. package/dist/modules/ai_assistant/setup.js.map +2 -2
  117. package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +3 -1
  118. package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +2 -2
  119. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +7 -19
  120. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +2 -2
  121. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +5 -11
  122. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +2 -2
  123. package/package.json +6 -6
  124. package/src/frontend/hooks/useCommandPalette.ts +17 -16
  125. package/src/modules/ai_assistant/__tests__/events.test.ts +23 -5
  126. package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +21 -3
  127. package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +22 -3
  128. package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +26 -5
  129. package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +4 -1
  130. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +4 -1
  131. package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +4 -1
  132. package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +6 -3
  133. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +5 -2
  134. package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +6 -3
  135. package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +5 -2
  136. package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -1
  137. package/src/modules/ai_assistant/api/ai/chat/route.ts +7 -7
  138. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +6 -3
  139. package/src/modules/ai_assistant/api/ai/conversations/import/route.ts +4 -1
  140. package/src/modules/ai_assistant/api/ai/conversations/route.ts +5 -2
  141. package/src/modules/ai_assistant/api/ai/run-object/route.ts +4 -1
  142. package/src/modules/ai_assistant/api/chat/route.ts +24 -15
  143. package/src/modules/ai_assistant/api/health/route.ts +4 -1
  144. package/src/modules/ai_assistant/api/mcp-key/route.ts +4 -1
  145. package/src/modules/ai_assistant/api/route/route.ts +12 -9
  146. package/src/modules/ai_assistant/api/session-key/route.ts +4 -1
  147. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +5 -2
  148. package/src/modules/ai_assistant/api/settings/route.ts +7 -4
  149. package/src/modules/ai_assistant/api/tools/execute/route.ts +4 -1
  150. package/src/modules/ai_assistant/api/tools/route.ts +4 -1
  151. package/src/modules/ai_assistant/api/usage/daily/route.ts +4 -1
  152. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +4 -1
  153. package/src/modules/ai_assistant/api/usage/sessions/route.ts +4 -1
  154. package/src/modules/ai_assistant/cli.ts +5 -0
  155. package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +20 -2
  156. package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +25 -5
  157. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +21 -2
  158. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +23 -3
  159. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +20 -2
  160. package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +22 -3
  161. package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +23 -3
  162. package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +25 -7
  163. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +25 -5
  164. package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +19 -1
  165. package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +22 -3
  166. package/src/modules/ai_assistant/lib/agent-policy.ts +8 -5
  167. package/src/modules/ai_assistant/lib/agent-registry.ts +12 -15
  168. package/src/modules/ai_assistant/lib/agent-runtime.ts +23 -46
  169. package/src/modules/ai_assistant/lib/agent-tools.ts +7 -11
  170. package/src/modules/ai_assistant/lib/ai-overrides.ts +6 -9
  171. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +23 -22
  172. package/src/modules/ai_assistant/lib/attachment-parts.ts +14 -29
  173. package/src/modules/ai_assistant/lib/auth.ts +4 -1
  174. package/src/modules/ai_assistant/lib/codemode-tools.ts +17 -17
  175. package/src/modules/ai_assistant/lib/entity-index.ts +8 -5
  176. package/src/modules/ai_assistant/lib/generated-registry-loader.ts +4 -4
  177. package/src/modules/ai_assistant/lib/http-server.ts +46 -48
  178. package/src/modules/ai_assistant/lib/llm-bootstrap.ts +7 -13
  179. package/src/modules/ai_assistant/lib/mcp-client.ts +4 -1
  180. package/src/modules/ai_assistant/lib/mcp-dev-key-resolution.ts +4 -1
  181. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +4 -1
  182. package/src/modules/ai_assistant/lib/mcp-server.ts +27 -19
  183. package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +4 -1
  184. package/src/modules/ai_assistant/lib/model-factory.ts +6 -3
  185. package/src/modules/ai_assistant/lib/opencode-client.ts +6 -3
  186. package/src/modules/ai_assistant/lib/opencode-handlers.ts +32 -12
  187. package/src/modules/ai_assistant/lib/pending-action-cancel.ts +4 -1
  188. package/src/modules/ai_assistant/lib/pending-action-executor.ts +4 -1
  189. package/src/modules/ai_assistant/lib/prepare-mutation.ts +5 -6
  190. package/src/modules/ai_assistant/lib/schema-utils.ts +4 -1
  191. package/src/modules/ai_assistant/lib/token-usage-recorder.ts +5 -8
  192. package/src/modules/ai_assistant/lib/tool-executor.ts +4 -1
  193. package/src/modules/ai_assistant/lib/tool-loader.ts +19 -25
  194. package/src/modules/ai_assistant/lib/tool-registry.ts +4 -1
  195. package/src/modules/ai_assistant/lib/tool-test-runner.ts +4 -1
  196. package/src/modules/ai_assistant/setup.ts +5 -8
  197. package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +4 -1
  198. package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +8 -19
  199. package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +6 -11
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/ai_assistant/api/ai/conversations/import/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { aiChatConversationImportSchema } from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n} from '../../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Lazily import a localStorage AI chat conversation',\n methods: {\n POST: {\n operationId: 'aiAssistantImportConversation',\n summary: 'Import a conversation that previously lived only in browser localStorage.',\n description:\n 'Idempotent: messages with `clientMessageId` already present in the server transcript are ' +\n 'skipped and counted in `skippedMessageCount`. New messages are appended with the original ' +\n '`clientMessageId` so subsequent retries continue to dedupe. Up to 100 messages per request. ' +\n 'Attachment previews stored as `data:` URLs in the source localStorage record MUST NOT be ' +\n 'forwarded to this endpoint; the UI strips them before upload.',\n responses: [\n {\n status: 200,\n description: 'Import result including imported/skipped counters.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return jsonError(401, 'Unauthorized', 'unauthenticated')\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n const parseResult = aiChatConversationImportSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid import payload.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (!auth.tenantId) {\n return jsonError(400, 'Caller is not bound to a tenant.', 'tenant_required')\n }\n\n const repo = createConversationStorage(container)\n const result = await repo.importLocalConversation(\n {\n conversation: {\n conversationId: parseResult.data.conversation.conversationId,\n agentId: parseResult.data.conversation.agentId,\n title: parseResult.data.conversation.title ?? null,\n status: parseResult.data.conversation.status,\n pageContext: parseResult.data.conversation.pageContext ?? null,\n },\n messages: parseResult.data.messages,\n },\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n },\n )\n return NextResponse.json({\n conversation: serializeAiChatConversation(result.conversation),\n importedMessageCount: result.importedMessageCount,\n skippedMessageCount: result.skippedMessageCount,\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, error.message, 'conversation_not_found')\n }\n console.error('[AI Conversation Import] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to import conversation.',\n 'internal_error',\n )\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { aiChatConversationImportSchema } from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n} from '../../../../lib/conversation-storage'\n\nconst logger = createLogger('ai_assistant')\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Lazily import a localStorage AI chat conversation',\n methods: {\n POST: {\n operationId: 'aiAssistantImportConversation',\n summary: 'Import a conversation that previously lived only in browser localStorage.',\n description:\n 'Idempotent: messages with `clientMessageId` already present in the server transcript are ' +\n 'skipped and counted in `skippedMessageCount`. New messages are appended with the original ' +\n '`clientMessageId` so subsequent retries continue to dedupe. Up to 100 messages per request. ' +\n 'Attachment previews stored as `data:` URLs in the source localStorage record MUST NOT be ' +\n 'forwarded to this endpoint; the UI strips them before upload.',\n responses: [\n {\n status: 200,\n description: 'Import result including imported/skipped counters.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return jsonError(401, 'Unauthorized', 'unauthenticated')\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n const parseResult = aiChatConversationImportSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid import payload.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (!auth.tenantId) {\n return jsonError(400, 'Caller is not bound to a tenant.', 'tenant_required')\n }\n\n const repo = createConversationStorage(container)\n const result = await repo.importLocalConversation(\n {\n conversation: {\n conversationId: parseResult.data.conversation.conversationId,\n agentId: parseResult.data.conversation.agentId,\n title: parseResult.data.conversation.title ?? null,\n status: parseResult.data.conversation.status,\n pageContext: parseResult.data.conversation.pageContext ?? null,\n },\n messages: parseResult.data.messages,\n },\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n },\n )\n return NextResponse.json({\n conversation: serializeAiChatConversation(result.conversation),\n importedMessageCount: result.importedMessageCount,\n skippedMessageCount: result.skippedMessageCount,\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, error.message, 'conversation_not_found')\n }\n logger.error('AI Conversation Import \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to import conversation.',\n 'internal_error',\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
1
2
  import { NextResponse } from "next/server";
2
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
@@ -10,6 +11,7 @@ import {
10
11
  createConversationStorage,
11
12
  serializeAiChatConversation
12
13
  } from "../../../lib/conversation-storage.js";
14
+ const logger = createLogger("ai_assistant");
13
15
  const REQUIRED_FEATURE = "ai_assistant.view";
14
16
  const MANAGE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.manage";
15
17
  const openApi = {
@@ -136,7 +138,7 @@ async function GET(req) {
136
138
  nextCursor: result.nextCursor
137
139
  });
138
140
  } catch (error) {
139
- console.error("[AI Conversations GET] Failure:", error);
141
+ logger.error("AI Conversations GET \u2014 Failure", { err: error });
140
142
  return jsonError(
141
143
  500,
142
144
  error instanceof Error ? error.message : "Failed to list conversations.",
@@ -193,7 +195,7 @@ async function POST(req) {
193
195
  if (error instanceof Error && error.name === "AiChatConversationAccessError") {
194
196
  return jsonError(404, error.message, "conversation_not_found");
195
197
  }
196
- console.error("[AI Conversations POST] Failure:", error);
198
+ logger.error("AI Conversations POST \u2014 Failure", { err: error });
197
199
  return jsonError(
198
200
  500,
199
201
  error instanceof Error ? error.message : "Failed to create conversation.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ai_assistant/api/ai/conversations/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n aiChatConversationCreateSchema,\n aiChatConversationListQuerySchema,\n} from '../../../data/validators'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n} from '../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Server-side AI chat conversations',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversations',\n summary: 'List AI chat conversations visible to the caller.',\n description:\n 'Returns `{ items, nextCursor }` for the authenticated caller, ordered by `lastMessageAt` ' +\n 'descending. View-only callers receive only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` may list conversations across users in the same ' +\n 'tenant/organization. The ' +\n '`agent` and `status` filters are optional; `cursor` is the ISO timestamp returned by a ' +\n 'previous response.',\n responses: [\n {\n status: 200,\n description: 'Caller-owned conversation summaries.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantCreateConversation',\n summary: 'Idempotently create a new AI chat conversation.',\n description:\n 'If a non-deleted conversation already exists with the supplied `conversationId` for the ' +\n 'authenticated caller in this tenant/org, returns the existing summary. Otherwise creates a ' +\n 'fresh row and writes the owner-participant row in the same transaction.',\n responses: [\n {\n status: 200,\n description: 'Existing conversation (idempotent path).',\n mediaType: 'application/json',\n },\n {\n status: 201,\n description: 'Newly created conversation.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function loadCallerContext(req: NextRequest): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) {\n return { kind: 'missing-tenant' }\n }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return NextResponse.json({ items: [], nextCursor: null })\n }\n\n const url = new URL(req.url)\n const parseResult = aiChatConversationListQuerySchema.safeParse({\n agent: url.searchParams.get('agent') ?? undefined,\n status: url.searchParams.get('status') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n cursor: url.searchParams.get('cursor') ?? undefined,\n })\n if (!parseResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const result = await repo.list(\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n agentId: parseResult.data.agent ?? null,\n status: parseResult.data.status ?? null,\n limit: parseResult.data.limit,\n cursor: parseResult.data.cursor ?? null,\n },\n )\n return NextResponse.json({\n items: result.items.map((row) => serializeAiChatConversation(row)),\n nextCursor: result.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversations GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to list conversations.',\n 'internal_error',\n )\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(400, 'Caller is not bound to a tenant.', 'tenant_required')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const parseResult = aiChatConversationCreateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation payload.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const ctx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: false,\n }\n const beforeRow = parseResult.data.conversationId\n ? await repo.getById(parseResult.data.conversationId, ctx)\n : null\n const row = await repo.createOrGet(\n {\n conversationId: parseResult.data.conversationId,\n agentId: parseResult.data.agentId,\n title: parseResult.data.title ?? null,\n pageContext: parseResult.data.pageContext ?? null,\n },\n ctx,\n )\n const status = beforeRow ? 200 : 201\n return NextResponse.json(serializeAiChatConversation(row), { status })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationOrgNotFoundError') {\n return jsonError(400, error.message, 'organization_not_found')\n }\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, error.message, 'conversation_not_found')\n }\n console.error('[AI Conversations POST] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to create conversation.',\n 'internal_error',\n )\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n aiChatConversationCreateSchema,\n aiChatConversationListQuerySchema,\n} from '../../../data/validators'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n} from '../../../lib/conversation-storage'\n\nconst logger = createLogger('ai_assistant')\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Server-side AI chat conversations',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversations',\n summary: 'List AI chat conversations visible to the caller.',\n description:\n 'Returns `{ items, nextCursor }` for the authenticated caller, ordered by `lastMessageAt` ' +\n 'descending. View-only callers receive only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` may list conversations across users in the same ' +\n 'tenant/organization. The ' +\n '`agent` and `status` filters are optional; `cursor` is the ISO timestamp returned by a ' +\n 'previous response.',\n responses: [\n {\n status: 200,\n description: 'Caller-owned conversation summaries.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantCreateConversation',\n summary: 'Idempotently create a new AI chat conversation.',\n description:\n 'If a non-deleted conversation already exists with the supplied `conversationId` for the ' +\n 'authenticated caller in this tenant/org, returns the existing summary. Otherwise creates a ' +\n 'fresh row and writes the owner-participant row in the same transaction.',\n responses: [\n {\n status: 200,\n description: 'Existing conversation (idempotent path).',\n mediaType: 'application/json',\n },\n {\n status: 201,\n description: 'Newly created conversation.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function loadCallerContext(req: NextRequest): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) {\n return { kind: 'missing-tenant' }\n }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return NextResponse.json({ items: [], nextCursor: null })\n }\n\n const url = new URL(req.url)\n const parseResult = aiChatConversationListQuerySchema.safeParse({\n agent: url.searchParams.get('agent') ?? undefined,\n status: url.searchParams.get('status') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n cursor: url.searchParams.get('cursor') ?? undefined,\n })\n if (!parseResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const result = await repo.list(\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n agentId: parseResult.data.agent ?? null,\n status: parseResult.data.status ?? null,\n limit: parseResult.data.limit,\n cursor: parseResult.data.cursor ?? null,\n },\n )\n return NextResponse.json({\n items: result.items.map((row) => serializeAiChatConversation(row)),\n nextCursor: result.nextCursor,\n })\n } catch (error) {\n logger.error('AI Conversations GET \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to list conversations.',\n 'internal_error',\n )\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(400, 'Caller is not bound to a tenant.', 'tenant_required')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const parseResult = aiChatConversationCreateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation payload.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const ctx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: false,\n }\n const beforeRow = parseResult.data.conversationId\n ? await repo.getById(parseResult.data.conversationId, ctx)\n : null\n const row = await repo.createOrGet(\n {\n conversationId: parseResult.data.conversationId,\n agentId: parseResult.data.agentId,\n title: parseResult.data.title ?? null,\n pageContext: parseResult.data.pageContext ?? null,\n },\n ctx,\n )\n const status = beforeRow ? 200 : 201\n return NextResponse.json(serializeAiChatConversation(row), { status })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationOrgNotFoundError') {\n return jsonError(400, error.message, 'organization_not_found')\n }\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, error.message, 'conversation_not_found')\n }\n logger.error('AI Conversations POST \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to create conversation.',\n 'internal_error',\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
1
2
  import { NextResponse } from "next/server";
2
3
  import { z } from "zod";
3
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
@@ -6,6 +7,7 @@ import { loadAgentRegistry } from "../../../lib/agent-registry.js";
6
7
  import { checkAgentPolicy } from "../../../lib/agent-policy.js";
7
8
  import { runAiAgentObject } from "../../../lib/agent-runtime.js";
8
9
  import { AgentPolicyError } from "../../../lib/agent-tools.js";
10
+ const logger = createLogger("ai_assistant");
9
11
  const MAX_MESSAGES = 100;
10
12
  const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/;
11
13
  const agentIdSchema = z.string().regex(agentIdPattern, 'agent must match "<module>.<agent>" (lowercase, digits, underscores only)');
@@ -151,7 +153,7 @@ async function POST(req) {
151
153
  if (error instanceof AgentPolicyError) {
152
154
  return jsonError(statusForDenyCode(error.code), error.message, error.code);
153
155
  }
154
- console.error("[AI Run Object] Dispatch failure:", error);
156
+ logger.error("AI Run Object \u2014 Dispatch failure", { err: error });
155
157
  return jsonError(
156
158
  500,
157
159
  error instanceof Error ? error.message : "Agent object dispatch failed.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ai_assistant/api/ai/run-object/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport { runAiAgentObject } from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst agentIdSchema = z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)')\n\nconst chatMessageSchema = z.object({\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().optional(),\n entityType: z.string().optional(),\n recordId: z.string().optional(),\n })\n .passthrough()\n\nconst runObjectRequestSchema = z.object({\n agent: agentIdSchema,\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n pageContext: pageContextSchema.optional(),\n modelOverride: z.string().optional(),\n})\n\nexport type AiRunObjectRequest = z.infer<typeof runObjectRequestSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Run an AI agent in structured-output (object) mode',\n methods: {\n POST: {\n operationId: 'aiAssistantRunObject',\n summary: 'Run an object-mode AI agent and return the generated object',\n description:\n 'Invokes `runAiAgentObject` server-side for the registered AI agent identified by `agent` ' +\n '(matching \"<module>.<agent>\"). Enforces the same `requiredFeatures`, tool whitelisting, ' +\n 'mutationPolicy, and attachment media-type policy as the chat dispatcher, but additionally ' +\n 'requires the agent to declare `executionMode: \"object\"`. Returns the generated object in ' +\n 'a single JSON response (no streaming).',\n requestBody: {\n contentType: 'application/json',\n description:\n 'Object-mode dispatch payload. `agent` and `messages` are required; `attachmentIds`, `pageContext`, and `modelOverride` are optional.',\n schema: runObjectRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Object-mode run completed; response body contains `{ object, usage?, finishReason? }`.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Malformed payload or message cap exceeded.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 422, description: 'Agent does not support object-mode execution.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'execution_mode_not_supported':\n return 422\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = runObjectRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId: bodyResult.data.agent,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'object',\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n const result = await runAiAgentObject({\n agentId: bodyResult.data.agent,\n input: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n modelOverride: bodyResult.data.modelOverride,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n })\n\n if (result.mode !== 'generate') {\n return jsonError(\n 422,\n 'Streaming object-mode is not supported by the run-object HTTP route; agent must use generate mode.',\n 'execution_mode_not_supported',\n )\n }\n\n return NextResponse.json({\n object: result.object,\n finishReason: result.finishReason,\n usage: result.usage,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n console.error('[AI Run Object] Dispatch failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent object dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport { runAiAgentObject } from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\n\nconst logger = createLogger('ai_assistant')\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst agentIdSchema = z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)')\n\nconst chatMessageSchema = z.object({\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().optional(),\n entityType: z.string().optional(),\n recordId: z.string().optional(),\n })\n .passthrough()\n\nconst runObjectRequestSchema = z.object({\n agent: agentIdSchema,\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n pageContext: pageContextSchema.optional(),\n modelOverride: z.string().optional(),\n})\n\nexport type AiRunObjectRequest = z.infer<typeof runObjectRequestSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Run an AI agent in structured-output (object) mode',\n methods: {\n POST: {\n operationId: 'aiAssistantRunObject',\n summary: 'Run an object-mode AI agent and return the generated object',\n description:\n 'Invokes `runAiAgentObject` server-side for the registered AI agent identified by `agent` ' +\n '(matching \"<module>.<agent>\"). Enforces the same `requiredFeatures`, tool whitelisting, ' +\n 'mutationPolicy, and attachment media-type policy as the chat dispatcher, but additionally ' +\n 'requires the agent to declare `executionMode: \"object\"`. Returns the generated object in ' +\n 'a single JSON response (no streaming).',\n requestBody: {\n contentType: 'application/json',\n description:\n 'Object-mode dispatch payload. `agent` and `messages` are required; `attachmentIds`, `pageContext`, and `modelOverride` are optional.',\n schema: runObjectRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Object-mode run completed; response body contains `{ object, usage?, finishReason? }`.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Malformed payload or message cap exceeded.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 422, description: 'Agent does not support object-mode execution.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'execution_mode_not_supported':\n return 422\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = runObjectRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId: bodyResult.data.agent,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'object',\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n const result = await runAiAgentObject({\n agentId: bodyResult.data.agent,\n input: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n modelOverride: bodyResult.data.modelOverride,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n })\n\n if (result.mode !== 'generate') {\n return jsonError(\n 422,\n 'Streaming object-mode is not supported by the run-object HTTP route; agent must use generate mode.',\n 'execution_mode_not_supported',\n )\n }\n\n return NextResponse.json({\n object: result.object,\n finishReason: result.finishReason,\n usage: result.usage,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n logger.error('AI Run Object \u2014 Dispatch failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent object dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
1
2
  import { NextResponse } from "next/server";
2
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
4
  import {
@@ -14,6 +15,7 @@ import {
14
15
  import { UserRole } from "@open-mercato/core/modules/auth/data/entities";
15
16
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
16
17
  import { checkAiChatRateLimit } from "../../lib/rate-limit.js";
18
+ const logger = createLogger("ai_assistant");
17
19
  const CHAT_SYSTEM_INSTRUCTIONS = `
18
20
  You are a helpful business assistant for Open Mercato.
19
21
 
@@ -156,7 +158,7 @@ async function POST(req) {
156
158
 
157
159
  `));
158
160
  } catch (err) {
159
- console.warn("[AI Chat] Failed to write SSE event:", event.type);
161
+ logger.warn("Failed to write SSE event", { eventType: event.type });
160
162
  }
161
163
  };
162
164
  const closeWriter = async () => {
@@ -192,7 +194,7 @@ async function POST(req) {
192
194
  await client.answerQuestion(answerQuestion.questionId, answerQuestion.answer);
193
195
  return NextResponse.json({ success: true });
194
196
  } catch (error) {
195
- console.error("[AI Chat] Answer error:", error);
197
+ logger.error("AI Chat \u2014 Answer error", { err: error });
196
198
  return NextResponse.json({ error: "Session not available" }, { status: 403 });
197
199
  }
198
200
  }
@@ -211,8 +213,11 @@ async function POST(req) {
211
213
  });
212
214
  if (rateLimited) return rateLimited;
213
215
  const chatStartTime = Date.now();
214
- const messagePreview = lastUserMessage.slice(0, 80).replace(/\n/g, " ");
215
- console.error(`[AI Usage] Chat request: user=${auth.sub} session=${sessionId ? sessionId.slice(0, 16) + "..." : "new"} message="${messagePreview}${lastUserMessage.length > 80 ? "..." : ""}"`);
216
+ logger.info("Chat request", {
217
+ userId: auth.sub,
218
+ sessionId: sessionId ? sessionId.slice(0, 16) : "new",
219
+ messageChars: lastUserMessage.length
220
+ });
216
221
  let sessionToken = null;
217
222
  if (!sessionId) {
218
223
  try {
@@ -226,9 +231,9 @@ async function POST(req) {
226
231
  organizationId: auth.orgId,
227
232
  ttlMinutes: 120
228
233
  });
229
- console.log("[AI Chat] Created session token:", sessionToken.slice(0, 12) + "...");
234
+ logger.info("Created session token");
230
235
  } catch (error) {
231
- console.error("[AI Chat] Failed to create session key:", error);
236
+ logger.error("AI Chat \u2014 Failed to create session key", { err: error });
232
237
  }
233
238
  }
234
239
  let messageToSend = "";
@@ -249,7 +254,7 @@ async function POST(req) {
249
254
  let resultSessionId;
250
255
  try {
251
256
  if (sessionToken) {
252
- console.log("[AI Chat] Emitting session-authorized event");
257
+ logger.info("AI Chat \u2014 Emitting session-authorized event");
253
258
  await writeSSE({
254
259
  type: "session-authorized",
255
260
  sessionToken: sessionToken.slice(0, 12) + "..."
@@ -272,10 +277,7 @@ async function POST(req) {
272
277
  try {
273
278
  await bindOpencodeSessionToApiKey(em, sessionToken, event.sessionId);
274
279
  } catch (bindErr) {
275
- console.error(
276
- "[AI Chat] Failed to bind OpenCode session to api_key:",
277
- bindErr
278
- );
280
+ logger.error("AI Chat \u2014 Failed to bind OpenCode session to api_key", { err: bindErr });
279
281
  }
280
282
  }
281
283
  }
@@ -283,14 +285,21 @@ async function POST(req) {
283
285
  }
284
286
  );
285
287
  } catch (error) {
286
- console.error("[AI Chat] OpenCode error:", error);
288
+ logger.error("AI Chat \u2014 OpenCode error", { err: error });
287
289
  await writeSSE({
288
290
  type: "error",
289
291
  error: error instanceof Error ? error.message : "OpenCode request failed"
290
292
  });
291
293
  } finally {
292
294
  const durationMs = Date.now() - chatStartTime;
293
- console.error(`[AI Usage] Chat complete: user=${auth.sub} session=${(resultSessionId || sessionId || "unknown").slice(0, 16)}... duration=${durationMs}ms toolCalls=${toolCallCount}${lastTokens ? ` tokens={in:${lastTokens.input || 0},out:${lastTokens.output || 0}}` : ""}`);
295
+ logger.info("Chat complete", {
296
+ userId: auth.sub,
297
+ sessionId: (resultSessionId || sessionId || "unknown").slice(0, 16),
298
+ durationMs,
299
+ toolCalls: toolCallCount,
300
+ inputTokens: lastTokens?.input || 0,
301
+ outputTokens: lastTokens?.output || 0
302
+ });
294
303
  await closeWriter();
295
304
  }
296
305
  })();
@@ -302,7 +311,7 @@ async function POST(req) {
302
311
  }
303
312
  });
304
313
  } catch (error) {
305
- console.error("[AI Chat] Error:", error);
314
+ logger.error("AI Chat \u2014 Error", { err: error });
306
315
  return NextResponse.json({ error: "Chat request failed" }, { status: 500 });
307
316
  }
308
317
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/api/chat/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n handleOpenCodeMessageStreaming,\n type OpenCodeAuthContext,\n type OpenCodeStreamEvent,\n} from '../../lib/opencode-handlers'\nimport { createOpenCodeClient } from '../../lib/opencode-client'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n generateSessionToken,\n createSessionApiKey,\n bindOpencodeSessionToApiKey,\n findApiKeyByOpencodeSessionId,\n} from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { checkAiChatRateLimit } from '../../lib/rate-limit'\n\n/**\n * System instructions injected at the start of new chat sessions.\n * These ensure the AI follows the correct workflow for data operations.\n */\nconst CHAT_SYSTEM_INSTRUCTIONS = `\nYou are a helpful business assistant for Open Mercato.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nABSOLUTE RULES \u2014 FOLLOW THESE OR BE CUT OFF\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n1. READ = GET only. If the user says find/list/show/search/get \u2192 use only GET. NEVER call PUT/POST/DELETE for a read query.\n2. PUT path = collection path. id goes in the BODY, not the URL. Example: PUT /api/customers/companies with { id: '...', name: 'New' }. There are NO /{id} path segments.\n3. Confirm before ANY write. Before POST/PUT/DELETE: present your plan in business language, then STOP and wait for user to say \"yes\". Do NOT execute the write in the same turn.\n4. Maximum 4 tool calls per message. Hard limit is 10.\n\nYou have 2 tools \u2014 both accept a \"code\" parameter with an async JavaScript arrow function.\n\nTOOL: search \u2014 discover endpoints and schemas (READ-ONLY, fast)\n - spec.findEndpoints(keyword) \u2192 [{ path, methods }]\n - spec.describeEndpoint(path, method) \u2192 COMPACT: { requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }\n - spec.describeEntity(keyword) \u2192 { className, fields, relationships }\n\nTOOL: execute \u2014 make API calls (reads and writes)\n - api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }\n\nCOMMON API PATHS (use directly \u2014 do NOT call findEndpoints for these):\n /api/customers/companies \u2014 companies (GET list, POST create, PUT update)\n /api/customers/people \u2014 contacts/people\n /api/customers/deals \u2014 deals/opportunities\n /api/customers/activities \u2014 activities/tasks\n /api/sales/orders \u2014 sales orders\n /api/sales/quotes \u2014 quotes\n /api/sales/invoices \u2014 invoices\n /api/catalog/products \u2014 products\n /api/catalog/categories \u2014 categories\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nRECIPES \u2014 follow EXACTLY for each task type\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nFIND/LIST records (1 call):\n For COMMON PATHS: skip describeEndpoint, go straight to execute.\n 1. execute: api.request({ method: 'GET', path: '/api/<module>/<resource>' })\n The \"search\" query param only matches indexed text fields \u2014 it will NOT match concepts like \"Polish\" or \"large\".\n For conceptual/subjective queries, fetch ALL records and use YOUR reasoning to identify matches from the returned data.\n For unknown paths: 1 search + 1 execute.\n\nUPDATE a record (3-4 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'PUT') \u2192 learn requestBody fields AND relatedEntity\n 2. execute: GET the record \u2192 find it, get its ID\n 3. execute: PUT to the COLLECTION path with id IN THE BODY:\n api.request({ method: 'PUT', path: '/api/<module>/<resource>', body: { id: '<uuid>', ...changes } })\n NOTE: All CRUD endpoints use the COLLECTION path. The id goes in the request BODY, not the URL. There are NO /{id} path segments.\n\nCREATE a record (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 gives requiredFields, optionalFields, nestedCollections, and a working example\n 2. Ask user for confirmation with the field values\n 3. execute: api.request({ method: 'POST', ...body })\n If the endpoint has nestedCollections (like lines), include them INLINE in the body \u2014 do NOT create them separately.\n Use the \"example\" from describeEndpoint as your template \u2014 fill in real values.\n Example \u2014 create a quote with line items:\n api.request({ method: 'POST', path: '/api/sales/quotes', body: {\n currencyCode: 'EUR', customerEntityId: '<company-uuid>',\n lines: [{ currencyCode: 'EUR', quantity: 1, productId: '<product-uuid>', name: 'Product Name', kind: 'product' }]\n }})\n Do NOT create lines separately. Do NOT include id, quoteId, or total fields \u2014 the server generates them.\n\nCREATE MULTIPLE records (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 learn fields + example\n 2. execute: loop in one call:\n async () => {\n const results = [];\n for (const item of items) {\n results.push(await api.request({ method: 'POST', path: '...', body: item }));\n }\n return results;\n }\n\nDISCOVER (what endpoints/entities exist) (1 call):\n 1. search: spec.findEndpoints('<keyword>') or spec.describeEntity('<keyword>')\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nHARD RULES\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n- MAXIMUM 4 tool calls per user message. You WILL be cut off after 10.\n- NEVER call findEndpoints or describeEndpoint for COMMON PATHS listed above \u2014 use them directly with execute.\n- NEVER call describeEntity if describeEndpoint already returned relatedEntity.\n- NEVER repeat a search from earlier in the conversation \u2014 reuse previous results.\n- NEVER make N+1 API calls (1 call per record). Fetch a list and reason about the results yourself.\n- When you already have the data you need from a previous call, use it \u2014 do NOT fetch more data to \"enrich\" it.\n- Do NOT write JavaScript filters/regex to match records. Fetch data with a simple api.request() call and use YOUR knowledge to interpret the results.\n- The \"search\" query param is fulltext only \u2014 it won't match nationalities, categories, or subjective criteria. For those, fetch all and reason.\n- describeEndpoint returns a COMPACT summary with requiredFields, optionalFields, and an example. Use the example as your template \u2014 fill in real values and send it.\n- For fields you don't know, OMIT them \u2014 the API uses defaults for optional fields.\n- NEVER try to set computed/total fields (amounts, totals, counts) \u2014 the server calculates them.\n- For updates: describeEndpoint gives you the field names. Go straight to GET + PUT. PUT path is the COLLECTION path, id in BODY.\n- For creates with children (e.g. quote + lines): include children INLINE in the body using the nestedCollections field name.\n\nRESPONSE RULES:\n- Be proactive \u2014 fetch data and present results, don't ask what the user wants to see.\n- Never show technical terms, IDs, JSON, or internal reasoning.\n- Present results in clean business language with **bold names** and bullet points.\n- Only ask for confirmation before create/update/delete operations.\n`.trim()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI chat',\n methods: {\n POST: { summary: 'Send message to AI agent via SSE stream' },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * Get user's role IDs from the database.\n */\nasync function getUserRoleIds(\n em: EntityManager,\n userId: string,\n tenantId: string | null\n): Promise<string[]> {\n if (!tenantId) return []\n\n const links = await findWithDecryption(\n em,\n UserRole,\n { user: userId as any, role: { tenantId } } as any,\n { populate: ['role'] },\n { tenantId, organizationId: null },\n )\n const linkList = Array.isArray(links) ? links : []\n return linkList\n .map((l) => (l.role as any)?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\n/**\n * Chat endpoint that routes messages to OpenCode agent.\n * OpenCode connects to MCP server for tool access (search, execute, context_whoami).\n *\n * Emits verbose SSE events for debugging:\n * - thinking: Agent started processing\n * - metadata: Model, tokens, timing info\n * - tool-call: Tool invocation with args\n * - tool-result: Tool response\n * - text: Response text\n * - question: Confirmation question from agent\n * - done: Complete with session ID\n * - error: Error occurred\n */\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { messages, sessionId, answerQuestion } = body as {\n messages?: Array<{ role: string; content: string }>\n sessionId?: string\n // For answering a question\n answerQuestion?: {\n questionId: string\n answer: number\n sessionId: string\n }\n }\n\n // Create SSE stream for frontend compatibility\n const encoder = new TextEncoder()\n const stream = new TransformStream()\n const writer = stream.writable.getWriter()\n let writerClosed = false\n\n const writeSSE = async (event: OpenCodeStreamEvent | { type: string; [key: string]: unknown }) => {\n if (writerClosed) return // Guard against writes after close\n try {\n const jsonStr = JSON.stringify(event)\n await writer.write(encoder.encode(`data: ${jsonStr}\\n\\n`))\n } catch (err) {\n // Writer may have been closed by client disconnect\n console.warn('[AI Chat] Failed to write SSE event:', event.type)\n }\n }\n\n const closeWriter = async () => {\n if (writerClosed) return\n writerClosed = true\n try {\n await writer.close()\n } catch {\n // Already closed\n }\n }\n\n // Resolve a request-scoped EntityManager once \u2014 needed both by the\n // answerQuestion short-circuit (for ownership re-check) and by the\n // streaming branch (for session-token mint + post-`done` binding).\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const opencodeAuth: OpenCodeAuthContext = {\n userId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n // Handle question answer - simple JSON response, not SSE\n // The original SSE stream continues and will receive the follow-up response\n if (answerQuestion) {\n try {\n const client = createOpenCodeClient()\n // Resolve the question's actual sessionID from OpenCode \u2014 never\n // trust the caller-supplied `answerQuestion.sessionId` blindly.\n const pending = await client.getPendingQuestions()\n const matching = pending.find((q) => q.id === answerQuestion.questionId)\n if (!matching) {\n // Unknown / stale question id \u2014 same opaque response as a\n // foreign-owner mismatch to avoid leaking which case occurred.\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n if (matching.sessionID !== answerQuestion.sessionId) {\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n // Look up the api_key row bound to this OpenCode session and\n // assert ownership matches the authenticated caller.\n const ownerRow = await findApiKeyByOpencodeSessionId(em, matching.sessionID)\n if (\n !ownerRow ||\n ownerRow.sessionUserId !== opencodeAuth.userId ||\n (ownerRow.tenantId ?? null) !== opencodeAuth.tenantId ||\n (ownerRow.organizationId ?? null) !== opencodeAuth.organizationId\n ) {\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n\n await client.answerQuestion(answerQuestion.questionId, answerQuestion.answer)\n return NextResponse.json({ success: true })\n } catch (error) {\n // Fail-closed: any failure during the answerQuestion path \u2014 ownership\n // mismatch, OpenCode unreachable, getPendingQuestions throwing,\n // findApiKeyByOpencodeSessionId throwing \u2014 returns the same opaque\n // 403 envelope used for ownership rejection. This matches finding #1's\n // security intent that the answerQuestion short-circuit MUST NOT leak\n // whether the failure was authorization vs backend connectivity vs\n // implementation error. The real reason is captured in server logs.\n console.error('[AI Chat] Answer error:', error)\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n }\n\n // Handle regular message\n if (!messages || !Array.isArray(messages)) {\n return NextResponse.json({ error: 'messages array is required' }, { status: 400 })\n }\n\n // Get the latest user message\n const lastUserMessage = messages.filter((m) => m.role === 'user').pop()?.content\n if (!lastUserMessage) {\n return NextResponse.json({ error: 'No user message found' }, { status: 400 })\n }\n\n // Bound per-user LLM spend, DB writes (incl. the ephemeral session-key insert\n // below), and SSE buffering. Fail-open so a missing limiter never blocks a turn.\n // Reuses the request container resolved above for the answerQuestion ownership re-check.\n const rateLimited = await checkAiChatRateLimit({\n req,\n container,\n userId: auth.sub,\n tenantId: auth.tenantId,\n })\n if (rateLimited) return rateLimited\n\n const chatStartTime = Date.now()\n const messagePreview = lastUserMessage.slice(0, 80).replace(/\\n/g, ' ')\n console.error(`[AI Usage] Chat request: user=${auth.sub} session=${sessionId ? sessionId.slice(0, 16) + '...' : 'new'} message=\"${messagePreview}${lastUserMessage.length > 80 ? '...' : ''}\"`)\n\n\n // For new sessions, create an ephemeral API key that inherits user permissions\n // The API key secret is encrypted and stored; MCP server recovers it via session token\n let sessionToken: string | null = null\n if (!sessionId) {\n try {\n // Get user's role IDs from database\n const userRoleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)\n\n // Generate session token and create ephemeral key\n sessionToken = generateSessionToken()\n await createSessionApiKey(em, {\n sessionToken,\n userId: auth.sub,\n userRoles: userRoleIds,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n ttlMinutes: 120,\n })\n console.log('[AI Chat] Created session token:', sessionToken.slice(0, 12) + '...')\n } catch (error) {\n console.error('[AI Chat] Failed to create session key:', error)\n // Continue without session key - tools will use static API key auth\n }\n }\n\n // Build the message to send to OpenCode\n // For NEW sessions: inject system instructions + session token\n // For existing sessions: only inject session token if available\n let messageToSend = ''\n\n // For NEW sessions only, prepend system instructions\n if (!sessionId) {\n messageToSend = `${CHAT_SYSTEM_INSTRUCTIONS}\\n\\n`\n }\n\n // If we have a session token, prepend explicit instructions for the AI to include it in tool calls\n if (sessionToken) {\n messageToSend += `[Session Authorization: ${sessionToken}. Include \"_sessionToken\": \"${sessionToken}\" in EVERY tool call.]\\n\\n`\n }\n\n messageToSend += lastUserMessage\n\n // Process in background - starts AFTER Response is returned so there's a reader for the stream\n ;(async () => {\n let toolCallCount = 0\n let lastTokens: { input?: number; output?: number } | undefined\n let resultSessionId: string | undefined\n\n try {\n // Emit session-authorized event first (if we have a token)\n if (sessionToken) {\n console.log('[AI Chat] Emitting session-authorized event')\n await writeSSE({\n type: 'session-authorized',\n sessionToken: sessionToken.slice(0, 12) + '...',\n })\n }\n\n // Emit thinking event for UX feedback\n await writeSSE({ type: 'thinking' })\n\n // Use streaming handler that supports questions\n await handleOpenCodeMessageStreaming(\n {\n message: messageToSend,\n sessionId,\n auth: opencodeAuth,\n em,\n },\n async (event) => {\n // Track usage from stream events\n if (event.type === 'tool-call') toolCallCount++\n if (event.type === 'metadata' && 'tokens' in event) lastTokens = event.tokens\n if (event.type === 'done' && 'sessionId' in event) {\n resultSessionId = event.sessionId\n // First-time binding: when the caller did NOT supply a\n // sessionId (i.e., this is a freshly minted chat) and we\n // successfully created a session token earlier, bind the\n // newly-resolved OpenCode session id to that api_key row so\n // subsequent resumes can be authorized.\n if (!sessionId && sessionToken && event.sessionId) {\n try {\n await bindOpencodeSessionToApiKey(em, sessionToken, event.sessionId)\n } catch (bindErr) {\n // The response stream is already in-flight \u2014 surface the\n // failure to logs without disturbing the SSE pipeline.\n console.error(\n '[AI Chat] Failed to bind OpenCode session to api_key:',\n bindErr\n )\n }\n }\n }\n\n await writeSSE(event)\n }\n )\n } catch (error) {\n console.error('[AI Chat] OpenCode error:', error)\n await writeSSE({\n type: 'error',\n error: error instanceof Error ? error.message : 'OpenCode request failed',\n })\n } finally {\n const durationMs = Date.now() - chatStartTime\n console.error(`[AI Usage] Chat complete: user=${auth.sub} session=${(resultSessionId || sessionId || 'unknown').slice(0, 16)}... duration=${durationMs}ms toolCalls=${toolCallCount}${lastTokens ? ` tokens={in:${lastTokens.input || 0},out:${lastTokens.output || 0}}` : ''}`)\n await closeWriter()\n }\n })()\n\n return new Response(stream.readable, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n })\n } catch (error) {\n console.error('[AI Chat] Error:', error)\n return NextResponse.json({ error: 'Chat request failed' }, { status: 500 })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n handleOpenCodeMessageStreaming,\n type OpenCodeAuthContext,\n type OpenCodeStreamEvent,\n} from '../../lib/opencode-handlers'\nimport { createOpenCodeClient } from '../../lib/opencode-client'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n generateSessionToken,\n createSessionApiKey,\n bindOpencodeSessionToApiKey,\n findApiKeyByOpencodeSessionId,\n} from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { checkAiChatRateLimit } from '../../lib/rate-limit'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * System instructions injected at the start of new chat sessions.\n * These ensure the AI follows the correct workflow for data operations.\n */\nconst CHAT_SYSTEM_INSTRUCTIONS = `\nYou are a helpful business assistant for Open Mercato.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nABSOLUTE RULES \u2014 FOLLOW THESE OR BE CUT OFF\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n1. READ = GET only. If the user says find/list/show/search/get \u2192 use only GET. NEVER call PUT/POST/DELETE for a read query.\n2. PUT path = collection path. id goes in the BODY, not the URL. Example: PUT /api/customers/companies with { id: '...', name: 'New' }. There are NO /{id} path segments.\n3. Confirm before ANY write. Before POST/PUT/DELETE: present your plan in business language, then STOP and wait for user to say \"yes\". Do NOT execute the write in the same turn.\n4. Maximum 4 tool calls per message. Hard limit is 10.\n\nYou have 2 tools \u2014 both accept a \"code\" parameter with an async JavaScript arrow function.\n\nTOOL: search \u2014 discover endpoints and schemas (READ-ONLY, fast)\n - spec.findEndpoints(keyword) \u2192 [{ path, methods }]\n - spec.describeEndpoint(path, method) \u2192 COMPACT: { requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }\n - spec.describeEntity(keyword) \u2192 { className, fields, relationships }\n\nTOOL: execute \u2014 make API calls (reads and writes)\n - api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }\n\nCOMMON API PATHS (use directly \u2014 do NOT call findEndpoints for these):\n /api/customers/companies \u2014 companies (GET list, POST create, PUT update)\n /api/customers/people \u2014 contacts/people\n /api/customers/deals \u2014 deals/opportunities\n /api/customers/activities \u2014 activities/tasks\n /api/sales/orders \u2014 sales orders\n /api/sales/quotes \u2014 quotes\n /api/sales/invoices \u2014 invoices\n /api/catalog/products \u2014 products\n /api/catalog/categories \u2014 categories\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nRECIPES \u2014 follow EXACTLY for each task type\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nFIND/LIST records (1 call):\n For COMMON PATHS: skip describeEndpoint, go straight to execute.\n 1. execute: api.request({ method: 'GET', path: '/api/<module>/<resource>' })\n The \"search\" query param only matches indexed text fields \u2014 it will NOT match concepts like \"Polish\" or \"large\".\n For conceptual/subjective queries, fetch ALL records and use YOUR reasoning to identify matches from the returned data.\n For unknown paths: 1 search + 1 execute.\n\nUPDATE a record (3-4 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'PUT') \u2192 learn requestBody fields AND relatedEntity\n 2. execute: GET the record \u2192 find it, get its ID\n 3. execute: PUT to the COLLECTION path with id IN THE BODY:\n api.request({ method: 'PUT', path: '/api/<module>/<resource>', body: { id: '<uuid>', ...changes } })\n NOTE: All CRUD endpoints use the COLLECTION path. The id goes in the request BODY, not the URL. There are NO /{id} path segments.\n\nCREATE a record (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 gives requiredFields, optionalFields, nestedCollections, and a working example\n 2. Ask user for confirmation with the field values\n 3. execute: api.request({ method: 'POST', ...body })\n If the endpoint has nestedCollections (like lines), include them INLINE in the body \u2014 do NOT create them separately.\n Use the \"example\" from describeEndpoint as your template \u2014 fill in real values.\n Example \u2014 create a quote with line items:\n api.request({ method: 'POST', path: '/api/sales/quotes', body: {\n currencyCode: 'EUR', customerEntityId: '<company-uuid>',\n lines: [{ currencyCode: 'EUR', quantity: 1, productId: '<product-uuid>', name: 'Product Name', kind: 'product' }]\n }})\n Do NOT create lines separately. Do NOT include id, quoteId, or total fields \u2014 the server generates them.\n\nCREATE MULTIPLE records (2-3 calls):\n 1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 learn fields + example\n 2. execute: loop in one call:\n async () => {\n const results = [];\n for (const item of items) {\n results.push(await api.request({ method: 'POST', path: '...', body: item }));\n }\n return results;\n }\n\nDISCOVER (what endpoints/entities exist) (1 call):\n 1. search: spec.findEndpoints('<keyword>') or spec.describeEntity('<keyword>')\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nHARD RULES\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n- MAXIMUM 4 tool calls per user message. You WILL be cut off after 10.\n- NEVER call findEndpoints or describeEndpoint for COMMON PATHS listed above \u2014 use them directly with execute.\n- NEVER call describeEntity if describeEndpoint already returned relatedEntity.\n- NEVER repeat a search from earlier in the conversation \u2014 reuse previous results.\n- NEVER make N+1 API calls (1 call per record). Fetch a list and reason about the results yourself.\n- When you already have the data you need from a previous call, use it \u2014 do NOT fetch more data to \"enrich\" it.\n- Do NOT write JavaScript filters/regex to match records. Fetch data with a simple api.request() call and use YOUR knowledge to interpret the results.\n- The \"search\" query param is fulltext only \u2014 it won't match nationalities, categories, or subjective criteria. For those, fetch all and reason.\n- describeEndpoint returns a COMPACT summary with requiredFields, optionalFields, and an example. Use the example as your template \u2014 fill in real values and send it.\n- For fields you don't know, OMIT them \u2014 the API uses defaults for optional fields.\n- NEVER try to set computed/total fields (amounts, totals, counts) \u2014 the server calculates them.\n- For updates: describeEndpoint gives you the field names. Go straight to GET + PUT. PUT path is the COLLECTION path, id in BODY.\n- For creates with children (e.g. quote + lines): include children INLINE in the body using the nestedCollections field name.\n\nRESPONSE RULES:\n- Be proactive \u2014 fetch data and present results, don't ask what the user wants to see.\n- Never show technical terms, IDs, JSON, or internal reasoning.\n- Present results in clean business language with **bold names** and bullet points.\n- Only ask for confirmation before create/update/delete operations.\n`.trim()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI chat',\n methods: {\n POST: { summary: 'Send message to AI agent via SSE stream' },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * Get user's role IDs from the database.\n */\nasync function getUserRoleIds(\n em: EntityManager,\n userId: string,\n tenantId: string | null\n): Promise<string[]> {\n if (!tenantId) return []\n\n const links = await findWithDecryption(\n em,\n UserRole,\n { user: userId as any, role: { tenantId } } as any,\n { populate: ['role'] },\n { tenantId, organizationId: null },\n )\n const linkList = Array.isArray(links) ? links : []\n return linkList\n .map((l) => (l.role as any)?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\n/**\n * Chat endpoint that routes messages to OpenCode agent.\n * OpenCode connects to MCP server for tool access (search, execute, context_whoami).\n *\n * Emits verbose SSE events for debugging:\n * - thinking: Agent started processing\n * - metadata: Model, tokens, timing info\n * - tool-call: Tool invocation with args\n * - tool-result: Tool response\n * - text: Response text\n * - question: Confirmation question from agent\n * - done: Complete with session ID\n * - error: Error occurred\n */\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { messages, sessionId, answerQuestion } = body as {\n messages?: Array<{ role: string; content: string }>\n sessionId?: string\n // For answering a question\n answerQuestion?: {\n questionId: string\n answer: number\n sessionId: string\n }\n }\n\n // Create SSE stream for frontend compatibility\n const encoder = new TextEncoder()\n const stream = new TransformStream()\n const writer = stream.writable.getWriter()\n let writerClosed = false\n\n const writeSSE = async (event: OpenCodeStreamEvent | { type: string; [key: string]: unknown }) => {\n if (writerClosed) return // Guard against writes after close\n try {\n const jsonStr = JSON.stringify(event)\n await writer.write(encoder.encode(`data: ${jsonStr}\\n\\n`))\n } catch (err) {\n // Writer may have been closed by client disconnect\n logger.warn('Failed to write SSE event', { eventType: event.type })\n }\n }\n\n const closeWriter = async () => {\n if (writerClosed) return\n writerClosed = true\n try {\n await writer.close()\n } catch {\n // Already closed\n }\n }\n\n // Resolve a request-scoped EntityManager once \u2014 needed both by the\n // answerQuestion short-circuit (for ownership re-check) and by the\n // streaming branch (for session-token mint + post-`done` binding).\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const opencodeAuth: OpenCodeAuthContext = {\n userId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n // Handle question answer - simple JSON response, not SSE\n // The original SSE stream continues and will receive the follow-up response\n if (answerQuestion) {\n try {\n const client = createOpenCodeClient()\n // Resolve the question's actual sessionID from OpenCode \u2014 never\n // trust the caller-supplied `answerQuestion.sessionId` blindly.\n const pending = await client.getPendingQuestions()\n const matching = pending.find((q) => q.id === answerQuestion.questionId)\n if (!matching) {\n // Unknown / stale question id \u2014 same opaque response as a\n // foreign-owner mismatch to avoid leaking which case occurred.\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n if (matching.sessionID !== answerQuestion.sessionId) {\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n // Look up the api_key row bound to this OpenCode session and\n // assert ownership matches the authenticated caller.\n const ownerRow = await findApiKeyByOpencodeSessionId(em, matching.sessionID)\n if (\n !ownerRow ||\n ownerRow.sessionUserId !== opencodeAuth.userId ||\n (ownerRow.tenantId ?? null) !== opencodeAuth.tenantId ||\n (ownerRow.organizationId ?? null) !== opencodeAuth.organizationId\n ) {\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n\n await client.answerQuestion(answerQuestion.questionId, answerQuestion.answer)\n return NextResponse.json({ success: true })\n } catch (error) {\n // Fail-closed: any failure during the answerQuestion path \u2014 ownership\n // mismatch, OpenCode unreachable, getPendingQuestions throwing,\n // findApiKeyByOpencodeSessionId throwing \u2014 returns the same opaque\n // 403 envelope used for ownership rejection. This matches finding #1's\n // security intent that the answerQuestion short-circuit MUST NOT leak\n // whether the failure was authorization vs backend connectivity vs\n // implementation error. The real reason is captured in server logs.\n logger.error('AI Chat \u2014 Answer error', { err: error })\n return NextResponse.json({ error: 'Session not available' }, { status: 403 })\n }\n }\n\n // Handle regular message\n if (!messages || !Array.isArray(messages)) {\n return NextResponse.json({ error: 'messages array is required' }, { status: 400 })\n }\n\n // Get the latest user message\n const lastUserMessage = messages.filter((m) => m.role === 'user').pop()?.content\n if (!lastUserMessage) {\n return NextResponse.json({ error: 'No user message found' }, { status: 400 })\n }\n\n // Bound per-user LLM spend, DB writes (incl. the ephemeral session-key insert\n // below), and SSE buffering. Fail-open so a missing limiter never blocks a turn.\n // Reuses the request container resolved above for the answerQuestion ownership re-check.\n const rateLimited = await checkAiChatRateLimit({\n req,\n container,\n userId: auth.sub,\n tenantId: auth.tenantId,\n })\n if (rateLimited) return rateLimited\n\n const chatStartTime = Date.now()\n logger.info('Chat request', {\n userId: auth.sub,\n sessionId: sessionId ? sessionId.slice(0, 16) : 'new',\n messageChars: lastUserMessage.length,\n })\n\n // For new sessions, create an ephemeral API key that inherits user permissions\n // The API key secret is encrypted and stored; MCP server recovers it via session token\n let sessionToken: string | null = null\n if (!sessionId) {\n try {\n // Get user's role IDs from database\n const userRoleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)\n\n // Generate session token and create ephemeral key\n sessionToken = generateSessionToken()\n await createSessionApiKey(em, {\n sessionToken,\n userId: auth.sub,\n userRoles: userRoleIds,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n ttlMinutes: 120,\n })\n logger.info('Created session token')\n } catch (error) {\n logger.error('AI Chat \u2014 Failed to create session key', { err: error })\n // Continue without session key - tools will use static API key auth\n }\n }\n\n // Build the message to send to OpenCode\n // For NEW sessions: inject system instructions + session token\n // For existing sessions: only inject session token if available\n let messageToSend = ''\n\n // For NEW sessions only, prepend system instructions\n if (!sessionId) {\n messageToSend = `${CHAT_SYSTEM_INSTRUCTIONS}\\n\\n`\n }\n\n // If we have a session token, prepend explicit instructions for the AI to include it in tool calls\n if (sessionToken) {\n messageToSend += `[Session Authorization: ${sessionToken}. Include \"_sessionToken\": \"${sessionToken}\" in EVERY tool call.]\\n\\n`\n }\n\n messageToSend += lastUserMessage\n\n // Process in background - starts AFTER Response is returned so there's a reader for the stream\n ;(async () => {\n let toolCallCount = 0\n let lastTokens: { input?: number; output?: number } | undefined\n let resultSessionId: string | undefined\n\n try {\n // Emit session-authorized event first (if we have a token)\n if (sessionToken) {\n logger.info('AI Chat \u2014 Emitting session-authorized event')\n await writeSSE({\n type: 'session-authorized',\n sessionToken: sessionToken.slice(0, 12) + '...',\n })\n }\n\n // Emit thinking event for UX feedback\n await writeSSE({ type: 'thinking' })\n\n // Use streaming handler that supports questions\n await handleOpenCodeMessageStreaming(\n {\n message: messageToSend,\n sessionId,\n auth: opencodeAuth,\n em,\n },\n async (event) => {\n // Track usage from stream events\n if (event.type === 'tool-call') toolCallCount++\n if (event.type === 'metadata' && 'tokens' in event) lastTokens = event.tokens\n if (event.type === 'done' && 'sessionId' in event) {\n resultSessionId = event.sessionId\n // First-time binding: when the caller did NOT supply a\n // sessionId (i.e., this is a freshly minted chat) and we\n // successfully created a session token earlier, bind the\n // newly-resolved OpenCode session id to that api_key row so\n // subsequent resumes can be authorized.\n if (!sessionId && sessionToken && event.sessionId) {\n try {\n await bindOpencodeSessionToApiKey(em, sessionToken, event.sessionId)\n } catch (bindErr) {\n // The response stream is already in-flight \u2014 surface the\n // failure to logs without disturbing the SSE pipeline.\n logger.error('AI Chat \u2014 Failed to bind OpenCode session to api_key', { err: bindErr })\n }\n }\n }\n\n await writeSSE(event)\n }\n )\n } catch (error) {\n logger.error('AI Chat \u2014 OpenCode error', { err: error })\n await writeSSE({\n type: 'error',\n error: error instanceof Error ? error.message : 'OpenCode request failed',\n })\n } finally {\n const durationMs = Date.now() - chatStartTime\n logger.info('Chat complete', {\n userId: auth.sub,\n sessionId: (resultSessionId || sessionId || 'unknown').slice(0, 16),\n durationMs,\n toolCalls: toolCallCount,\n inputTokens: lastTokens?.input || 0,\n outputTokens: lastTokens?.output || 0,\n })\n await closeWriter()\n }\n })()\n\n return new Response(stream.readable, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n })\n } catch (error) {\n logger.error('AI Chat \u2014 Error', { err: error })\n return NextResponse.json({ error: 'Chat request failed' }, { status: 500 })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,8 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
1
2
  import { NextResponse } from "next/server";
2
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
4
  import { handleOpenCodeHealth } from "../../lib/opencode-handlers.js";
5
+ const logger = createLogger("ai_assistant");
4
6
  const openApi = {
5
7
  tag: "AI Assistant",
6
8
  summary: "AI assistant health check",
@@ -20,7 +22,7 @@ async function GET(req) {
20
22
  const health = await handleOpenCodeHealth();
21
23
  return NextResponse.json(health);
22
24
  } catch (error) {
23
- console.error("[AI Health] Error:", error);
25
+ logger.error("AI Health \u2014 Error", { err: error });
24
26
  return NextResponse.json(
25
27
  { error: "Failed to check health", message: error instanceof Error ? error.message : "Unknown error" },
26
28
  { status: 500 }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/api/health/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { handleOpenCodeHealth } from '../../lib/opencode-handlers'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant health check',\n methods: {\n GET: { summary: 'Check OpenCode and MCP connection status' },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * GET /api/ai_assistant/health\n *\n * Returns OpenCode and MCP connection status.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await handleOpenCodeHealth()\n return NextResponse.json(health)\n } catch (error) {\n console.error('[AI Health] Error:', error)\n return NextResponse.json(\n { error: 'Failed to check health', message: error instanceof Error ? error.message : 'Unknown error' },\n { status: 500 }\n )\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AAE9B,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,2CAA2C;AAAA,EAC7D;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAOA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,qBAAqB;AAC1C,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,OAAO;AACd,YAAQ,MAAM,sBAAsB,KAAK;AACzC,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,0BAA0B,SAAS,iBAAiB,QAAQ,MAAM,UAAU,gBAAgB;AAAA,MACrG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { handleOpenCodeHealth } from '../../lib/opencode-handlers'\n\nconst logger = createLogger('ai_assistant')\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant health check',\n methods: {\n GET: { summary: 'Check OpenCode and MCP connection status' },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * GET /api/ai_assistant/health\n *\n * Returns OpenCode and MCP connection status.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await handleOpenCodeHealth()\n return NextResponse.json(health)\n } catch (error) {\n logger.error('AI Health \u2014 Error', { err: error })\n return NextResponse.json(\n { error: 'Failed to check health', message: error instanceof Error ? error.message : 'Unknown error' },\n { status: 500 }\n )\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AAErC,MAAM,SAAS,aAAa,cAAc;AAEnC,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,2CAA2C;AAAA,EAC7D;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAOA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,qBAAqB;AAC1C,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,OAAO;AACd,WAAO,MAAM,0BAAqB,EAAE,KAAK,MAAM,CAAC;AAChD,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,0BAA0B,SAAS,iBAAiB,QAAQ,MAAM,UAAU,gBAAgB;AAAA,MACrG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
1
2
  import { NextResponse } from "next/server";
2
3
  import { z } from "zod";
3
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
@@ -5,6 +6,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
6
  import { createApiKey } from "@open-mercato/core/modules/api_keys/services/apiKeyService";
6
7
  import { UserRole } from "@open-mercato/core/modules/auth/data/entities";
7
8
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
9
+ const logger = createLogger("ai_assistant");
8
10
  const metadata = {
9
11
  POST: { requireAuth: true, requireFeatures: ["api_keys.create"] }
10
12
  };
@@ -53,7 +55,7 @@ async function POST(req) {
53
55
  roles: roleIds
54
56
  });
55
57
  } catch (error) {
56
- console.error("[MCP Key] Error:", error);
58
+ logger.error("MCP Key \u2014 Error", { err: error });
57
59
  return NextResponse.json(
58
60
  { error: "Failed to create MCP API key" },
59
61
  { status: 500 }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/api/mcp-key/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createApiKey } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n}\n\n/**\n * Resolve the calling user's role ids for the active tenant. Mirrors the\n * `session-key` route so the generated key carries exactly the caller's roles.\n */\nasync function getUserRoleIds(\n em: EntityManager,\n userId: string,\n tenantId: string | null,\n): Promise<string[]> {\n if (!tenantId) return []\n const links = await findWithDecryption(\n em,\n UserRole,\n { user: userId as any, role: { tenantId } } as any,\n { populate: ['role'] },\n { tenantId, organizationId: null },\n )\n const linkList = Array.isArray(links) ? links : []\n return linkList\n .map((link) => (link.role as any)?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nconst bodySchema = z.object({\n name: z.string().min(1).max(120).optional(),\n description: z.string().max(1000).optional().nullable(),\n})\n\n/**\n * POST /api/ai_assistant/mcp-key\n *\n * Creates a persistent MCP API key (`omk_...`) for use in `.mcp.json`. Unlike a\n * raw `POST /api/api_keys/keys` call \u2014 which assigns no roles unless the client\n * passes them \u2014 this endpoint resolves the **caller's own roles** server-side\n * and attaches them to the key, so the key inherits the current user's ACL.\n * The user cannot escalate: they only ever grant their own roles to their own\n * key.\n */\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const raw = await req.json().catch(() => ({}))\n const body = bodySchema.parse(raw ?? {})\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const roleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)\n\n const { record, secret } = await createApiKey(em, {\n name: body.name ?? `MCP Config - ${new Date().toISOString().slice(0, 10)}`,\n description: body.description ?? 'Generated from AI Assistant settings for MCP client',\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roles: roleIds,\n createdBy: auth.sub,\n })\n\n return NextResponse.json({\n id: record.id,\n name: record.name,\n keyPrefix: record.keyPrefix,\n secret,\n tenantId: record.tenantId ?? null,\n organizationId: record.organizationId ?? null,\n roles: roleIds,\n })\n } catch (error) {\n console.error('[MCP Key] Error:', error)\n return NextResponse.json(\n { error: 'Failed to create MCP API key' },\n { status: 500 },\n )\n }\n}\n\nconst responseSchema = z.object({\n id: z.string(),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value (omk_...). Shown once.'),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(z.string()).describe('Role ids inherited from the calling user.'),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Create MCP API key',\n description:\n 'Creates a persistent MCP API key that inherits the calling user\\'s roles, for use in an MCP client `.mcp.json`.',\n methods: {\n POST: {\n summary: 'Generate MCP API key',\n description:\n 'Generates a persistent `omk_` API key scoped to the calling user\\'s tenant/organization and carrying the caller\\'s own roles, so the key has the same ACL as the user.',\n responses: [\n { status: 200, description: 'API key created', schema: responseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 500, description: 'Failed to create MCP API key', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createApiKey } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst logger = createLogger('ai_assistant')\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n}\n\n/**\n * Resolve the calling user's role ids for the active tenant. Mirrors the\n * `session-key` route so the generated key carries exactly the caller's roles.\n */\nasync function getUserRoleIds(\n em: EntityManager,\n userId: string,\n tenantId: string | null,\n): Promise<string[]> {\n if (!tenantId) return []\n const links = await findWithDecryption(\n em,\n UserRole,\n { user: userId as any, role: { tenantId } } as any,\n { populate: ['role'] },\n { tenantId, organizationId: null },\n )\n const linkList = Array.isArray(links) ? links : []\n return linkList\n .map((link) => (link.role as any)?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nconst bodySchema = z.object({\n name: z.string().min(1).max(120).optional(),\n description: z.string().max(1000).optional().nullable(),\n})\n\n/**\n * POST /api/ai_assistant/mcp-key\n *\n * Creates a persistent MCP API key (`omk_...`) for use in `.mcp.json`. Unlike a\n * raw `POST /api/api_keys/keys` call \u2014 which assigns no roles unless the client\n * passes them \u2014 this endpoint resolves the **caller's own roles** server-side\n * and attaches them to the key, so the key inherits the current user's ACL.\n * The user cannot escalate: they only ever grant their own roles to their own\n * key.\n */\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const raw = await req.json().catch(() => ({}))\n const body = bodySchema.parse(raw ?? {})\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const roleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)\n\n const { record, secret } = await createApiKey(em, {\n name: body.name ?? `MCP Config - ${new Date().toISOString().slice(0, 10)}`,\n description: body.description ?? 'Generated from AI Assistant settings for MCP client',\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roles: roleIds,\n createdBy: auth.sub,\n })\n\n return NextResponse.json({\n id: record.id,\n name: record.name,\n keyPrefix: record.keyPrefix,\n secret,\n tenantId: record.tenantId ?? null,\n organizationId: record.organizationId ?? null,\n roles: roleIds,\n })\n } catch (error) {\n logger.error('MCP Key \u2014 Error', { err: error })\n return NextResponse.json(\n { error: 'Failed to create MCP API key' },\n { status: 500 },\n )\n }\n}\n\nconst responseSchema = z.object({\n id: z.string(),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value (omk_...). Shown once.'),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(z.string()).describe('Role ids inherited from the calling user.'),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Create MCP API key',\n description:\n 'Creates a persistent MCP API key that inherits the calling user\\'s roles, for use in an MCP client `.mcp.json`.',\n methods: {\n POST: {\n summary: 'Generate MCP API key',\n description:\n 'Generates a persistent `omk_` API key scoped to the calling user\\'s tenant/organization and carrying the caller\\'s own roles, so the key has the same ACL as the user.',\n responses: [\n { status: 200, description: 'API key created', schema: responseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 500, description: 'Failed to create MCP API key', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }