@open-mercato/ai-assistant 0.6.3-develop.3894.1.352abf4240 → 0.6.3

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 (80) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js +87 -0
  3. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js.map +7 -0
  4. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js +119 -0
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js.map +7 -0
  6. package/dist/modules/ai_assistant/acl.js +1 -0
  7. package/dist/modules/ai_assistant/acl.js.map +2 -2
  8. package/dist/modules/ai_assistant/api/ai/chat/route.js +3 -0
  9. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  10. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js +128 -0
  11. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js.map +7 -0
  12. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js +271 -0
  13. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js.map +7 -0
  14. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +9 -1
  15. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +2 -2
  16. package/dist/modules/ai_assistant/api/ai/conversations/route.js +4 -1
  17. package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +2 -2
  18. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +5 -1
  19. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  20. package/dist/modules/ai_assistant/components/ConversationShareButton.js +5 -0
  21. package/dist/modules/ai_assistant/components/ConversationShareButton.js.map +7 -0
  22. package/dist/modules/ai_assistant/components/ConversationShareDialog.js +5 -0
  23. package/dist/modules/ai_assistant/components/ConversationShareDialog.js.map +7 -0
  24. package/dist/modules/ai_assistant/data/entities.js +3 -0
  25. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  26. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +235 -5
  27. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +2 -2
  28. package/dist/modules/ai_assistant/events.js +14 -0
  29. package/dist/modules/ai_assistant/events.js.map +2 -2
  30. package/dist/modules/ai_assistant/i18n/de.json +17 -0
  31. package/dist/modules/ai_assistant/i18n/en.json +17 -0
  32. package/dist/modules/ai_assistant/i18n/es.json +17 -0
  33. package/dist/modules/ai_assistant/i18n/pl.json +17 -0
  34. package/dist/modules/ai_assistant/lib/conversation-storage.js +12 -3
  35. package/dist/modules/ai_assistant/lib/conversation-storage.js.map +2 -2
  36. package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js +15 -0
  37. package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js.map +7 -0
  38. package/dist/modules/ai_assistant/notifications.client.js +30 -0
  39. package/dist/modules/ai_assistant/notifications.client.js.map +7 -0
  40. package/dist/modules/ai_assistant/notifications.js +27 -0
  41. package/dist/modules/ai_assistant/notifications.js.map +7 -0
  42. package/dist/modules/ai_assistant/setup.js +2 -1
  43. package/dist/modules/ai_assistant/setup.js.map +2 -2
  44. package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +59 -0
  45. package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +7 -0
  46. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js +123 -0
  47. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js.map +7 -0
  48. package/generated/entities/ai_chat_conversation_participant/index.ts +1 -0
  49. package/generated/entity-fields-registry.ts +1 -0
  50. package/package.json +7 -8
  51. package/src/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.ts +117 -0
  52. package/src/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.ts +159 -0
  53. package/src/modules/ai_assistant/__tests__/integration/ai-chat-sharing.test.ts +406 -0
  54. package/src/modules/ai_assistant/acl.ts +1 -0
  55. package/src/modules/ai_assistant/api/ai/chat/route.ts +3 -0
  56. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.ts +149 -0
  57. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.ts +314 -0
  58. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +9 -1
  59. package/src/modules/ai_assistant/api/ai/conversations/route.ts +4 -1
  60. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +4 -0
  61. package/src/modules/ai_assistant/components/ConversationShareButton.tsx +1 -0
  62. package/src/modules/ai_assistant/components/ConversationShareDialog.tsx +1 -0
  63. package/src/modules/ai_assistant/data/entities.ts +4 -0
  64. package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +270 -7
  65. package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +297 -3
  66. package/src/modules/ai_assistant/events.ts +31 -0
  67. package/src/modules/ai_assistant/i18n/__tests__/conversation-share-translations.test.ts +59 -0
  68. package/src/modules/ai_assistant/i18n/de.json +17 -0
  69. package/src/modules/ai_assistant/i18n/en.json +17 -0
  70. package/src/modules/ai_assistant/i18n/es.json +17 -0
  71. package/src/modules/ai_assistant/i18n/pl.json +17 -0
  72. package/src/modules/ai_assistant/lib/conversation-storage.ts +22 -1
  73. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +25 -0
  74. package/src/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.ts +15 -0
  75. package/src/modules/ai_assistant/notifications.client.ts +29 -0
  76. package/src/modules/ai_assistant/notifications.ts +25 -0
  77. package/src/modules/ai_assistant/setup.ts +2 -1
  78. package/src/modules/ai_assistant/subscribers/__tests__/conversation-shared-notify.test.ts +116 -0
  79. package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +78 -0
  80. package/src/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.tsx +121 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/participants/%5BuserId%5D/route.ts"],
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\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 { hasRequiredFeatures } from '../../../../../../lib/auth'\nimport {\n AiChatConversationAccessError,\n AiChatParticipantNotFoundError,\n createConversationStorage,\n} from '../../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst participantParamsSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n userId: z.string().uuid('userId must be a valid UUID'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Revoke a conversation participant',\n methods: {\n DELETE: {\n operationId: 'aiAssistantRevokeConversationParticipant',\n summary: 'Revoke a participant from a conversation (soft-delete).',\n description:\n 'Soft-deletes the participant row. If no active non-owner participants remain, ' +\n 'the conversation visibility is reset to \"private\". ' +\n 'Only the conversation owner or a manager may revoke participants.',\n responses: [\n {\n status: 204,\n description: 'Participant revoked.',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string; userId: string }>\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 DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return jsonError(401, 'Unauthorized', 'unauthenticated')\n const rawParams = await context.params\n const parseResult = participantParamsSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid path parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n if (!auth.tenantId) return jsonError(404, 'Conversation not found.', 'conversation_not_found')\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n const canShare = hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n try {\n const repo = createConversationStorage(container)\n await repo.revokeParticipant(\n parseResult.data.conversationId,\n parseResult.data.userId,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.unshared',\n {\n conversationId: parseResult.data.conversationId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n ownerUserId: auth.sub,\n participantUserId: parseResult.data.userId,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return new NextResponse(null, { status: 204 })\n } catch (err) {\n if (err instanceof AiChatParticipantNotFoundError) {\n return jsonError(404, err.message || 'Participant not found or already revoked.', 'participant_not_found')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, err.message || 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,271 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import { User } from "@open-mercato/core/modules/auth/data/entities";
7
+ import { hasRequiredFeatures } from "../../../../../lib/auth.js";
8
+ import {
9
+ createConversationStorage,
10
+ AiChatConversationAccessError,
11
+ AiChatConversationDuplicateParticipantError
12
+ } from "../../../../../lib/conversation-storage.js";
13
+ import { emitAiAssistantEvent } from "../../../../../events.js";
14
+ const REQUIRED_FEATURE = "ai_assistant.view";
15
+ const MANAGE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.manage";
16
+ const SHARE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.share";
17
+ const conversationIdParamSchema = z.object({
18
+ conversationId: z.string().trim().min(1, "conversationId must be a non-empty string").max(128, "conversationId exceeds the maximum length of 128 characters")
19
+ });
20
+ const addParticipantBodySchema = z.object({
21
+ userId: z.string().uuid("userId must be a valid UUID"),
22
+ role: z.enum(["viewer"]).default("viewer")
23
+ });
24
+ const openApi = {
25
+ tag: "AI Assistant",
26
+ summary: "Manage conversation participants",
27
+ methods: {
28
+ GET: {
29
+ operationId: "aiAssistantListConversationParticipants",
30
+ summary: "List active participants of a conversation.",
31
+ description: "Returns the list of active (non-revoked) participants for the conversation. Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.",
32
+ responses: [
33
+ {
34
+ status: 200,
35
+ description: "List of active participants.",
36
+ mediaType: "application/json"
37
+ }
38
+ ],
39
+ errors: [
40
+ { status: 401, description: "Unauthenticated caller." },
41
+ { status: 403, description: "Caller lacks required features." },
42
+ { status: 404, description: "Conversation not found or not accessible." }
43
+ ]
44
+ },
45
+ POST: {
46
+ operationId: "aiAssistantAddConversationParticipant",
47
+ summary: "Add a participant to a conversation.",
48
+ description: "Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.",
49
+ responses: [
50
+ {
51
+ status: 201,
52
+ description: 'Participant added; conversation visibility updated to "shared".',
53
+ mediaType: "application/json"
54
+ }
55
+ ],
56
+ errors: [
57
+ { status: 400, description: "Invalid request body." },
58
+ { status: 401, description: "Unauthenticated caller." },
59
+ { status: 403, description: "Caller lacks required feature or is not the owner." },
60
+ { status: 404, description: "Conversation not found." }
61
+ ]
62
+ }
63
+ }
64
+ };
65
+ const metadata = {
66
+ GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },
67
+ POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
68
+ };
69
+ function jsonError(status, message, code, extra) {
70
+ return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
71
+ }
72
+ async function resolveCallerContext(req, context) {
73
+ const auth = await getAuthFromRequest(req);
74
+ if (!auth) return { kind: "unauthorized" };
75
+ const rawParams = await context.params;
76
+ const parseResult = conversationIdParamSchema.safeParse(rawParams);
77
+ if (!parseResult.success) {
78
+ return { kind: "invalid-id", issues: parseResult.error.issues };
79
+ }
80
+ const container = await createRequestContainer();
81
+ const rbacService = container.resolve("rbacService");
82
+ const acl = await rbacService.loadAcl(auth.sub, {
83
+ tenantId: auth.tenantId,
84
+ organizationId: auth.orgId
85
+ });
86
+ if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
87
+ return { kind: "forbidden" };
88
+ }
89
+ if (!auth.tenantId) return { kind: "missing-tenant" };
90
+ return {
91
+ kind: "ok",
92
+ tenantId: auth.tenantId,
93
+ organizationId: auth.orgId ?? null,
94
+ userId: auth.sub,
95
+ conversationId: parseResult.data.conversationId,
96
+ canManageConversations: hasRequiredFeatures(
97
+ [MANAGE_CONVERSATIONS_FEATURE],
98
+ acl.features,
99
+ acl.isSuperAdmin,
100
+ rbacService
101
+ ),
102
+ canShare: hasRequiredFeatures(
103
+ [SHARE_CONVERSATIONS_FEATURE],
104
+ acl.features,
105
+ acl.isSuperAdmin,
106
+ rbacService
107
+ )
108
+ };
109
+ }
110
+ async function GET(req, context) {
111
+ const callerCtx = await resolveCallerContext(req, context);
112
+ if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
113
+ if (callerCtx.kind === "invalid-id") {
114
+ return jsonError(400, "Invalid conversation id.", "validation_error", {
115
+ issues: callerCtx.issues
116
+ });
117
+ }
118
+ if (callerCtx.kind === "forbidden") {
119
+ return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
120
+ }
121
+ if (callerCtx.kind === "missing-tenant") {
122
+ return jsonError(404, "Conversation not found.", "conversation_not_found");
123
+ }
124
+ try {
125
+ const container = await createRequestContainer();
126
+ const repo = createConversationStorage(container);
127
+ const repoCtx = {
128
+ tenantId: callerCtx.tenantId,
129
+ organizationId: callerCtx.organizationId,
130
+ userId: callerCtx.userId,
131
+ canManageConversations: callerCtx.canManageConversations
132
+ };
133
+ const conversation = await repo.getById(callerCtx.conversationId, repoCtx);
134
+ if (!conversation) {
135
+ return jsonError(404, "Conversation not found.", "conversation_not_found");
136
+ }
137
+ const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx);
138
+ return NextResponse.json({
139
+ ownerUserId: conversation.ownerUserId,
140
+ participants: participants.map((p) => ({
141
+ userId: p.userId,
142
+ role: p.role,
143
+ lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,
144
+ addedAt: p.createdAt.toISOString()
145
+ }))
146
+ });
147
+ } catch (err) {
148
+ if (err instanceof AiChatConversationAccessError) {
149
+ return jsonError(403, "Access denied.", "forbidden");
150
+ }
151
+ return jsonError(500, "Internal server error.", "internal_error");
152
+ }
153
+ }
154
+ async function POST(req, context) {
155
+ const callerCtx = await resolveCallerContext(req, context);
156
+ if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
157
+ if (callerCtx.kind === "invalid-id") {
158
+ return jsonError(400, "Invalid conversation id.", "validation_error", {
159
+ issues: callerCtx.issues
160
+ });
161
+ }
162
+ if (callerCtx.kind === "forbidden") {
163
+ return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
164
+ }
165
+ if (callerCtx.kind === "missing-tenant") {
166
+ return jsonError(404, "Conversation not found.", "conversation_not_found");
167
+ }
168
+ if (!callerCtx.canShare) {
169
+ return jsonError(
170
+ 403,
171
+ `Caller lacks required feature "${SHARE_CONVERSATIONS_FEATURE}".`,
172
+ "forbidden"
173
+ );
174
+ }
175
+ let body;
176
+ try {
177
+ body = await req.json();
178
+ } catch {
179
+ return jsonError(400, "Invalid JSON body.", "invalid_body");
180
+ }
181
+ const parseResult = addParticipantBodySchema.safeParse(body);
182
+ if (!parseResult.success) {
183
+ return jsonError(400, "Invalid request body.", "validation_error", {
184
+ issues: parseResult.error.issues
185
+ });
186
+ }
187
+ const targetUserId = parseResult.data.userId;
188
+ if (targetUserId === callerCtx.userId) {
189
+ return jsonError(400, "Cannot share a conversation with yourself.", "self_share_not_allowed");
190
+ }
191
+ try {
192
+ const container = await createRequestContainer();
193
+ const em = container.resolve("em");
194
+ const targetUserFilter = {
195
+ id: targetUserId,
196
+ tenantId: callerCtx.tenantId,
197
+ deletedAt: null,
198
+ ...callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}
199
+ };
200
+ const targetUser = await findOneWithDecryption(
201
+ em,
202
+ User,
203
+ targetUserFilter,
204
+ {},
205
+ { tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId }
206
+ );
207
+ if (!targetUser) {
208
+ return jsonError(
209
+ 400,
210
+ "Target user must be a staff user in the same tenant and organization.",
211
+ "user_not_found"
212
+ );
213
+ }
214
+ const repo = createConversationStorage(container);
215
+ const participant = await repo.addParticipant(
216
+ callerCtx.conversationId,
217
+ targetUserId,
218
+ parseResult.data.role,
219
+ {
220
+ tenantId: callerCtx.tenantId,
221
+ organizationId: callerCtx.organizationId,
222
+ userId: callerCtx.userId,
223
+ canManageConversations: callerCtx.canManageConversations
224
+ }
225
+ );
226
+ try {
227
+ await emitAiAssistantEvent(
228
+ "ai_assistant.conversation.shared",
229
+ {
230
+ conversationId: callerCtx.conversationId,
231
+ tenantId: callerCtx.tenantId,
232
+ organizationId: callerCtx.organizationId,
233
+ ownerUserId: callerCtx.userId,
234
+ participantUserId: participant.userId,
235
+ role: participant.role
236
+ },
237
+ { persistent: false }
238
+ );
239
+ } catch {
240
+ }
241
+ return NextResponse.json(
242
+ {
243
+ participant: {
244
+ userId: participant.userId,
245
+ role: participant.role,
246
+ lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,
247
+ addedAt: participant.createdAt.toISOString()
248
+ }
249
+ },
250
+ { status: 201 }
251
+ );
252
+ } catch (err) {
253
+ if (err instanceof AiChatConversationDuplicateParticipantError) {
254
+ return jsonError(409, err.message, "duplicate_participant");
255
+ }
256
+ if (err instanceof AiChatConversationAccessError) {
257
+ return jsonError(404, "Conversation not found.", "conversation_not_found");
258
+ }
259
+ if (err instanceof Error && err.message.toLowerCase().includes("owner")) {
260
+ return jsonError(403, err.message, "forbidden");
261
+ }
262
+ return jsonError(500, "Internal server error.", "internal_error");
263
+ }
264
+ }
265
+ export {
266
+ GET,
267
+ POST,
268
+ metadata,
269
+ openApi
270
+ };
271
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/participants/route.ts"],
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\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 { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport {\n createConversationStorage,\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n} from '../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nconst addParticipantBodySchema = z.object({\n userId: z.string().uuid('userId must be a valid UUID'),\n role: z.enum(['viewer']).default('viewer'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Manage conversation participants',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversationParticipants',\n summary: 'List active participants of a conversation.',\n description:\n 'Returns the list of active (non-revoked) participants for the conversation. ' +\n 'Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.',\n responses: [\n {\n status: 200,\n description: 'List of active participants.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features.' },\n { status: 404, description: 'Conversation not found or not accessible.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantAddConversationParticipant',\n summary: 'Add a participant to a conversation.',\n description:\n 'Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. ' +\n 'Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.',\n responses: [\n {\n status: 201,\n description: 'Participant added; conversation visibility updated to \"shared\".',\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 required feature or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\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 resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n canShare: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n canShare: hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const repoCtx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n }\n const conversation = await repo.getById(callerCtx.conversationId, repoCtx)\n if (!conversation) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx)\n return NextResponse.json({\n ownerUserId: conversation.ownerUserId,\n participants: participants.map((p) => ({\n userId: p.userId,\n role: p.role,\n lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,\n addedAt: p.createdAt.toISOString(),\n })),\n })\n } catch (err) {\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (!callerCtx.canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return jsonError(400, 'Invalid JSON body.', 'invalid_body')\n }\n const parseResult = addParticipantBodySchema.safeParse(body)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n const targetUserId = parseResult.data.userId\n if (targetUserId === callerCtx.userId) {\n return jsonError(400, 'Cannot share a conversation with yourself.', 'self_share_not_allowed')\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const targetUserFilter: FilterQuery<User> = {\n id: targetUserId,\n tenantId: callerCtx.tenantId,\n deletedAt: null,\n ...(callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}),\n }\n const targetUser = await findOneWithDecryption<User>(\n em,\n User,\n targetUserFilter,\n {},\n { tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId },\n )\n if (!targetUser) {\n return jsonError(\n 400,\n 'Target user must be a staff user in the same tenant and organization.',\n 'user_not_found',\n )\n }\n\n const repo = createConversationStorage(container)\n const participant = await repo.addParticipant(\n callerCtx.conversationId,\n targetUserId,\n parseResult.data.role,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.shared',\n {\n conversationId: callerCtx.conversationId,\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n ownerUserId: callerCtx.userId,\n participantUserId: participant.userId,\n role: participant.role,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return NextResponse.json(\n {\n participant: {\n userId: participant.userId,\n role: participant.role,\n lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,\n addedAt: participant.createdAt.toISOString(),\n },\n },\n { status: 201 },\n )\n } catch (err) {\n if (err instanceof AiChatConversationDuplicateParticipantError) {\n return jsonError(409, err.message, 'duplicate_participant')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (err instanceof Error && err.message.toLowerCase().includes('owner')) {\n return jsonError(403, err.message, 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -160,8 +160,16 @@ async function GET(req, context) {
160
160
  if (!transcript) {
161
161
  return jsonError(404, "Conversation not found.", "conversation_not_found");
162
162
  }
163
+ const participantCount = await repo.getParticipantCount(
164
+ callerCtx.tenantId,
165
+ callerCtx.organizationId,
166
+ callerCtx.conversationId
167
+ );
163
168
  return NextResponse.json({
164
- conversation: serializeAiChatConversation(transcript.conversation),
169
+ conversation: serializeAiChatConversation(transcript.conversation, {
170
+ callerUserId: callerCtx.userId,
171
+ participantCount
172
+ }),
165
173
  messages: transcript.messages.map(serializeAiChatMessage),
166
174
  nextCursor: transcript.nextCursor
167
175
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\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 {\n aiChatConversationTranscriptQuerySchema,\n aiChatConversationUpdateSchema,\n} from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n serializeAiChatMessage,\n} from '../../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-conversation AI chat operations',\n methods: {\n GET: {\n operationId: 'aiAssistantGetConversation',\n summary: 'Fetch a conversation summary and recent transcript.',\n description:\n 'Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. ' +\n 'View-only callers can load only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can load conversations across users in the same ' +\n 'tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor ' +\n 'returns the next older page when paging back through long transcripts.',\n responses: [\n {\n status: 200,\n description: 'Conversation transcript page for the authenticated owner.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path or query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n PATCH: {\n operationId: 'aiAssistantUpdateConversation',\n summary: 'Update an existing conversation.',\n description:\n 'Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting ' +\n '`status` to `closed` archives the conversation while keeping its transcript intact. ' +\n 'View-only callers can update only their own conversations; conversation managers can ' +\n 'update conversations in the same tenant/organization.',\n responses: [\n {\n status: 200,\n description: 'Updated conversation summary.',\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 { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n DELETE: {\n operationId: 'aiAssistantDeleteConversation',\n summary: 'Soft-delete a conversation and its messages.',\n description:\n 'View-only callers can delete only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. ' +\n 'Marks the conversation row and every undeleted message row with a `deleted_at` timestamp ' +\n 'in one transaction. The transcript remains in the database for audit/restore until a future ' +\n 'retention worker hard-deletes it.',\n responses: [\n {\n status: 200,\n description: 'Soft-delete acknowledgment.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n PATCH: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\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 resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\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 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) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n const url = new URL(req.url)\n const queryResult = aiChatConversationTranscriptQuerySchema.safeParse({\n limit: url.searchParams.get('limit') ?? undefined,\n before: url.searchParams.get('before') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const transcript = await repo.getTranscript(\n callerCtx.conversationId,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n limit: queryResult.data.limit,\n before: queryResult.data.before ?? null,\n },\n )\n if (!transcript) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n return NextResponse.json({\n conversation: serializeAiChatConversation(transcript.conversation),\n messages: transcript.messages.map(serializeAiChatMessage),\n nextCursor: transcript.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversation GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function PATCH(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\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 const parseResult = aiChatConversationUpdateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation patch.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const row = await repo.update(\n callerCtx.conversationId,\n parseResult.data,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n return NextResponse.json(serializeAiChatConversation(row))\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation PATCH] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to update conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n await repo.softDelete(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation DELETE] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to delete conversation.',\n 'internal_error',\n )\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,mBAAmB;AACzB,MAAM,+BAA+B;AAErC,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,gBAAgB,EACb,OAAO,EACP,KAAK,EACL,IAAI,GAAG,2CAA2C,EAClD,IAAI,KAAK,6DAA6D;AAC3E,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAKF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,oCAAoC;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,QAC5E,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,MAC1E;AAAA,IACF;AAAA,IACA,OAAO;AAAA,MACL,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAIF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,QACpD,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,QAC5E,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,MAC1E;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAKF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,QAC5E,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAAA,EAC9D,OAAO,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAAA,EAChE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AACnE;AAMA,SAAS,UACP,QACA,SACA,MACA,OACc;AACd,SAAO,aAAa,KAAK,EAAE,OAAO,SAAS,MAAM,GAAI,SAAS,CAAC,EAAG,GAAG,EAAE,OAAO,CAAC;AACjF;AAEA,eAAe,qBAAqB,KAAkB,SAapD;AACA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,eAAe;AACzC,QAAM,YAAY,MAAM,QAAQ;AAChC,QAAM,cAAc,0BAA0B,UAAU,SAAS;AACjE,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,EAAE,MAAM,cAAc,QAAQ,YAAY,MAAM,OAAO;AAAA,EAChE;AACA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,QAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,IAC9C,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AACD,MAAI,CAAC,oBAAoB,CAAC,gBAAgB,GAAG,IAAI,UAAU,IAAI,cAAc,WAAW,GAAG;AACzF,WAAO,EAAE,MAAM,YAAY;AAAA,EAC7B;AACA,QAAM,yBAAyB;AAAA,IAC7B,CAAC,4BAA4B;AAAA,IAC7B,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ;AAAA,EACF;AACA,MAAI,CAAC,KAAK,SAAU,QAAO,EAAE,MAAM,iBAAiB;AACpD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK,SAAS;AAAA,IAC9B,QAAQ,KAAK;AAAA,IACb,gBAAgB,YAAY,KAAK;AAAA,IACjC;AAAA,EACF;AACF;AAEA,eAAsB,IAAI,KAAkB,SAA0C;AACpF,QAAM,YAAY,MAAM,qBAAqB,KAAK,OAAO;AACzD,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,cAAc;AACnC,WAAO,UAAU,KAAK,4BAA4B,oBAAoB;AAAA,MACpE,QAAQ,UAAU;AAAA,IACpB,CAAC;AAAA,EACH;AACA,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,EAC3E;AAEA,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,wCAAwC,UAAU;AAAA,IACpE,OAAO,IAAI,aAAa,IAAI,OAAO,KAAK;AAAA,IACxC,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,EAC5C,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,UAAU,KAAK,6BAA6B,oBAAoB;AAAA,MACrE,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,aAAa,MAAM,KAAK;AAAA,MAC5B,UAAU;AAAA,MACV;AAAA,QACE,UAAU,UAAU;AAAA,QACpB,gBAAgB,UAAU;AAAA,QAC1B,QAAQ,UAAU;AAAA,QAClB,wBAAwB,UAAU;AAAA,MACpC;AAAA,MACA;AAAA,QACE,OAAO,YAAY,KAAK;AAAA,QACxB,QAAQ,YAAY,KAAK,UAAU;AAAA,MACrC;AAAA,IACF;AACA,QAAI,CAAC,YAAY;AACf,aAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,IAC3E;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,cAAc,4BAA4B,WAAW,YAAY;AAAA,MACjE,UAAU,WAAW,SAAS,IAAI,sBAAsB;AAAA,MACxD,YAAY,WAAW;AAAA,IACzB,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,kCAAkC,KAAK;AACrD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,MAAM,KAAkB,SAA0C;AACtF,QAAM,YAAY,MAAM,qBAAqB,KAAK,OAAO;AACzD,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,cAAc;AACnC,WAAO,UAAU,KAAK,4BAA4B,oBAAoB;AAAA,MACpE,QAAQ,UAAU;AAAA,IACpB,CAAC;AAAA,EACH;AACA,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,EAC3E;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,IAAI,KAAK;AAAA,EAC3B,QAAQ;AACN,WAAO,UAAU,KAAK,oCAAoC,kBAAkB;AAAA,EAC9E;AACA,QAAM,cAAc,+BAA+B,UAAU,OAAO;AACpE,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,UAAU,KAAK,+BAA+B,oBAAoB;AAAA,MACvE,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,UAAU;AAAA,MACV,YAAY;AAAA,MACZ;AAAA,QACE,UAAU,UAAU;AAAA,QACpB,gBAAgB,UAAU;AAAA,QAC1B,QAAQ,UAAU;AAAA,QAClB,wBAAwB,UAAU;AAAA,MACpC;AAAA,IACF;AACA,WAAO,aAAa,KAAK,4BAA4B,GAAG,CAAC;AAAA,EAC3D,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,SAAS,iCAAiC;AAC5E,aAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,IAC3E;AACA,YAAQ,MAAM,oCAAoC,KAAK;AACvD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,OAAO,KAAkB,SAA0C;AACvF,QAAM,YAAY,MAAM,qBAAqB,KAAK,OAAO;AACzD,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,cAAc;AACnC,WAAO,UAAU,KAAK,4BAA4B,oBAAoB;AAAA,MACpE,QAAQ,UAAU;AAAA,IACpB,CAAC;AAAA,EACH;AACA,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,EAC3E;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,KAAK,WAAW,UAAU,gBAAgB;AAAA,MAC9C,UAAU,UAAU;AAAA,MACpB,gBAAgB,UAAU;AAAA,MAC1B,QAAQ,UAAU;AAAA,MAClB,wBAAwB,UAAU;AAAA,IACpC,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,SAAS,iCAAiC;AAC5E,aAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,IAC3E;AACA,YAAQ,MAAM,qCAAqC,KAAK;AACxD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\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 {\n aiChatConversationTranscriptQuerySchema,\n aiChatConversationUpdateSchema,\n} from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n serializeAiChatMessage,\n} from '../../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-conversation AI chat operations',\n methods: {\n GET: {\n operationId: 'aiAssistantGetConversation',\n summary: 'Fetch a conversation summary and recent transcript.',\n description:\n 'Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. ' +\n 'View-only callers can load only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can load conversations across users in the same ' +\n 'tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor ' +\n 'returns the next older page when paging back through long transcripts.',\n responses: [\n {\n status: 200,\n description: 'Conversation transcript page for the authenticated owner.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path or query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n PATCH: {\n operationId: 'aiAssistantUpdateConversation',\n summary: 'Update an existing conversation.',\n description:\n 'Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting ' +\n '`status` to `closed` archives the conversation while keeping its transcript intact. ' +\n 'View-only callers can update only their own conversations; conversation managers can ' +\n 'update conversations in the same tenant/organization.',\n responses: [\n {\n status: 200,\n description: 'Updated conversation summary.',\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 { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n DELETE: {\n operationId: 'aiAssistantDeleteConversation',\n summary: 'Soft-delete a conversation and its messages.',\n description:\n 'View-only callers can delete only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. ' +\n 'Marks the conversation row and every undeleted message row with a `deleted_at` timestamp ' +\n 'in one transaction. The transcript remains in the database for audit/restore until a future ' +\n 'retention worker hard-deletes it.',\n responses: [\n {\n status: 200,\n description: 'Soft-delete acknowledgment.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n PATCH: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\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 resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\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 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) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n const url = new URL(req.url)\n const queryResult = aiChatConversationTranscriptQuerySchema.safeParse({\n limit: url.searchParams.get('limit') ?? undefined,\n before: url.searchParams.get('before') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const transcript = await repo.getTranscript(\n callerCtx.conversationId,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n limit: queryResult.data.limit,\n before: queryResult.data.before ?? null,\n },\n )\n if (!transcript) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participantCount = await repo.getParticipantCount(\n callerCtx.tenantId,\n callerCtx.organizationId,\n callerCtx.conversationId,\n )\n return NextResponse.json({\n conversation: serializeAiChatConversation(transcript.conversation, {\n callerUserId: callerCtx.userId,\n participantCount,\n }),\n messages: transcript.messages.map(serializeAiChatMessage),\n nextCursor: transcript.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversation GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function PATCH(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\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 const parseResult = aiChatConversationUpdateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation patch.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const row = await repo.update(\n callerCtx.conversationId,\n parseResult.data,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n return NextResponse.json(serializeAiChatConversation(row))\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation PATCH] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to update conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\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(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n await repo.softDelete(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation DELETE] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to delete conversation.',\n 'internal_error',\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -132,7 +132,7 @@ async function GET(req) {
132
132
  }
133
133
  );
134
134
  return NextResponse.json({
135
- items: result.items.map(serializeAiChatConversation),
135
+ items: result.items.map((row) => serializeAiChatConversation(row)),
136
136
  nextCursor: result.nextCursor
137
137
  });
138
138
  } catch (error) {
@@ -187,6 +187,9 @@ async function POST(req) {
187
187
  const status = beforeRow ? 200 : 201;
188
188
  return NextResponse.json(serializeAiChatConversation(row), { status });
189
189
  } catch (error) {
190
+ if (error instanceof Error && error.name === "AiChatConversationOrgNotFoundError") {
191
+ return jsonError(400, error.message, "organization_not_found");
192
+ }
190
193
  if (error instanceof Error && error.name === "AiChatConversationAccessError") {
191
194
  return jsonError(404, error.message, "conversation_not_found");
192
195
  }
@@ -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(serializeAiChatConversation),\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 === '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": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,mBAAmB;AACzB,MAAM,+BAA+B;AAE9B,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAMF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,4BAA4B;AAAA,QACxD,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,MAC9E;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAGF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,QACpD,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,MAC9E;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAAA,EAC9D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AACjE;AAEA,SAAS,UACP,QACA,SACA,MACA,OACc;AACd,SAAO,aAAa,KAAK,EAAE,OAAO,SAAS,MAAM,GAAI,SAAS,CAAC,EAAG,GAAG,EAAE,OAAO,CAAC;AACjF;AAEA,eAAe,kBAAkB,KAW/B;AACA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,eAAe;AACzC,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,QAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,IAC9C,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AACD,MAAI,CAAC,oBAAoB,CAAC,gBAAgB,GAAG,IAAI,UAAU,IAAI,cAAc,WAAW,GAAG;AACzF,WAAO,EAAE,MAAM,YAAY;AAAA,EAC7B;AACA,QAAM,yBAAyB;AAAA,IAC7B,CAAC,4BAA4B;AAAA,IAC7B,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ;AAAA,EACF;AACA,MAAI,CAAC,KAAK,UAAU;AAClB,WAAO,EAAE,MAAM,iBAAiB;AAAA,EAClC;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK,SAAS;AAAA,IAC9B,QAAQ,KAAK;AAAA,IACb;AAAA,EACF;AACF;AAEA,eAAsB,IAAI,KAAqC;AAC7D,QAAM,YAAY,MAAM,kBAAkB,GAAG;AAC7C,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC;AAAA,EAC1D;AAEA,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,kCAAkC,UAAU;AAAA,IAC9D,OAAO,IAAI,aAAa,IAAI,OAAO,KAAK;AAAA,IACxC,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,IAC1C,OAAO,IAAI,aAAa,IAAI,OAAO,KAAK;AAAA,IACxC,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,EAC5C,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,UAAU,KAAK,6BAA6B,oBAAoB;AAAA,MACrE,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,SAAS,MAAM,KAAK;AAAA,MACxB;AAAA,QACE,UAAU,UAAU;AAAA,QACpB,gBAAgB,UAAU;AAAA,QAC1B,QAAQ,UAAU;AAAA,QAClB,wBAAwB,UAAU;AAAA,MACpC;AAAA,MACA;AAAA,QACE,SAAS,YAAY,KAAK,SAAS;AAAA,QACnC,QAAQ,YAAY,KAAK,UAAU;AAAA,QACnC,OAAO,YAAY,KAAK;AAAA,QACxB,QAAQ,YAAY,KAAK,UAAU;AAAA,MACrC;AAAA,IACF;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,OAAO,MAAM,IAAI,2BAA2B;AAAA,MACnD,YAAY,OAAO;AAAA,IACrB,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,mCAAmC,KAAK;AACtD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,KAAK,KAAqC;AAC9D,QAAM,YAAY,MAAM,kBAAkB,GAAG;AAC7C,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,UAAU,KAAK,oCAAoC,iBAAiB;AAAA,EAC7E;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,IAAI,KAAK;AAAA,EAC3B,QAAQ;AACN,WAAO,UAAU,KAAK,oCAAoC,kBAAkB;AAAA,EAC9E;AAEA,QAAM,cAAc,+BAA+B,UAAU,OAAO;AACpE,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,UAAU,KAAK,iCAAiC,oBAAoB;AAAA,MACzE,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,MAAM;AAAA,MACV,UAAU,UAAU;AAAA,MACpB,gBAAgB,UAAU;AAAA,MAC1B,QAAQ,UAAU;AAAA,MAClB,wBAAwB;AAAA,IAC1B;AACA,UAAM,YAAY,YAAY,KAAK,iBAC/B,MAAM,KAAK,QAAQ,YAAY,KAAK,gBAAgB,GAAG,IACvD;AACJ,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB;AAAA,QACE,gBAAgB,YAAY,KAAK;AAAA,QACjC,SAAS,YAAY,KAAK;AAAA,QAC1B,OAAO,YAAY,KAAK,SAAS;AAAA,QACjC,aAAa,YAAY,KAAK,eAAe;AAAA,MAC/C;AAAA,MACA;AAAA,IACF;AACA,UAAM,SAAS,YAAY,MAAM;AACjC,WAAO,aAAa,KAAK,4BAA4B,GAAG,GAAG,EAAE,OAAO,CAAC;AAAA,EACvE,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,SAAS,iCAAiC;AAC5E,aAAO,UAAU,KAAK,MAAM,SAAS,wBAAwB;AAAA,IAC/D;AACA,YAAQ,MAAM,oCAAoC,KAAK;AACvD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;",
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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",
6
6
  "names": []
7
7
  }
@@ -14,6 +14,7 @@ import { Textarea } from "@open-mercato/ui/primitives/textarea";
14
14
  import { EmptyState } from "@open-mercato/ui/backend/EmptyState";
15
15
  import { apiCall, apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
16
16
  import { AiChat, createAiUiPartRegistry, LoopDisabledBanner, useAiShortcuts } from "@open-mercato/ui/ai";
17
+ import { ConversationShareButton } from "../../../../components/ConversationShareButton.js";
17
18
  async function fetchAgents() {
18
19
  const { result, status } = await apiCallOrThrow(
19
20
  "/api/ai_assistant/ai/agents",
@@ -196,6 +197,7 @@ function ChatLane({ agent, debug }) {
196
197
  [agent]
197
198
  );
198
199
  const [uiParts, setUiParts] = React.useState([]);
200
+ const [conversationId, setConversationId] = React.useState(null);
199
201
  React.useEffect(() => {
200
202
  const seeds = readPlaygroundUiPartSeeds();
201
203
  if (seeds.length > 0) setUiParts(seeds);
@@ -222,7 +224,9 @@ function ChatLane({ agent, debug }) {
222
224
  className: "min-h-96",
223
225
  debugTools,
224
226
  debugPromptSections,
225
- uiParts
227
+ uiParts,
228
+ onConversationIdChange: setConversationId,
229
+ headerActions: conversationId ? /* @__PURE__ */ jsx(ConversationShareButton, { conversationId }) : null
226
230
  },
227
231
  agent.id
228
232
  ) });