@open-mercato/ai-assistant 0.6.2-develop.3446.1.bd060c6017 → 0.6.2-develop.3467.1.2a1818709d
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/ai_assistant/acl.js +1 -0
- package/dist/modules/ai_assistant/acl.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +197 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +272 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/conversations/import/route.js +108 -0
- package/dist/modules/ai_assistant/api/ai/conversations/import/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/conversations/route.js +207 -0
- package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiChatConversation.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiChatConversation.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiChatConversationParticipant.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiChatConversationParticipant.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiChatMessage.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiChatMessage.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +200 -0
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +448 -0
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/validators.js +72 -0
- package/dist/modules/ai_assistant/data/validators.js.map +7 -0
- package/dist/modules/ai_assistant/i18n/de.json +3 -0
- package/dist/modules/ai_assistant/i18n/en.json +3 -0
- package/dist/modules/ai_assistant/i18n/es.json +3 -0
- package/dist/modules/ai_assistant/i18n/pl.json +3 -0
- package/dist/modules/ai_assistant/lib/conversation-storage.js +43 -0
- package/dist/modules/ai_assistant/lib/conversation-storage.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.js +28 -0
- package/dist/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +1 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/generated/entities/ai_chat_conversation/index.ts +15 -0
- package/generated/entities/ai_chat_conversation_participant/index.ts +9 -0
- package/generated/entities/ai_chat_message/index.ts +16 -0
- package/generated/entities.ids.generated.ts +4 -1
- package/generated/entity-fields-registry.ts +46 -0
- package/package.json +6 -6
- package/src/modules/ai_assistant/acl.ts +1 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +107 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +245 -1
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +320 -0
- package/src/modules/ai_assistant/api/ai/conversations/__tests__/route.test.ts +93 -0
- package/src/modules/ai_assistant/api/ai/conversations/import/route.ts +122 -0
- package/src/modules/ai_assistant/api/ai/conversations/route.ts +241 -0
- package/src/modules/ai_assistant/data/entities/AiChatConversation.ts +2 -0
- package/src/modules/ai_assistant/data/entities/AiChatConversationParticipant.ts +2 -0
- package/src/modules/ai_assistant/data/entities/AiChatMessage.ts +2 -0
- package/src/modules/ai_assistant/data/entities.ts +255 -0
- package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +597 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +592 -0
- package/src/modules/ai_assistant/data/validators.ts +134 -0
- package/src/modules/ai_assistant/i18n/de.json +3 -0
- package/src/modules/ai_assistant/i18n/en.json +3 -0
- package/src/modules/ai_assistant/i18n/es.json +3 -0
- package/src/modules/ai_assistant/i18n/pl.json +3 -0
- package/src/modules/ai_assistant/lib/conversation-storage.ts +93 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +822 -0
- package/src/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.ts +39 -0
- package/src/modules/ai_assistant/setup.ts +1 -0
|
@@ -0,0 +1,272 @@
|
|
|
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 {
|
|
6
|
+
aiChatConversationTranscriptQuerySchema,
|
|
7
|
+
aiChatConversationUpdateSchema
|
|
8
|
+
} from "../../../../data/validators.js";
|
|
9
|
+
import { hasRequiredFeatures } from "../../../../lib/auth.js";
|
|
10
|
+
import {
|
|
11
|
+
createConversationStorage,
|
|
12
|
+
serializeAiChatConversation,
|
|
13
|
+
serializeAiChatMessage
|
|
14
|
+
} from "../../../../lib/conversation-storage.js";
|
|
15
|
+
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
16
|
+
const MANAGE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.manage";
|
|
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 openApi = {
|
|
21
|
+
tag: "AI Assistant",
|
|
22
|
+
summary: "Per-conversation AI chat operations",
|
|
23
|
+
methods: {
|
|
24
|
+
GET: {
|
|
25
|
+
operationId: "aiAssistantGetConversation",
|
|
26
|
+
summary: "Fetch a conversation summary and recent transcript.",
|
|
27
|
+
description: "Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. View-only callers can load only their own conversations. Callers with `ai_assistant.conversations.manage` can load conversations across users in the same tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor returns the next older page when paging back through long transcripts.",
|
|
28
|
+
responses: [
|
|
29
|
+
{
|
|
30
|
+
status: 200,
|
|
31
|
+
description: "Conversation transcript page for the authenticated owner.",
|
|
32
|
+
mediaType: "application/json"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
errors: [
|
|
36
|
+
{ status: 400, description: "Invalid path or query parameters." },
|
|
37
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
38
|
+
{ status: 403, description: "Caller lacks the `ai_assistant.view` feature." },
|
|
39
|
+
{ status: 404, description: "No conversation accessible to the caller." }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
PATCH: {
|
|
43
|
+
operationId: "aiAssistantUpdateConversation",
|
|
44
|
+
summary: "Update an existing conversation.",
|
|
45
|
+
description: "Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting `status` to `closed` archives the conversation while keeping its transcript intact. View-only callers can update only their own conversations; conversation managers can update conversations in the same tenant/organization.",
|
|
46
|
+
responses: [
|
|
47
|
+
{
|
|
48
|
+
status: 200,
|
|
49
|
+
description: "Updated conversation summary.",
|
|
50
|
+
mediaType: "application/json"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
errors: [
|
|
54
|
+
{ status: 400, description: "Invalid request body." },
|
|
55
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
56
|
+
{ status: 403, description: "Caller lacks the `ai_assistant.view` feature." },
|
|
57
|
+
{ status: 404, description: "No conversation accessible to the caller." }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
DELETE: {
|
|
61
|
+
operationId: "aiAssistantDeleteConversation",
|
|
62
|
+
summary: "Soft-delete a conversation and its messages.",
|
|
63
|
+
description: "View-only callers can delete only their own conversations. Callers with `ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. Marks the conversation row and every undeleted message row with a `deleted_at` timestamp in one transaction. The transcript remains in the database for audit/restore until a future retention worker hard-deletes it.",
|
|
64
|
+
responses: [
|
|
65
|
+
{
|
|
66
|
+
status: 200,
|
|
67
|
+
description: "Soft-delete acknowledgment.",
|
|
68
|
+
mediaType: "application/json"
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
errors: [
|
|
72
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
73
|
+
{ status: 403, description: "Caller lacks the `ai_assistant.view` feature." },
|
|
74
|
+
{ status: 404, description: "No conversation accessible to the caller." }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const metadata = {
|
|
80
|
+
GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },
|
|
81
|
+
PATCH: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },
|
|
82
|
+
DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
|
|
83
|
+
};
|
|
84
|
+
function jsonError(status, message, code, extra) {
|
|
85
|
+
return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
|
|
86
|
+
}
|
|
87
|
+
async function resolveCallerContext(req, context) {
|
|
88
|
+
const auth = await getAuthFromRequest(req);
|
|
89
|
+
if (!auth) return { kind: "unauthorized" };
|
|
90
|
+
const rawParams = await context.params;
|
|
91
|
+
const parseResult = conversationIdParamSchema.safeParse(rawParams);
|
|
92
|
+
if (!parseResult.success) {
|
|
93
|
+
return { kind: "invalid-id", issues: parseResult.error.issues };
|
|
94
|
+
}
|
|
95
|
+
const container = await createRequestContainer();
|
|
96
|
+
const rbacService = container.resolve("rbacService");
|
|
97
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
98
|
+
tenantId: auth.tenantId,
|
|
99
|
+
organizationId: auth.orgId
|
|
100
|
+
});
|
|
101
|
+
if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
|
|
102
|
+
return { kind: "forbidden" };
|
|
103
|
+
}
|
|
104
|
+
const canManageConversations = hasRequiredFeatures(
|
|
105
|
+
[MANAGE_CONVERSATIONS_FEATURE],
|
|
106
|
+
acl.features,
|
|
107
|
+
acl.isSuperAdmin,
|
|
108
|
+
rbacService
|
|
109
|
+
);
|
|
110
|
+
if (!auth.tenantId) return { kind: "missing-tenant" };
|
|
111
|
+
return {
|
|
112
|
+
kind: "ok",
|
|
113
|
+
tenantId: auth.tenantId,
|
|
114
|
+
organizationId: auth.orgId ?? null,
|
|
115
|
+
userId: auth.sub,
|
|
116
|
+
conversationId: parseResult.data.conversationId,
|
|
117
|
+
canManageConversations
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function GET(req, context) {
|
|
121
|
+
const callerCtx = await resolveCallerContext(req, context);
|
|
122
|
+
if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
|
|
123
|
+
if (callerCtx.kind === "invalid-id") {
|
|
124
|
+
return jsonError(400, "Invalid conversation id.", "validation_error", {
|
|
125
|
+
issues: callerCtx.issues
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (callerCtx.kind === "forbidden") {
|
|
129
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
130
|
+
}
|
|
131
|
+
if (callerCtx.kind === "missing-tenant") {
|
|
132
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
133
|
+
}
|
|
134
|
+
const url = new URL(req.url);
|
|
135
|
+
const queryResult = aiChatConversationTranscriptQuerySchema.safeParse({
|
|
136
|
+
limit: url.searchParams.get("limit") ?? void 0,
|
|
137
|
+
before: url.searchParams.get("before") ?? void 0
|
|
138
|
+
});
|
|
139
|
+
if (!queryResult.success) {
|
|
140
|
+
return jsonError(400, "Invalid query parameters.", "validation_error", {
|
|
141
|
+
issues: queryResult.error.issues
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const container = await createRequestContainer();
|
|
146
|
+
const repo = createConversationStorage(container);
|
|
147
|
+
const transcript = await repo.getTranscript(
|
|
148
|
+
callerCtx.conversationId,
|
|
149
|
+
{
|
|
150
|
+
tenantId: callerCtx.tenantId,
|
|
151
|
+
organizationId: callerCtx.organizationId,
|
|
152
|
+
userId: callerCtx.userId,
|
|
153
|
+
canManageConversations: callerCtx.canManageConversations
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
limit: queryResult.data.limit,
|
|
157
|
+
before: queryResult.data.before ?? null
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
if (!transcript) {
|
|
161
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
162
|
+
}
|
|
163
|
+
return NextResponse.json({
|
|
164
|
+
conversation: serializeAiChatConversation(transcript.conversation),
|
|
165
|
+
messages: transcript.messages.map(serializeAiChatMessage),
|
|
166
|
+
nextCursor: transcript.nextCursor
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error("[AI Conversation GET] Failure:", error);
|
|
170
|
+
return jsonError(
|
|
171
|
+
500,
|
|
172
|
+
error instanceof Error ? error.message : "Failed to load conversation.",
|
|
173
|
+
"internal_error"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function PATCH(req, context) {
|
|
178
|
+
const callerCtx = await resolveCallerContext(req, context);
|
|
179
|
+
if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
|
|
180
|
+
if (callerCtx.kind === "invalid-id") {
|
|
181
|
+
return jsonError(400, "Invalid conversation id.", "validation_error", {
|
|
182
|
+
issues: callerCtx.issues
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (callerCtx.kind === "forbidden") {
|
|
186
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
187
|
+
}
|
|
188
|
+
if (callerCtx.kind === "missing-tenant") {
|
|
189
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
190
|
+
}
|
|
191
|
+
let rawBody;
|
|
192
|
+
try {
|
|
193
|
+
rawBody = await req.json();
|
|
194
|
+
} catch {
|
|
195
|
+
return jsonError(400, "Request body must be valid JSON.", "validation_error");
|
|
196
|
+
}
|
|
197
|
+
const parseResult = aiChatConversationUpdateSchema.safeParse(rawBody);
|
|
198
|
+
if (!parseResult.success) {
|
|
199
|
+
return jsonError(400, "Invalid conversation patch.", "validation_error", {
|
|
200
|
+
issues: parseResult.error.issues
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const container = await createRequestContainer();
|
|
205
|
+
const repo = createConversationStorage(container);
|
|
206
|
+
const row = await repo.update(
|
|
207
|
+
callerCtx.conversationId,
|
|
208
|
+
parseResult.data,
|
|
209
|
+
{
|
|
210
|
+
tenantId: callerCtx.tenantId,
|
|
211
|
+
organizationId: callerCtx.organizationId,
|
|
212
|
+
userId: callerCtx.userId,
|
|
213
|
+
canManageConversations: callerCtx.canManageConversations
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
return NextResponse.json(serializeAiChatConversation(row));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
219
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
220
|
+
}
|
|
221
|
+
console.error("[AI Conversation PATCH] Failure:", error);
|
|
222
|
+
return jsonError(
|
|
223
|
+
500,
|
|
224
|
+
error instanceof Error ? error.message : "Failed to update conversation.",
|
|
225
|
+
"internal_error"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function DELETE(req, context) {
|
|
230
|
+
const callerCtx = await resolveCallerContext(req, context);
|
|
231
|
+
if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
|
|
232
|
+
if (callerCtx.kind === "invalid-id") {
|
|
233
|
+
return jsonError(400, "Invalid conversation id.", "validation_error", {
|
|
234
|
+
issues: callerCtx.issues
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (callerCtx.kind === "forbidden") {
|
|
238
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
239
|
+
}
|
|
240
|
+
if (callerCtx.kind === "missing-tenant") {
|
|
241
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const container = await createRequestContainer();
|
|
245
|
+
const repo = createConversationStorage(container);
|
|
246
|
+
await repo.softDelete(callerCtx.conversationId, {
|
|
247
|
+
tenantId: callerCtx.tenantId,
|
|
248
|
+
organizationId: callerCtx.organizationId,
|
|
249
|
+
userId: callerCtx.userId,
|
|
250
|
+
canManageConversations: callerCtx.canManageConversations
|
|
251
|
+
});
|
|
252
|
+
return NextResponse.json({ ok: true });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
255
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
256
|
+
}
|
|
257
|
+
console.error("[AI Conversation DELETE] Failure:", error);
|
|
258
|
+
return jsonError(
|
|
259
|
+
500,
|
|
260
|
+
error instanceof Error ? error.message : "Failed to delete conversation.",
|
|
261
|
+
"internal_error"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export {
|
|
266
|
+
DELETE,
|
|
267
|
+
GET,
|
|
268
|
+
PATCH,
|
|
269
|
+
metadata,
|
|
270
|
+
openApi
|
|
271
|
+
};
|
|
272
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import { aiChatConversationImportSchema } from "../../../../data/validators.js";
|
|
5
|
+
import { hasRequiredFeatures } from "../../../../lib/auth.js";
|
|
6
|
+
import {
|
|
7
|
+
createConversationStorage,
|
|
8
|
+
serializeAiChatConversation
|
|
9
|
+
} from "../../../../lib/conversation-storage.js";
|
|
10
|
+
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
11
|
+
const openApi = {
|
|
12
|
+
tag: "AI Assistant",
|
|
13
|
+
summary: "Lazily import a localStorage AI chat conversation",
|
|
14
|
+
methods: {
|
|
15
|
+
POST: {
|
|
16
|
+
operationId: "aiAssistantImportConversation",
|
|
17
|
+
summary: "Import a conversation that previously lived only in browser localStorage.",
|
|
18
|
+
description: "Idempotent: messages with `clientMessageId` already present in the server transcript are skipped and counted in `skippedMessageCount`. New messages are appended with the original `clientMessageId` so subsequent retries continue to dedupe. Up to 100 messages per request. Attachment previews stored as `data:` URLs in the source localStorage record MUST NOT be forwarded to this endpoint; the UI strips them before upload.",
|
|
19
|
+
responses: [
|
|
20
|
+
{
|
|
21
|
+
status: 200,
|
|
22
|
+
description: "Import result including imported/skipped counters.",
|
|
23
|
+
mediaType: "application/json"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
errors: [
|
|
27
|
+
{ status: 400, description: "Invalid request body." },
|
|
28
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
29
|
+
{ status: 403, description: "Caller lacks the `ai_assistant.view` feature." }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const metadata = {
|
|
35
|
+
POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
|
|
36
|
+
};
|
|
37
|
+
function jsonError(status, message, code, extra) {
|
|
38
|
+
return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
|
|
39
|
+
}
|
|
40
|
+
async function POST(req) {
|
|
41
|
+
const auth = await getAuthFromRequest(req);
|
|
42
|
+
if (!auth) return jsonError(401, "Unauthorized", "unauthenticated");
|
|
43
|
+
let rawBody;
|
|
44
|
+
try {
|
|
45
|
+
rawBody = await req.json();
|
|
46
|
+
} catch {
|
|
47
|
+
return jsonError(400, "Request body must be valid JSON.", "validation_error");
|
|
48
|
+
}
|
|
49
|
+
const parseResult = aiChatConversationImportSchema.safeParse(rawBody);
|
|
50
|
+
if (!parseResult.success) {
|
|
51
|
+
return jsonError(400, "Invalid import payload.", "validation_error", {
|
|
52
|
+
issues: parseResult.error.issues
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const container = await createRequestContainer();
|
|
57
|
+
const rbacService = container.resolve("rbacService");
|
|
58
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
59
|
+
tenantId: auth.tenantId,
|
|
60
|
+
organizationId: auth.orgId
|
|
61
|
+
});
|
|
62
|
+
if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
|
|
63
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
64
|
+
}
|
|
65
|
+
if (!auth.tenantId) {
|
|
66
|
+
return jsonError(400, "Caller is not bound to a tenant.", "tenant_required");
|
|
67
|
+
}
|
|
68
|
+
const repo = createConversationStorage(container);
|
|
69
|
+
const result = await repo.importLocalConversation(
|
|
70
|
+
{
|
|
71
|
+
conversation: {
|
|
72
|
+
conversationId: parseResult.data.conversation.conversationId,
|
|
73
|
+
agentId: parseResult.data.conversation.agentId,
|
|
74
|
+
title: parseResult.data.conversation.title ?? null,
|
|
75
|
+
status: parseResult.data.conversation.status,
|
|
76
|
+
pageContext: parseResult.data.conversation.pageContext ?? null
|
|
77
|
+
},
|
|
78
|
+
messages: parseResult.data.messages
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
tenantId: auth.tenantId,
|
|
82
|
+
organizationId: auth.orgId ?? null,
|
|
83
|
+
userId: auth.sub
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
return NextResponse.json({
|
|
87
|
+
conversation: serializeAiChatConversation(result.conversation),
|
|
88
|
+
importedMessageCount: result.importedMessageCount,
|
|
89
|
+
skippedMessageCount: result.skippedMessageCount
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
93
|
+
return jsonError(404, error.message, "conversation_not_found");
|
|
94
|
+
}
|
|
95
|
+
console.error("[AI Conversation Import] Failure:", error);
|
|
96
|
+
return jsonError(
|
|
97
|
+
500,
|
|
98
|
+
error instanceof Error ? error.message : "Failed to import conversation.",
|
|
99
|
+
"internal_error"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
POST,
|
|
105
|
+
metadata,
|
|
106
|
+
openApi
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,sCAAsC;AAC/C,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,mBAAmB;AAElB,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,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,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,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,eAAsB,KAAK,KAAqC;AAC9D,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAElE,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,2BAA2B,oBAAoB;AAAA,MACnE,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AACD,QAAI,CAAC,oBAAoB,CAAC,gBAAgB,GAAG,IAAI,UAAU,IAAI,cAAc,WAAW,GAAG;AACzF,aAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,IAC3F;AACA,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,UAAU,KAAK,oCAAoC,iBAAiB;AAAA,IAC7E;AAEA,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,SAAS,MAAM,KAAK;AAAA,MACxB;AAAA,QACE,cAAc;AAAA,UACZ,gBAAgB,YAAY,KAAK,aAAa;AAAA,UAC9C,SAAS,YAAY,KAAK,aAAa;AAAA,UACvC,OAAO,YAAY,KAAK,aAAa,SAAS;AAAA,UAC9C,QAAQ,YAAY,KAAK,aAAa;AAAA,UACtC,aAAa,YAAY,KAAK,aAAa,eAAe;AAAA,QAC5D;AAAA,QACA,UAAU,YAAY,KAAK;AAAA,MAC7B;AAAA,MACA;AAAA,QACE,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK,SAAS;AAAA,QAC9B,QAAQ,KAAK;AAAA,MACf;AAAA,IACF;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,cAAc,4BAA4B,OAAO,YAAY;AAAA,MAC7D,sBAAsB,OAAO;AAAA,MAC7B,qBAAqB,OAAO;AAAA,IAC9B,CAAC;AAAA,EACH,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,SAAS,iCAAiC;AAC5E,aAAO,UAAU,KAAK,MAAM,SAAS,wBAAwB;AAAA,IAC/D;AACA,YAAQ,MAAM,qCAAqC,KAAK;AACxD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import {
|
|
5
|
+
aiChatConversationCreateSchema,
|
|
6
|
+
aiChatConversationListQuerySchema
|
|
7
|
+
} from "../../../data/validators.js";
|
|
8
|
+
import { hasRequiredFeatures } from "../../../lib/auth.js";
|
|
9
|
+
import {
|
|
10
|
+
createConversationStorage,
|
|
11
|
+
serializeAiChatConversation
|
|
12
|
+
} from "../../../lib/conversation-storage.js";
|
|
13
|
+
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
14
|
+
const MANAGE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.manage";
|
|
15
|
+
const openApi = {
|
|
16
|
+
tag: "AI Assistant",
|
|
17
|
+
summary: "Server-side AI chat conversations",
|
|
18
|
+
methods: {
|
|
19
|
+
GET: {
|
|
20
|
+
operationId: "aiAssistantListConversations",
|
|
21
|
+
summary: "List AI chat conversations visible to the caller.",
|
|
22
|
+
description: "Returns `{ items, nextCursor }` for the authenticated caller, ordered by `lastMessageAt` descending. View-only callers receive only their own conversations. Callers with `ai_assistant.conversations.manage` may list conversations across users in the same tenant/organization. The `agent` and `status` filters are optional; `cursor` is the ISO timestamp returned by a previous response.",
|
|
23
|
+
responses: [
|
|
24
|
+
{
|
|
25
|
+
status: 200,
|
|
26
|
+
description: "Caller-owned conversation summaries.",
|
|
27
|
+
mediaType: "application/json"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
errors: [
|
|
31
|
+
{ status: 400, description: "Invalid query parameters." },
|
|
32
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
33
|
+
{ status: 403, description: "Caller lacks the `ai_assistant.view` feature." }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
POST: {
|
|
37
|
+
operationId: "aiAssistantCreateConversation",
|
|
38
|
+
summary: "Idempotently create a new AI chat conversation.",
|
|
39
|
+
description: "If a non-deleted conversation already exists with the supplied `conversationId` for the authenticated caller in this tenant/org, returns the existing summary. Otherwise creates a fresh row and writes the owner-participant row in the same transaction.",
|
|
40
|
+
responses: [
|
|
41
|
+
{
|
|
42
|
+
status: 200,
|
|
43
|
+
description: "Existing conversation (idempotent path).",
|
|
44
|
+
mediaType: "application/json"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
status: 201,
|
|
48
|
+
description: "Newly created conversation.",
|
|
49
|
+
mediaType: "application/json"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
errors: [
|
|
53
|
+
{ status: 400, description: "Invalid request body." },
|
|
54
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
55
|
+
{ status: 403, description: "Caller lacks the `ai_assistant.view` feature." }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const metadata = {
|
|
61
|
+
GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },
|
|
62
|
+
POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
|
|
63
|
+
};
|
|
64
|
+
function jsonError(status, message, code, extra) {
|
|
65
|
+
return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
|
|
66
|
+
}
|
|
67
|
+
async function loadCallerContext(req) {
|
|
68
|
+
const auth = await getAuthFromRequest(req);
|
|
69
|
+
if (!auth) return { kind: "unauthorized" };
|
|
70
|
+
const container = await createRequestContainer();
|
|
71
|
+
const rbacService = container.resolve("rbacService");
|
|
72
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
73
|
+
tenantId: auth.tenantId,
|
|
74
|
+
organizationId: auth.orgId
|
|
75
|
+
});
|
|
76
|
+
if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
|
|
77
|
+
return { kind: "forbidden" };
|
|
78
|
+
}
|
|
79
|
+
const canManageConversations = hasRequiredFeatures(
|
|
80
|
+
[MANAGE_CONVERSATIONS_FEATURE],
|
|
81
|
+
acl.features,
|
|
82
|
+
acl.isSuperAdmin,
|
|
83
|
+
rbacService
|
|
84
|
+
);
|
|
85
|
+
if (!auth.tenantId) {
|
|
86
|
+
return { kind: "missing-tenant" };
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
kind: "ok",
|
|
90
|
+
tenantId: auth.tenantId,
|
|
91
|
+
organizationId: auth.orgId ?? null,
|
|
92
|
+
userId: auth.sub,
|
|
93
|
+
canManageConversations
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function GET(req) {
|
|
97
|
+
const callerCtx = await loadCallerContext(req);
|
|
98
|
+
if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
|
|
99
|
+
if (callerCtx.kind === "forbidden") {
|
|
100
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
101
|
+
}
|
|
102
|
+
if (callerCtx.kind === "missing-tenant") {
|
|
103
|
+
return NextResponse.json({ items: [], nextCursor: null });
|
|
104
|
+
}
|
|
105
|
+
const url = new URL(req.url);
|
|
106
|
+
const parseResult = aiChatConversationListQuerySchema.safeParse({
|
|
107
|
+
agent: url.searchParams.get("agent") ?? void 0,
|
|
108
|
+
status: url.searchParams.get("status") ?? void 0,
|
|
109
|
+
limit: url.searchParams.get("limit") ?? void 0,
|
|
110
|
+
cursor: url.searchParams.get("cursor") ?? void 0
|
|
111
|
+
});
|
|
112
|
+
if (!parseResult.success) {
|
|
113
|
+
return jsonError(400, "Invalid query parameters.", "validation_error", {
|
|
114
|
+
issues: parseResult.error.issues
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const container = await createRequestContainer();
|
|
119
|
+
const repo = createConversationStorage(container);
|
|
120
|
+
const result = await repo.list(
|
|
121
|
+
{
|
|
122
|
+
tenantId: callerCtx.tenantId,
|
|
123
|
+
organizationId: callerCtx.organizationId,
|
|
124
|
+
userId: callerCtx.userId,
|
|
125
|
+
canManageConversations: callerCtx.canManageConversations
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
agentId: parseResult.data.agent ?? null,
|
|
129
|
+
status: parseResult.data.status ?? null,
|
|
130
|
+
limit: parseResult.data.limit,
|
|
131
|
+
cursor: parseResult.data.cursor ?? null
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
return NextResponse.json({
|
|
135
|
+
items: result.items.map(serializeAiChatConversation),
|
|
136
|
+
nextCursor: result.nextCursor
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("[AI Conversations GET] Failure:", error);
|
|
140
|
+
return jsonError(
|
|
141
|
+
500,
|
|
142
|
+
error instanceof Error ? error.message : "Failed to list conversations.",
|
|
143
|
+
"internal_error"
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function POST(req) {
|
|
148
|
+
const callerCtx = await loadCallerContext(req);
|
|
149
|
+
if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
|
|
150
|
+
if (callerCtx.kind === "forbidden") {
|
|
151
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
152
|
+
}
|
|
153
|
+
if (callerCtx.kind === "missing-tenant") {
|
|
154
|
+
return jsonError(400, "Caller is not bound to a tenant.", "tenant_required");
|
|
155
|
+
}
|
|
156
|
+
let rawBody;
|
|
157
|
+
try {
|
|
158
|
+
rawBody = await req.json();
|
|
159
|
+
} catch {
|
|
160
|
+
return jsonError(400, "Request body must be valid JSON.", "validation_error");
|
|
161
|
+
}
|
|
162
|
+
const parseResult = aiChatConversationCreateSchema.safeParse(rawBody);
|
|
163
|
+
if (!parseResult.success) {
|
|
164
|
+
return jsonError(400, "Invalid conversation payload.", "validation_error", {
|
|
165
|
+
issues: parseResult.error.issues
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const container = await createRequestContainer();
|
|
170
|
+
const repo = createConversationStorage(container);
|
|
171
|
+
const ctx = {
|
|
172
|
+
tenantId: callerCtx.tenantId,
|
|
173
|
+
organizationId: callerCtx.organizationId,
|
|
174
|
+
userId: callerCtx.userId,
|
|
175
|
+
canManageConversations: false
|
|
176
|
+
};
|
|
177
|
+
const beforeRow = parseResult.data.conversationId ? await repo.getById(parseResult.data.conversationId, ctx) : null;
|
|
178
|
+
const row = await repo.createOrGet(
|
|
179
|
+
{
|
|
180
|
+
conversationId: parseResult.data.conversationId,
|
|
181
|
+
agentId: parseResult.data.agentId,
|
|
182
|
+
title: parseResult.data.title ?? null,
|
|
183
|
+
pageContext: parseResult.data.pageContext ?? null
|
|
184
|
+
},
|
|
185
|
+
ctx
|
|
186
|
+
);
|
|
187
|
+
const status = beforeRow ? 200 : 201;
|
|
188
|
+
return NextResponse.json(serializeAiChatConversation(row), { status });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
191
|
+
return jsonError(404, error.message, "conversation_not_found");
|
|
192
|
+
}
|
|
193
|
+
console.error("[AI Conversations POST] Failure:", error);
|
|
194
|
+
return jsonError(
|
|
195
|
+
500,
|
|
196
|
+
error instanceof Error ? error.message : "Failed to create conversation.",
|
|
197
|
+
"internal_error"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export {
|
|
202
|
+
GET,
|
|
203
|
+
POST,
|
|
204
|
+
metadata,
|
|
205
|
+
openApi
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=route.js.map
|