@open-mercato/ai-assistant 0.6.3-develop.3901.1.ddad60693a → 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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js +87 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js +119 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js.map +7 -0
- 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 +3 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js +128 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js +271 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +9 -1
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/route.js +4 -1
- package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +5 -1
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/components/ConversationShareButton.js +5 -0
- package/dist/modules/ai_assistant/components/ConversationShareButton.js.map +7 -0
- package/dist/modules/ai_assistant/components/ConversationShareDialog.js +5 -0
- package/dist/modules/ai_assistant/components/ConversationShareDialog.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +3 -0
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +235 -5
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +2 -2
- package/dist/modules/ai_assistant/events.js +14 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +17 -0
- package/dist/modules/ai_assistant/i18n/en.json +17 -0
- package/dist/modules/ai_assistant/i18n/es.json +17 -0
- package/dist/modules/ai_assistant/i18n/pl.json +17 -0
- package/dist/modules/ai_assistant/lib/conversation-storage.js +12 -3
- package/dist/modules/ai_assistant/lib/conversation-storage.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js +15 -0
- package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js.map +7 -0
- package/dist/modules/ai_assistant/notifications.client.js +30 -0
- package/dist/modules/ai_assistant/notifications.client.js.map +7 -0
- package/dist/modules/ai_assistant/notifications.js +27 -0
- package/dist/modules/ai_assistant/notifications.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +2 -1
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +59 -0
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +7 -0
- package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js +123 -0
- package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js.map +7 -0
- package/generated/entities/ai_chat_conversation_participant/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -8
- package/src/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.ts +117 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.ts +159 -0
- package/src/modules/ai_assistant/__tests__/integration/ai-chat-sharing.test.ts +406 -0
- package/src/modules/ai_assistant/acl.ts +1 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +3 -0
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.ts +149 -0
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.ts +314 -0
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +9 -1
- package/src/modules/ai_assistant/api/ai/conversations/route.ts +4 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +4 -0
- package/src/modules/ai_assistant/components/ConversationShareButton.tsx +1 -0
- package/src/modules/ai_assistant/components/ConversationShareDialog.tsx +1 -0
- package/src/modules/ai_assistant/data/entities.ts +4 -0
- package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +270 -7
- package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +297 -3
- package/src/modules/ai_assistant/events.ts +31 -0
- package/src/modules/ai_assistant/i18n/__tests__/conversation-share-translations.test.ts +59 -0
- package/src/modules/ai_assistant/i18n/de.json +17 -0
- package/src/modules/ai_assistant/i18n/en.json +17 -0
- package/src/modules/ai_assistant/i18n/es.json +17 -0
- package/src/modules/ai_assistant/i18n/pl.json +17 -0
- package/src/modules/ai_assistant/lib/conversation-storage.ts +22 -1
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +25 -0
- package/src/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.ts +15 -0
- package/src/modules/ai_assistant/notifications.client.ts +29 -0
- package/src/modules/ai_assistant/notifications.ts +25 -0
- package/src/modules/ai_assistant/setup.ts +2 -1
- package/src/modules/ai_assistant/subscribers/__tests__/conversation-shared-notify.test.ts +116 -0
- package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +78 -0
- package/src/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.tsx +121 -0
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
findOneWithDecryption,
|
|
3
3
|
findWithDecryption
|
|
4
4
|
} from "@open-mercato/shared/lib/encryption/find";
|
|
5
|
+
import { Organization } from "@open-mercato/core/modules/directory/data/entities";
|
|
5
6
|
import {
|
|
6
7
|
AiChatConversation,
|
|
7
8
|
AiChatConversationParticipant,
|
|
@@ -17,6 +18,24 @@ class AiChatConversationAccessError extends Error {
|
|
|
17
18
|
this.name = "AiChatConversationAccessError";
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
class AiChatConversationDuplicateParticipantError extends Error {
|
|
22
|
+
constructor(message = "User is already an active participant in this conversation.") {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "AiChatConversationDuplicateParticipantError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
class AiChatParticipantNotFoundError extends Error {
|
|
28
|
+
constructor(message = "Participant not found or already revoked.") {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "AiChatParticipantNotFoundError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class AiChatConversationOrgNotFoundError extends Error {
|
|
34
|
+
constructor(message = "Organization does not exist or is inactive for this tenant.") {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "AiChatConversationOrgNotFoundError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
20
39
|
class AiChatConversationRepository {
|
|
21
40
|
constructor(em) {
|
|
22
41
|
this.em = em;
|
|
@@ -46,6 +65,7 @@ class AiChatConversationRepository {
|
|
|
46
65
|
}
|
|
47
66
|
return existing;
|
|
48
67
|
}
|
|
68
|
+
await assertOrganizationExists(tx, ctx);
|
|
49
69
|
const conversation = tx.create(AiChatConversation, {
|
|
50
70
|
tenantId: ctx.tenantId,
|
|
51
71
|
organizationId: ctx.organizationId ?? null,
|
|
@@ -82,10 +102,17 @@ class AiChatConversationRepository {
|
|
|
82
102
|
if (!conversationId) return null;
|
|
83
103
|
const row = await findOneAccessibleConversation(this.em, conversationId, ctx);
|
|
84
104
|
if (!row) return null;
|
|
85
|
-
|
|
105
|
+
const isParticipant = !canManageConversations(ctx) && row.ownerUserId !== ctx.userId ? await this.loadParticipantFlag(
|
|
106
|
+
this.em,
|
|
107
|
+
ctx.tenantId,
|
|
108
|
+
ctx.organizationId,
|
|
109
|
+
row.conversationId,
|
|
110
|
+
ctx.userId
|
|
111
|
+
) : false;
|
|
112
|
+
if (!canAccessConversation(row, ctx, isParticipant)) return null;
|
|
86
113
|
return row;
|
|
87
114
|
}
|
|
88
|
-
/** Owner-scoped list unless the caller has tenant/org manage access. */
|
|
115
|
+
/** Owner-scoped list unless the caller has tenant/org manage access. Participants also see shared conversations. */
|
|
89
116
|
async list(ctx, options = {}) {
|
|
90
117
|
assertContext(ctx, "list");
|
|
91
118
|
const limit = clampLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
|
|
@@ -94,7 +121,30 @@ class AiChatConversationRepository {
|
|
|
94
121
|
organizationId: ctx.organizationId ?? null,
|
|
95
122
|
deletedAt: null
|
|
96
123
|
};
|
|
97
|
-
if (!canManageConversations(ctx))
|
|
124
|
+
if (!canManageConversations(ctx)) {
|
|
125
|
+
const participantFilter = {
|
|
126
|
+
tenantId: ctx.tenantId,
|
|
127
|
+
userId: ctx.userId,
|
|
128
|
+
deletedAt: null,
|
|
129
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
130
|
+
};
|
|
131
|
+
const participantRows = await findWithDecryption(
|
|
132
|
+
this.em,
|
|
133
|
+
AiChatConversationParticipant,
|
|
134
|
+
participantFilter,
|
|
135
|
+
{ fields: ["conversationId"] },
|
|
136
|
+
{ tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null }
|
|
137
|
+
);
|
|
138
|
+
const participantConvIds = participantRows.map((p) => p.conversationId);
|
|
139
|
+
if (participantConvIds.length > 0) {
|
|
140
|
+
where.$or = [
|
|
141
|
+
{ ownerUserId: ctx.userId },
|
|
142
|
+
{ conversationId: { $in: participantConvIds } }
|
|
143
|
+
];
|
|
144
|
+
} else {
|
|
145
|
+
where.ownerUserId = ctx.userId;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
98
148
|
if (options.agentId) where.agentId = options.agentId;
|
|
99
149
|
if (options.status) where.status = options.status;
|
|
100
150
|
if (options.cursor) {
|
|
@@ -382,6 +432,160 @@ class AiChatConversationRepository {
|
|
|
382
432
|
skippedMessageCount: skipped
|
|
383
433
|
};
|
|
384
434
|
}
|
|
435
|
+
async listParticipants(conversationId, ctx) {
|
|
436
|
+
assertContext(ctx, "listParticipants");
|
|
437
|
+
const conv = await findOneAccessibleConversation(this.em, conversationId, ctx);
|
|
438
|
+
if (!conv) {
|
|
439
|
+
throw new AiChatConversationAccessError(
|
|
440
|
+
`Conversation "${conversationId}" was not found for the caller.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if (conv.ownerUserId !== ctx.userId && !canManageConversations(ctx)) {
|
|
444
|
+
throw new AiChatConversationAccessError(
|
|
445
|
+
"Only the conversation owner or a manager can list participants."
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
const filter = {
|
|
449
|
+
tenantId: ctx.tenantId,
|
|
450
|
+
conversationId,
|
|
451
|
+
deletedAt: null,
|
|
452
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
453
|
+
};
|
|
454
|
+
return findWithDecryption(
|
|
455
|
+
this.em,
|
|
456
|
+
AiChatConversationParticipant,
|
|
457
|
+
filter,
|
|
458
|
+
{ orderBy: { createdAt: "asc" } },
|
|
459
|
+
{ tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null }
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
async addParticipant(conversationId, userId, role, ctx) {
|
|
463
|
+
assertContext(ctx, "addParticipant");
|
|
464
|
+
return this.em.transactional(async (tx) => {
|
|
465
|
+
const conv = await findOneAccessibleConversation(
|
|
466
|
+
tx,
|
|
467
|
+
conversationId,
|
|
468
|
+
ctx
|
|
469
|
+
);
|
|
470
|
+
if (!conv) {
|
|
471
|
+
throw new AiChatConversationAccessError(
|
|
472
|
+
`Conversation "${conversationId}" was not found for the caller.`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (conv.ownerUserId !== ctx.userId) {
|
|
476
|
+
throw new AiChatConversationAccessError(
|
|
477
|
+
"Only the conversation owner can add participants."
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const existingFilter = {
|
|
481
|
+
tenantId: ctx.tenantId,
|
|
482
|
+
conversationId,
|
|
483
|
+
userId,
|
|
484
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
485
|
+
};
|
|
486
|
+
const existing = await findOneWithDecryption(
|
|
487
|
+
tx,
|
|
488
|
+
AiChatConversationParticipant,
|
|
489
|
+
existingFilter
|
|
490
|
+
);
|
|
491
|
+
if (existing) {
|
|
492
|
+
if (existing.deletedAt === null) {
|
|
493
|
+
throw new AiChatConversationDuplicateParticipantError();
|
|
494
|
+
}
|
|
495
|
+
existing.deletedAt = null;
|
|
496
|
+
existing.role = role;
|
|
497
|
+
await tx.persist(existing).flush();
|
|
498
|
+
if (conv.visibility === "private") {
|
|
499
|
+
conv.visibility = "shared";
|
|
500
|
+
await tx.persist(conv).flush();
|
|
501
|
+
}
|
|
502
|
+
return existing;
|
|
503
|
+
}
|
|
504
|
+
const participant = tx.create(AiChatConversationParticipant, {
|
|
505
|
+
tenantId: ctx.tenantId,
|
|
506
|
+
organizationId: ctx.organizationId ?? null,
|
|
507
|
+
conversationId,
|
|
508
|
+
userId,
|
|
509
|
+
role
|
|
510
|
+
});
|
|
511
|
+
if (conv.visibility === "private") {
|
|
512
|
+
conv.visibility = "shared";
|
|
513
|
+
}
|
|
514
|
+
await tx.persist(participant).persist(conv).flush();
|
|
515
|
+
return participant;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async revokeParticipant(conversationId, targetUserId, ctx) {
|
|
519
|
+
assertContext(ctx, "revokeParticipant");
|
|
520
|
+
await this.em.transactional(async (tx) => {
|
|
521
|
+
const conv = await findOneAccessibleConversation(
|
|
522
|
+
tx,
|
|
523
|
+
conversationId,
|
|
524
|
+
ctx
|
|
525
|
+
);
|
|
526
|
+
if (!conv) {
|
|
527
|
+
throw new AiChatConversationAccessError(
|
|
528
|
+
`Conversation "${conversationId}" was not found for the caller.`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (conv.ownerUserId !== ctx.userId) {
|
|
532
|
+
throw new AiChatConversationAccessError(
|
|
533
|
+
"Only the conversation owner can revoke participants."
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (targetUserId === conv.ownerUserId) {
|
|
537
|
+
throw new AiChatConversationAccessError("Cannot revoke the conversation owner.");
|
|
538
|
+
}
|
|
539
|
+
const participantFilter = {
|
|
540
|
+
tenantId: ctx.tenantId,
|
|
541
|
+
conversationId,
|
|
542
|
+
userId: targetUserId,
|
|
543
|
+
deletedAt: null,
|
|
544
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
545
|
+
};
|
|
546
|
+
const participant = await findOneWithDecryption(
|
|
547
|
+
tx,
|
|
548
|
+
AiChatConversationParticipant,
|
|
549
|
+
participantFilter
|
|
550
|
+
);
|
|
551
|
+
if (!participant) throw new AiChatParticipantNotFoundError();
|
|
552
|
+
participant.deletedAt = /* @__PURE__ */ new Date();
|
|
553
|
+
const remainingCount = await tx.count(AiChatConversationParticipant, {
|
|
554
|
+
tenantId: ctx.tenantId,
|
|
555
|
+
conversationId,
|
|
556
|
+
deletedAt: null,
|
|
557
|
+
role: { $ne: "owner" }
|
|
558
|
+
});
|
|
559
|
+
if (remainingCount <= 1) {
|
|
560
|
+
conv.visibility = "private";
|
|
561
|
+
await tx.persist(conv);
|
|
562
|
+
}
|
|
563
|
+
await tx.persist(participant).flush();
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
async getParticipantCount(tenantId, organizationId, conversationId) {
|
|
567
|
+
return this.em.count(AiChatConversationParticipant, {
|
|
568
|
+
tenantId,
|
|
569
|
+
conversationId,
|
|
570
|
+
deletedAt: null,
|
|
571
|
+
role: { $ne: "owner" },
|
|
572
|
+
...organizationId ? { organizationId } : {}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async loadParticipantFlag(em, tenantId, organizationId, conversationId, userId) {
|
|
576
|
+
const row = await findOneWithDecryption(
|
|
577
|
+
em,
|
|
578
|
+
AiChatConversationParticipant,
|
|
579
|
+
{
|
|
580
|
+
tenantId,
|
|
581
|
+
conversationId,
|
|
582
|
+
userId,
|
|
583
|
+
deletedAt: null,
|
|
584
|
+
...organizationId ? { organizationId } : {}
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
return row !== null;
|
|
588
|
+
}
|
|
385
589
|
}
|
|
386
590
|
function assertContext(ctx, method) {
|
|
387
591
|
if (!ctx?.tenantId) {
|
|
@@ -394,8 +598,31 @@ function assertContext(ctx, method) {
|
|
|
394
598
|
function canManageConversations(ctx) {
|
|
395
599
|
return ctx.canManageConversations === true;
|
|
396
600
|
}
|
|
397
|
-
function canAccessConversation(row, ctx) {
|
|
398
|
-
return canManageConversations(ctx) || row.ownerUserId === ctx.userId;
|
|
601
|
+
function canAccessConversation(row, ctx, isParticipant = false) {
|
|
602
|
+
return canManageConversations(ctx) || row.ownerUserId === ctx.userId || isParticipant;
|
|
603
|
+
}
|
|
604
|
+
async function assertOrganizationExists(em, ctx) {
|
|
605
|
+
if (!ctx.organizationId) return;
|
|
606
|
+
const org = await findOneWithDecryption(
|
|
607
|
+
em,
|
|
608
|
+
Organization,
|
|
609
|
+
{
|
|
610
|
+
id: ctx.organizationId,
|
|
611
|
+
tenant: ctx.tenantId,
|
|
612
|
+
deletedAt: null,
|
|
613
|
+
isActive: true
|
|
614
|
+
},
|
|
615
|
+
{},
|
|
616
|
+
{
|
|
617
|
+
tenantId: ctx.tenantId ?? null,
|
|
618
|
+
organizationId: ctx.organizationId ?? null
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
if (!org) {
|
|
622
|
+
throw new AiChatConversationOrgNotFoundError(
|
|
623
|
+
`Organization "${ctx.organizationId}" does not exist or is inactive in tenant "${ctx.tenantId}".`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
399
626
|
}
|
|
400
627
|
async function findOneAccessibleConversation(em, conversationId, ctx) {
|
|
401
628
|
const row = await findOneWithDecryption(
|
|
@@ -442,7 +669,10 @@ function generateConversationId() {
|
|
|
442
669
|
var AiChatConversationRepository_default = AiChatConversationRepository;
|
|
443
670
|
export {
|
|
444
671
|
AiChatConversationAccessError,
|
|
672
|
+
AiChatConversationDuplicateParticipantError,
|
|
673
|
+
AiChatConversationOrgNotFoundError,
|
|
445
674
|
AiChatConversationRepository,
|
|
675
|
+
AiChatParticipantNotFoundError,
|
|
446
676
|
AiChatConversationRepository_default as default
|
|
447
677
|
};
|
|
448
678
|
//# sourceMappingURL=AiChatConversationRepository.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport {\n AiChatConversation,\n AiChatConversationParticipant,\n AiChatMessage,\n} from '../entities'\nimport type {\n AiChatMessageAppendInput,\n AiChatPageContextInput,\n} from '../validators'\n\n/**\n * Persistent store for AI chat conversations, participants, and messages.\n *\n * Owner-first MVP per spec\n * `2026-05-05-ai-chat-server-side-conversation-storage`. Every read/write\n * goes through `findOneWithDecryption` / `findWithDecryption` so the repo\n * stays consistent with the rest of the module and is GDPR-encryption-ready\n * without a second refactor when `content` / `ui_parts` columns are\n * eventually flagged.\n *\n * Tenant + organization scope is required on every method. View-only callers\n * are owner-scoped. Callers with `ai_assistant.conversations.manage` may\n * list/read/update/delete any conversation in the same tenant/org, but never\n * outside that boundary. The participant row is written transactionally\n * alongside conversation create/import.\n *\n * TODO(ai-chat-sharing): widen the non-manage read predicate to include\n * explicit undeleted participants once shared conversations are implemented.\n */\n\nexport interface AiChatConversationContext {\n tenantId: string\n organizationId?: string | null\n userId: string\n canManageConversations?: boolean\n}\n\nexport interface AiChatConversationCreateOrGetInput {\n conversationId?: string | null\n agentId: string\n title?: string | null\n pageContext?: AiChatPageContextInput | null\n /** Marks the conversation as imported from local storage (sets `importedFromLocalAt`). */\n importedFromLocal?: boolean\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatConversationListOptions {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n cursor?: string | null\n}\n\nexport interface AiChatConversationUpdateInput {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: AiChatPageContextInput | null\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatTranscriptOptions {\n limit?: number\n /** ISO timestamp string; rows strictly older than this are returned. */\n before?: string | null\n}\n\nexport interface AiChatTranscriptResult {\n conversation: AiChatConversation\n messages: AiChatMessage[]\n nextCursor: string | null\n}\n\nexport interface AiChatMessageAppendOptions {\n /** Override the message timestamp (used to thread server-injected stream-completion turns). */\n createdAt?: Date\n /** Override `createdByUserId` (defaults to the calling context user). */\n createdByUserId?: string | null\n}\n\nexport interface AiChatConversationImportResult {\n conversation: AiChatConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\nconst DEFAULT_LIST_LIMIT = 50\nconst MAX_LIST_LIMIT = 100\nconst DEFAULT_TRANSCRIPT_LIMIT = 100\nconst MAX_TRANSCRIPT_LIMIT = 200\n\nexport class AiChatConversationAccessError extends Error {\n override readonly name = 'AiChatConversationAccessError'\n constructor(message: string = 'Conversation is not accessible to the caller.') {\n super(message)\n }\n}\n\nexport class AiChatConversationRepository {\n constructor(private readonly em: EntityManager) {}\n\n /**\n * Idempotent create. If a non-deleted conversation already exists for the\n * caller in this tenant/org with the same `conversationId`, returns the\n * existing row. The owner-participant row is created in the same\n * transaction; a partial failure leaves no orphan conversation.\n */\n async createOrGet(\n input: AiChatConversationCreateOrGetInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'createOrGet')\n if (!input?.agentId) {\n throw new Error('AiChatConversationRepository.createOrGet requires agentId')\n }\n const now = input.now ?? new Date()\n const conversationId = (input.conversationId ?? '').trim() || generateConversationId()\n\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (existing) {\n if (existing.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n return existing\n }\n const conversation = tx.create(AiChatConversation, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n agentId: input.agentId,\n ownerUserId: ctx.userId,\n title: normalizeTitle(input.title),\n status: 'open',\n visibility: 'private',\n pageContext: input.pageContext ?? null,\n lastMessageAt: null,\n importedFromLocalAt: input.importedFromLocal ? now : null,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatConversation)\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId: ctx.userId,\n role: 'owner',\n lastReadAt: null,\n createdAt: now,\n updatedAt: now,\n } as unknown as AiChatConversationParticipant)\n await tx.persist(conversation).persist(participant).flush()\n return conversation\n })\n }\n\n /** Fetch within tenant/org. View-only callers see only their own conversations. */\n async getById(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation | null> {\n assertContext(ctx, 'getById')\n if (!conversationId) return null\n const row = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!row) return null\n if (!canAccessConversation(row, ctx)) return null\n return row\n }\n\n /** Owner-scoped list unless the caller has tenant/org manage access. */\n async list(\n ctx: AiChatConversationContext,\n options: AiChatConversationListOptions = {},\n ): Promise<{ items: AiChatConversation[]; nextCursor: string | null }> {\n assertContext(ctx, 'list')\n const limit = clampLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n deletedAt: null,\n }\n if (!canManageConversations(ctx)) where.ownerUserId = ctx.userId\n if (options.agentId) where.agentId = options.agentId\n if (options.status) where.status = options.status\n if (options.cursor) {\n const cursorDate = parseIso(options.cursor)\n if (cursorDate) {\n where.lastMessageAt = { $lt: cursorDate }\n }\n }\n const rows = await findWithDecryption<AiChatConversation>(\n this.em,\n AiChatConversation,\n where as any,\n {\n orderBy: [{ lastMessageAt: 'desc' }, { createdAt: 'desc' }] as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n if (rows.length > limit) {\n const lastIncluded = rows[limit - 1]\n const cursorValue = lastIncluded.lastMessageAt ?? lastIncluded.createdAt\n nextCursor = cursorValue ? cursorValue.toISOString() : null\n }\n return { items: rows.slice(0, limit), nextCursor }\n }\n\n /** Update within tenant/org. View-only callers can update only their own conversations. */\n async update(\n conversationId: string,\n patch: AiChatConversationUpdateInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'update')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.update requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n const now = patch.now ?? new Date()\n if (Object.prototype.hasOwnProperty.call(patch, 'title')) {\n existing.title = normalizeTitle(patch.title)\n }\n if (patch.status) existing.status = patch.status\n if (Object.prototype.hasOwnProperty.call(patch, 'pageContext')) {\n existing.pageContext = patch.pageContext ?? null\n }\n existing.updatedAt = now\n await tx.persist(existing).flush()\n return existing\n })\n }\n\n /** Soft-delete the conversation and all its messages in one transaction. */\n async softDelete(\n conversationId: string,\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<void> {\n assertContext(ctx, 'softDelete')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.softDelete requires conversationId')\n }\n await this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n existing.deletedAt = now\n existing.status = 'closed'\n existing.updatedAt = now\n await tx.persist(existing).flush()\n\n const messages = await findWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n for (const msg of messages) {\n msg.deletedAt = now\n msg.updatedAt = now\n tx.persist(msg)\n }\n if (messages.length > 0) await tx.flush()\n })\n }\n\n /**\n * Owner-only transcript hydration. Internally fetched DESC so the `before`\n * cursor naturally advances toward older messages, then reversed so the\n * response contract (`messages` array ordered ascending by `createdAt`)\n * stays stable for callers. `nextCursor` points to the OLDEST message in\n * the returned page \u2014 the next call with `before=<cursor>` fetches the\n * next-older window.\n */\n async getTranscript(\n conversationId: string,\n ctx: AiChatConversationContext,\n options: AiChatTranscriptOptions = {},\n ): Promise<AiChatTranscriptResult | null> {\n assertContext(ctx, 'getTranscript')\n if (!conversationId) return null\n const conversation = await this.getById(conversationId, ctx)\n if (!conversation) return null\n const limit = clampLimit(options.limit, DEFAULT_TRANSCRIPT_LIMIT, MAX_TRANSCRIPT_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n }\n if (options.before) {\n const beforeDate = parseIso(options.before)\n if (beforeDate) {\n where.createdAt = { $lt: beforeDate }\n }\n }\n const rows = await findWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n where as any,\n {\n orderBy: { createdAt: 'desc' } as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n let pageDesc: AiChatMessage[]\n if (rows.length > limit) {\n pageDesc = rows.slice(0, limit)\n const oldestIncluded = pageDesc[pageDesc.length - 1]\n nextCursor = oldestIncluded?.createdAt ? oldestIncluded.createdAt.toISOString() : null\n } else {\n pageDesc = rows\n }\n const messages = [...pageDesc].reverse()\n return { conversation, messages, nextCursor }\n }\n\n /**\n * Append a single message to an owner-accessible conversation. Honors\n * `clientMessageId` idempotency: if a non-deleted message with the same\n * client id already exists, returns it untouched.\n */\n async appendMessage(\n conversationId: string,\n input: AiChatMessageAppendInput,\n ctx: AiChatConversationContext,\n options: AiChatMessageAppendOptions = {},\n ): Promise<AiChatMessage> {\n assertContext(ctx, 'appendMessage')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.appendMessage requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const conversation = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conversation) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conversation.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n const now = options.createdAt ?? new Date()\n if (input.clientMessageId) {\n const existing = await findOneWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (existing) return existing\n }\n const message = tx.create(AiChatMessage, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId ?? null,\n role: input.role,\n content: input.content,\n uiParts: normalizeArray(input.uiParts),\n attachmentIds: normalizeArray(input.attachmentIds),\n filesMetadata: normalizeArray(input.files),\n model: input.model ?? null,\n metadata: input.metadata ?? null,\n createdByUserId:\n options.createdByUserId === undefined\n ? input.role === 'user'\n ? ctx.userId\n : null\n : options.createdByUserId,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatMessage)\n conversation.lastMessageAt = now\n conversation.updatedAt = now\n await tx.persist(message).persist(conversation).flush()\n return message\n })\n }\n\n /**\n * Lazy migration entrypoint: create-or-get the conversation and append the\n * provided messages with `clientMessageId` dedupe. Designed to be safe to\n * call repeatedly \u2014 repeated imports of the same payload return the same\n * counts of imported/skipped rows.\n */\n async importLocalConversation(\n input: {\n conversation: AiChatConversationCreateOrGetInput & {\n status?: 'open' | 'closed'\n }\n messages: AiChatMessageAppendInput[]\n },\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<AiChatConversationImportResult> {\n assertContext(ctx, 'importLocalConversation')\n const conversation = await this.createOrGet(\n { ...input.conversation, importedFromLocal: true, now },\n ctx,\n )\n if (input.conversation.status && conversation.status !== input.conversation.status) {\n await this.update(\n conversation.conversationId,\n { status: input.conversation.status, now },\n ctx,\n )\n }\n let imported = 0\n let skipped = 0\n for (const message of input.messages) {\n if (!message.clientMessageId) {\n // Without an idempotency key the import has no safe way to dedupe.\n skipped += 1\n continue\n }\n const before = await findOneWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId: conversation.conversationId,\n clientMessageId: message.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (before) {\n skipped += 1\n continue\n }\n await this.appendMessage(\n conversation.conversationId,\n message,\n ctx,\n { createdAt: now },\n )\n imported += 1\n }\n return {\n conversation,\n importedMessageCount: imported,\n skippedMessageCount: skipped,\n }\n }\n}\n\nfunction assertContext(ctx: AiChatConversationContext | undefined, method: string): void {\n if (!ctx?.tenantId) {\n throw new Error(`AiChatConversationRepository.${method} requires tenantId`)\n }\n if (!ctx?.userId) {\n throw new Error(`AiChatConversationRepository.${method} requires userId`)\n }\n}\n\nfunction canManageConversations(ctx: AiChatConversationContext): boolean {\n return ctx.canManageConversations === true\n}\n\nfunction canAccessConversation(\n row: AiChatConversation,\n ctx: AiChatConversationContext,\n): boolean {\n return canManageConversations(ctx) || row.ownerUserId === ctx.userId\n}\n\nasync function findOneAccessibleConversation(\n em: EntityManager,\n conversationId: string,\n ctx: AiChatConversationContext,\n): Promise<AiChatConversation | null> {\n const row = await findOneWithDecryption<AiChatConversation>(\n em,\n AiChatConversation,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return row ?? null\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | null {\n if (title === undefined) return null\n if (title === null) return null\n const trimmed = title.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeArray<T>(value: T[] | null | undefined): T[] | null {\n if (!Array.isArray(value) || value.length === 0) return null\n return value\n}\n\nfunction clampLimit(value: number | undefined | null, fallback: number, max: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return fallback\n return Math.max(1, Math.min(Math.floor(value), max))\n}\n\nfunction parseIso(value: string): Date | null {\n if (!value) return null\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction generateConversationId(): string {\n // Prefer the runtime crypto generator when present; fall back to a non-cryptographic\n // string for environments without `crypto.randomUUID()` (older Node / test mocks).\n const cryptoMod: { randomUUID?: () => string } | undefined =\n typeof globalThis === 'object' ? (globalThis as any).crypto : undefined\n if (cryptoMod?.randomUUID) return cryptoMod.randomUUID()\n return `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`\n}\n\nexport default AiChatConversationRepository\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport {\n AiChatConversation,\n AiChatConversationParticipant,\n AiChatMessage,\n} from '../entities'\nimport type {\n AiChatMessageAppendInput,\n AiChatPageContextInput,\n} from '../validators'\n\n/**\n * Persistent store for AI chat conversations, participants, and messages.\n *\n * Owner-first MVP per spec\n * `2026-05-05-ai-chat-server-side-conversation-storage`. Every read/write\n * goes through `findOneWithDecryption` / `findWithDecryption` so the repo\n * stays consistent with the rest of the module and is GDPR-encryption-ready\n * without a second refactor when `content` / `ui_parts` columns are\n * eventually flagged.\n *\n * Tenant + organization scope is required on every method. View-only callers\n * are owner-scoped. Callers with `ai_assistant.conversations.manage` may\n * list/read/update/delete any conversation in the same tenant/org, but never\n * outside that boundary. The participant row is written transactionally\n * alongside conversation create/import.\n *\n */\n\nexport interface AiChatConversationContext {\n tenantId: string\n organizationId?: string | null\n userId: string\n canManageConversations?: boolean\n}\n\nexport interface AiChatConversationCreateOrGetInput {\n conversationId?: string | null\n agentId: string\n title?: string | null\n pageContext?: AiChatPageContextInput | null\n /** Marks the conversation as imported from local storage (sets `importedFromLocalAt`). */\n importedFromLocal?: boolean\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatConversationListOptions {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n cursor?: string | null\n}\n\nexport interface AiChatConversationUpdateInput {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: AiChatPageContextInput | null\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatTranscriptOptions {\n limit?: number\n /** ISO timestamp string; rows strictly older than this are returned. */\n before?: string | null\n}\n\nexport interface AiChatTranscriptResult {\n conversation: AiChatConversation\n messages: AiChatMessage[]\n nextCursor: string | null\n}\n\nexport interface AiChatMessageAppendOptions {\n /** Override the message timestamp (used to thread server-injected stream-completion turns). */\n createdAt?: Date\n /** Override `createdByUserId` (defaults to the calling context user). */\n createdByUserId?: string | null\n}\n\nexport interface AiChatConversationImportResult {\n conversation: AiChatConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\nconst DEFAULT_LIST_LIMIT = 50\nconst MAX_LIST_LIMIT = 100\nconst DEFAULT_TRANSCRIPT_LIMIT = 100\nconst MAX_TRANSCRIPT_LIMIT = 200\n\nexport class AiChatConversationAccessError extends Error {\n override readonly name = 'AiChatConversationAccessError'\n constructor(message: string = 'Conversation is not accessible to the caller.') {\n super(message)\n }\n}\n\nexport class AiChatConversationDuplicateParticipantError extends Error {\n override readonly name = 'AiChatConversationDuplicateParticipantError'\n constructor(message: string = 'User is already an active participant in this conversation.') {\n super(message)\n }\n}\n\nexport class AiChatParticipantNotFoundError extends Error {\n override readonly name = 'AiChatParticipantNotFoundError'\n constructor(message: string = 'Participant not found or already revoked.') {\n super(message)\n }\n}\n\nexport class AiChatConversationOrgNotFoundError extends Error {\n override readonly name = 'AiChatConversationOrgNotFoundError'\n constructor(message: string = 'Organization does not exist or is inactive for this tenant.') {\n super(message)\n }\n}\n\nexport class AiChatConversationRepository {\n constructor(private readonly em: EntityManager) {}\n\n /**\n * Idempotent create. If a non-deleted conversation already exists for the\n * caller in this tenant/org with the same `conversationId`, returns the\n * existing row. The owner-participant row is created in the same\n * transaction; a partial failure leaves no orphan conversation.\n */\n async createOrGet(\n input: AiChatConversationCreateOrGetInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'createOrGet')\n if (!input?.agentId) {\n throw new Error('AiChatConversationRepository.createOrGet requires agentId')\n }\n const now = input.now ?? new Date()\n const conversationId = (input.conversationId ?? '').trim() || generateConversationId()\n\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (existing) {\n if (existing.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n return existing\n }\n await assertOrganizationExists(tx as unknown as EntityManager, ctx)\n const conversation = tx.create(AiChatConversation, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n agentId: input.agentId,\n ownerUserId: ctx.userId,\n title: normalizeTitle(input.title),\n status: 'open',\n visibility: 'private',\n pageContext: input.pageContext ?? null,\n lastMessageAt: null,\n importedFromLocalAt: input.importedFromLocal ? now : null,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatConversation)\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId: ctx.userId,\n role: 'owner',\n lastReadAt: null,\n createdAt: now,\n updatedAt: now,\n } as unknown as AiChatConversationParticipant)\n await tx.persist(conversation).persist(participant).flush()\n return conversation\n })\n }\n\n /** Fetch within tenant/org. View-only callers see only their own conversations. */\n async getById(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation | null> {\n assertContext(ctx, 'getById')\n if (!conversationId) return null\n const row = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!row) return null\n const isParticipant =\n !canManageConversations(ctx) && row.ownerUserId !== ctx.userId\n ? await this.loadParticipantFlag(\n this.em,\n ctx.tenantId!,\n ctx.organizationId,\n row.conversationId,\n ctx.userId!,\n )\n : false\n if (!canAccessConversation(row, ctx, isParticipant)) return null\n return row\n }\n\n /** Owner-scoped list unless the caller has tenant/org manage access. Participants also see shared conversations. */\n async list(\n ctx: AiChatConversationContext,\n options: AiChatConversationListOptions = {},\n ): Promise<{ items: AiChatConversation[]; nextCursor: string | null }> {\n assertContext(ctx, 'list')\n const limit = clampLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n deletedAt: null,\n }\n if (!canManageConversations(ctx)) {\n const participantFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n userId: ctx.userId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const participantRows = await findWithDecryption<AiChatConversationParticipant>(\n this.em,\n AiChatConversationParticipant,\n participantFilter,\n { fields: ['conversationId'] as any },\n { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },\n )\n const participantConvIds = participantRows.map((p) => p.conversationId)\n if (participantConvIds.length > 0) {\n where.$or = [\n { ownerUserId: ctx.userId },\n { conversationId: { $in: participantConvIds } },\n ]\n } else {\n where.ownerUserId = ctx.userId\n }\n }\n if (options.agentId) where.agentId = options.agentId\n if (options.status) where.status = options.status\n if (options.cursor) {\n const cursorDate = parseIso(options.cursor)\n if (cursorDate) {\n where.lastMessageAt = { $lt: cursorDate }\n }\n }\n const rows = await findWithDecryption<AiChatConversation>(\n this.em,\n AiChatConversation,\n where as any,\n {\n orderBy: [{ lastMessageAt: 'desc' }, { createdAt: 'desc' }] as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n if (rows.length > limit) {\n const lastIncluded = rows[limit - 1]\n const cursorValue = lastIncluded.lastMessageAt ?? lastIncluded.createdAt\n nextCursor = cursorValue ? cursorValue.toISOString() : null\n }\n return { items: rows.slice(0, limit), nextCursor }\n }\n\n /** Update within tenant/org. View-only callers can update only their own conversations. */\n async update(\n conversationId: string,\n patch: AiChatConversationUpdateInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'update')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.update requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n const now = patch.now ?? new Date()\n if (Object.prototype.hasOwnProperty.call(patch, 'title')) {\n existing.title = normalizeTitle(patch.title)\n }\n if (patch.status) existing.status = patch.status\n if (Object.prototype.hasOwnProperty.call(patch, 'pageContext')) {\n existing.pageContext = patch.pageContext ?? null\n }\n existing.updatedAt = now\n await tx.persist(existing).flush()\n return existing\n })\n }\n\n /** Soft-delete the conversation and all its messages in one transaction. */\n async softDelete(\n conversationId: string,\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<void> {\n assertContext(ctx, 'softDelete')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.softDelete requires conversationId')\n }\n await this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n existing.deletedAt = now\n existing.status = 'closed'\n existing.updatedAt = now\n await tx.persist(existing).flush()\n\n const messages = await findWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n for (const msg of messages) {\n msg.deletedAt = now\n msg.updatedAt = now\n tx.persist(msg)\n }\n if (messages.length > 0) await tx.flush()\n })\n }\n\n /**\n * Owner-only transcript hydration. Internally fetched DESC so the `before`\n * cursor naturally advances toward older messages, then reversed so the\n * response contract (`messages` array ordered ascending by `createdAt`)\n * stays stable for callers. `nextCursor` points to the OLDEST message in\n * the returned page \u2014 the next call with `before=<cursor>` fetches the\n * next-older window.\n */\n async getTranscript(\n conversationId: string,\n ctx: AiChatConversationContext,\n options: AiChatTranscriptOptions = {},\n ): Promise<AiChatTranscriptResult | null> {\n assertContext(ctx, 'getTranscript')\n if (!conversationId) return null\n const conversation = await this.getById(conversationId, ctx)\n if (!conversation) return null\n const limit = clampLimit(options.limit, DEFAULT_TRANSCRIPT_LIMIT, MAX_TRANSCRIPT_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n }\n if (options.before) {\n const beforeDate = parseIso(options.before)\n if (beforeDate) {\n where.createdAt = { $lt: beforeDate }\n }\n }\n const rows = await findWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n where as any,\n {\n orderBy: { createdAt: 'desc' } as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n let pageDesc: AiChatMessage[]\n if (rows.length > limit) {\n pageDesc = rows.slice(0, limit)\n const oldestIncluded = pageDesc[pageDesc.length - 1]\n nextCursor = oldestIncluded?.createdAt ? oldestIncluded.createdAt.toISOString() : null\n } else {\n pageDesc = rows\n }\n const messages = [...pageDesc].reverse()\n return { conversation, messages, nextCursor }\n }\n\n /**\n * Append a single message to an owner-accessible conversation. Honors\n * `clientMessageId` idempotency: if a non-deleted message with the same\n * client id already exists, returns it untouched.\n */\n async appendMessage(\n conversationId: string,\n input: AiChatMessageAppendInput,\n ctx: AiChatConversationContext,\n options: AiChatMessageAppendOptions = {},\n ): Promise<AiChatMessage> {\n assertContext(ctx, 'appendMessage')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.appendMessage requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const conversation = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conversation) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conversation.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n const now = options.createdAt ?? new Date()\n if (input.clientMessageId) {\n const existing = await findOneWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (existing) return existing\n }\n const message = tx.create(AiChatMessage, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId ?? null,\n role: input.role,\n content: input.content,\n uiParts: normalizeArray(input.uiParts),\n attachmentIds: normalizeArray(input.attachmentIds),\n filesMetadata: normalizeArray(input.files),\n model: input.model ?? null,\n metadata: input.metadata ?? null,\n createdByUserId:\n options.createdByUserId === undefined\n ? input.role === 'user'\n ? ctx.userId\n : null\n : options.createdByUserId,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatMessage)\n conversation.lastMessageAt = now\n conversation.updatedAt = now\n await tx.persist(message).persist(conversation).flush()\n return message\n })\n }\n\n /**\n * Lazy migration entrypoint: create-or-get the conversation and append the\n * provided messages with `clientMessageId` dedupe. Designed to be safe to\n * call repeatedly \u2014 repeated imports of the same payload return the same\n * counts of imported/skipped rows.\n */\n async importLocalConversation(\n input: {\n conversation: AiChatConversationCreateOrGetInput & {\n status?: 'open' | 'closed'\n }\n messages: AiChatMessageAppendInput[]\n },\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<AiChatConversationImportResult> {\n assertContext(ctx, 'importLocalConversation')\n const conversation = await this.createOrGet(\n { ...input.conversation, importedFromLocal: true, now },\n ctx,\n )\n if (input.conversation.status && conversation.status !== input.conversation.status) {\n await this.update(\n conversation.conversationId,\n { status: input.conversation.status, now },\n ctx,\n )\n }\n let imported = 0\n let skipped = 0\n for (const message of input.messages) {\n if (!message.clientMessageId) {\n // Without an idempotency key the import has no safe way to dedupe.\n skipped += 1\n continue\n }\n const before = await findOneWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId: conversation.conversationId,\n clientMessageId: message.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (before) {\n skipped += 1\n continue\n }\n await this.appendMessage(\n conversation.conversationId,\n message,\n ctx,\n { createdAt: now },\n )\n imported += 1\n }\n return {\n conversation,\n importedMessageCount: imported,\n skippedMessageCount: skipped,\n }\n }\n\n async listParticipants(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversationParticipant[]> {\n assertContext(ctx, 'listParticipants')\n const conv = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId && !canManageConversations(ctx)) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner or a manager can list participants.',\n )\n }\n const filter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n return findWithDecryption<AiChatConversationParticipant>(\n this.em,\n AiChatConversationParticipant,\n filter,\n { orderBy: { createdAt: 'asc' } as any },\n { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },\n )\n }\n\n async addParticipant(\n conversationId: string,\n userId: string,\n role: 'viewer',\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversationParticipant> {\n assertContext(ctx, 'addParticipant')\n return this.em.transactional(async (tx) => {\n const conv = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner can add participants.',\n )\n }\n const existingFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n userId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const existing = await findOneWithDecryption<AiChatConversationParticipant>(\n tx as unknown as EntityManager,\n AiChatConversationParticipant,\n existingFilter,\n )\n if (existing) {\n if (existing.deletedAt === null) {\n throw new AiChatConversationDuplicateParticipantError()\n }\n existing.deletedAt = null\n existing.role = role\n await tx.persist(existing).flush()\n if (conv.visibility === 'private') {\n conv.visibility = 'shared'\n await tx.persist(conv).flush()\n }\n return existing\n }\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId!,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId,\n role,\n } as unknown as AiChatConversationParticipant)\n if (conv.visibility === 'private') {\n conv.visibility = 'shared'\n }\n await tx.persist(participant).persist(conv).flush()\n return participant\n })\n }\n\n async revokeParticipant(\n conversationId: string,\n targetUserId: string,\n ctx: AiChatConversationContext,\n ): Promise<void> {\n assertContext(ctx, 'revokeParticipant')\n await this.em.transactional(async (tx) => {\n const conv = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner can revoke participants.',\n )\n }\n if (targetUserId === conv.ownerUserId) {\n throw new AiChatConversationAccessError('Cannot revoke the conversation owner.')\n }\n const participantFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n userId: targetUserId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const participant = await findOneWithDecryption<AiChatConversationParticipant>(\n tx as unknown as EntityManager,\n AiChatConversationParticipant,\n participantFilter,\n )\n if (!participant) throw new AiChatParticipantNotFoundError()\n participant.deletedAt = new Date()\n const remainingCount = await tx.count(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n conversationId,\n deletedAt: null,\n role: { $ne: 'owner' },\n } as FilterQuery<AiChatConversationParticipant>)\n if (remainingCount <= 1) {\n conv.visibility = 'private'\n await tx.persist(conv)\n }\n await tx.persist(participant).flush()\n })\n }\n\n async getParticipantCount(\n tenantId: string,\n organizationId: string | null | undefined,\n conversationId: string,\n ): Promise<number> {\n return this.em.count(AiChatConversationParticipant, {\n tenantId,\n conversationId,\n deletedAt: null,\n role: { $ne: 'owner' },\n ...(organizationId ? { organizationId } : {}),\n } as FilterQuery<AiChatConversationParticipant>)\n }\n\n private async loadParticipantFlag(\n em: EntityManager,\n tenantId: string,\n organizationId: string | null | undefined,\n conversationId: string,\n userId: string,\n ): Promise<boolean> {\n const row = await findOneWithDecryption<AiChatConversationParticipant>(\n em,\n AiChatConversationParticipant,\n {\n tenantId,\n conversationId,\n userId,\n deletedAt: null,\n ...(organizationId ? { organizationId } : {}),\n } as FilterQuery<AiChatConversationParticipant>,\n )\n return row !== null\n }\n}\n\nfunction assertContext(ctx: AiChatConversationContext | undefined, method: string): void {\n if (!ctx?.tenantId) {\n throw new Error(`AiChatConversationRepository.${method} requires tenantId`)\n }\n if (!ctx?.userId) {\n throw new Error(`AiChatConversationRepository.${method} requires userId`)\n }\n}\n\nfunction canManageConversations(ctx: AiChatConversationContext): boolean {\n return ctx.canManageConversations === true\n}\n\nfunction canAccessConversation(\n row: AiChatConversation,\n ctx: AiChatConversationContext,\n isParticipant = false,\n): boolean {\n return canManageConversations(ctx) || row.ownerUserId === ctx.userId || isParticipant\n}\n\nasync function assertOrganizationExists(\n em: EntityManager,\n ctx: AiChatConversationContext,\n): Promise<void> {\n if (!ctx.organizationId) return\n const org = await findOneWithDecryption<Organization>(\n em,\n Organization,\n {\n id: ctx.organizationId,\n tenant: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!org) {\n throw new AiChatConversationOrgNotFoundError(\n `Organization \"${ctx.organizationId}\" does not exist or is inactive in tenant \"${ctx.tenantId}\".`,\n )\n }\n}\n\nasync function findOneAccessibleConversation(\n em: EntityManager,\n conversationId: string,\n ctx: AiChatConversationContext,\n): Promise<AiChatConversation | null> {\n const row = await findOneWithDecryption<AiChatConversation>(\n em,\n AiChatConversation,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return row ?? null\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | null {\n if (title === undefined) return null\n if (title === null) return null\n const trimmed = title.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeArray<T>(value: T[] | null | undefined): T[] | null {\n if (!Array.isArray(value) || value.length === 0) return null\n return value\n}\n\nfunction clampLimit(value: number | undefined | null, fallback: number, max: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return fallback\n return Math.max(1, Math.min(Math.floor(value), max))\n}\n\nfunction parseIso(value: string): Date | null {\n if (!value) return null\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction generateConversationId(): string {\n // Prefer the runtime crypto generator when present; fall back to a non-cryptographic\n // string for environments without `crypto.randomUUID()` (older Node / test mocks).\n const cryptoMod: { randomUUID?: () => string } | undefined =\n typeof globalThis === 'object' ? (globalThis as any).crypto : undefined\n if (cryptoMod?.randomUUID) return cryptoMod.randomUUID()\n return `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`\n}\n\nexport default AiChatConversationRepository\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -25,6 +25,20 @@ const events = [
|
|
|
25
25
|
category: "system",
|
|
26
26
|
clientBroadcast: false,
|
|
27
27
|
portalBroadcast: false
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "ai_assistant.conversation.shared",
|
|
31
|
+
label: "AI Conversation Shared",
|
|
32
|
+
entity: "ai_chat_conversation",
|
|
33
|
+
category: "lifecycle",
|
|
34
|
+
clientBroadcast: true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "ai_assistant.conversation.unshared",
|
|
38
|
+
label: "AI Conversation Unshared",
|
|
39
|
+
entity: "ai_chat_conversation",
|
|
40
|
+
category: "lifecycle",
|
|
41
|
+
clientBroadcast: true
|
|
28
42
|
}
|
|
29
43
|
];
|
|
30
44
|
const eventsConfig = createModuleEvents({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/ai_assistant/events.ts"],
|
|
4
|
-
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\n/**\n * AI Assistant Module Events\n *\n * Typed declarations for the pending-action lifecycle events emitted by\n * the Phase 3 WS-C mutation approval flow. The event IDs are FROZEN per\n * `BACKWARD_COMPATIBILITY.md` \u00A75 (contract surface #5) and MUST NOT be\n * renamed; additive payload changes are allowed.\n *\n * - `ai.action.confirmed` \u2014 emitted by `executePendingActionConfirm`\n * (Step 5.8) after the `pending \u2192 confirmed \u2192 executing \u2192 {confirmed|\n * failed}` transition. The handler's outcome lives in\n * `executionResult`; partial-stale rows carry the surviving stale\n * records via `failedRecords`.\n * - `ai.action.cancelled` \u2014 emitted by `executePendingActionCancel`\n * (Step 5.9) after the atomic `pending \u2192 cancelled` transition.\n * - `ai.action.expired` \u2014 emitted by the Step 5.9 expired short-circuit\n * AND by the Step 5.12 cleanup worker when the TTL elapses. The\n * worker is the actor in that path, so `resolvedByUserId` is NOT part\n * of the payload.\n */\nconst events = [\n {\n id: 'ai.action.confirmed',\n label: 'AI Pending Action Confirmed',\n entity: 'ai_pending_action',\n category: 'system' as const,\n },\n {\n id: 'ai.action.cancelled',\n label: 'AI Pending Action Cancelled',\n entity: 'ai_pending_action',\n category: 'system' as const,\n },\n {\n id: 'ai.action.expired',\n label: 'AI Pending Action Expired',\n entity: 'ai_pending_action',\n category: 'system' as const,\n },\n {\n id: 'ai.token_usage.recorded',\n label: 'AI Token Usage Recorded',\n entity: 'token_usage',\n category: 'system' as const,\n clientBroadcast: false,\n portalBroadcast: false,\n },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'ai_assistant',\n events,\n})\n\n/** Type-safe event emitter for the ai_assistant module. */\nexport const emitAiAssistantEvent = eventsConfig.emit\n\n/** Event IDs declared by the ai_assistant module. */\nexport type AiAssistantEventId = (typeof events)[number]['id']\n\n/**\n * Typed payload contracts for each ai_assistant event. Payloads are\n * additive-only \u2014 extend existing fields rather than renaming/removing.\n */\nexport interface AiActionFailedRecordPayload {\n recordId: string\n error: { code: string; message: string }\n}\n\nexport interface AiActionExecutionResultPayload {\n recordId?: string\n commandName?: string\n error?: { code: string; message: string }\n}\n\nexport interface AiActionConfirmedPayload {\n pendingActionId: string\n agentId: string\n toolName: string\n status: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n resolvedByUserId: string\n resolvedAt: string\n executionResult: AiActionExecutionResultPayload | null\n failedRecords?: AiActionFailedRecordPayload[] | null\n}\n\nexport interface AiActionCancelledPayload {\n pendingActionId: string\n agentId: string\n toolName: string\n status: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n resolvedByUserId: string\n resolvedAt: string\n executionResult: AiActionExecutionResultPayload | null\n reason?: string\n}\n\nexport interface AiActionExpiredPayload {\n pendingActionId: string\n agentId: string\n toolName: string\n status: string\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n resolvedByUserId: null\n resolvedAt: string\n expiresAt?: string\n expiredAt?: string\n}\n\nexport default eventsConfig\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,0BAA0B;AAsBnC,MAAM,SAAS;AAAA,EACb;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,EACnB;AACF;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAGM,MAAM,uBAAuB,aAAa;
|
|
4
|
+
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\n/**\n * AI Assistant Module Events\n *\n * Typed declarations for the pending-action lifecycle events emitted by\n * the Phase 3 WS-C mutation approval flow. The event IDs are FROZEN per\n * `BACKWARD_COMPATIBILITY.md` \u00A75 (contract surface #5) and MUST NOT be\n * renamed; additive payload changes are allowed.\n *\n * - `ai.action.confirmed` \u2014 emitted by `executePendingActionConfirm`\n * (Step 5.8) after the `pending \u2192 confirmed \u2192 executing \u2192 {confirmed|\n * failed}` transition. The handler's outcome lives in\n * `executionResult`; partial-stale rows carry the surviving stale\n * records via `failedRecords`.\n * - `ai.action.cancelled` \u2014 emitted by `executePendingActionCancel`\n * (Step 5.9) after the atomic `pending \u2192 cancelled` transition.\n * - `ai.action.expired` \u2014 emitted by the Step 5.9 expired short-circuit\n * AND by the Step 5.12 cleanup worker when the TTL elapses. The\n * worker is the actor in that path, so `resolvedByUserId` is NOT part\n * of the payload.\n */\nconst events = [\n {\n id: 'ai.action.confirmed',\n label: 'AI Pending Action Confirmed',\n entity: 'ai_pending_action',\n category: 'system' as const,\n },\n {\n id: 'ai.action.cancelled',\n label: 'AI Pending Action Cancelled',\n entity: 'ai_pending_action',\n category: 'system' as const,\n },\n {\n id: 'ai.action.expired',\n label: 'AI Pending Action Expired',\n entity: 'ai_pending_action',\n category: 'system' as const,\n },\n {\n id: 'ai.token_usage.recorded',\n label: 'AI Token Usage Recorded',\n entity: 'token_usage',\n category: 'system' as const,\n clientBroadcast: false,\n portalBroadcast: false,\n },\n {\n id: 'ai_assistant.conversation.shared',\n label: 'AI Conversation Shared',\n entity: 'ai_chat_conversation',\n category: 'lifecycle' as const,\n clientBroadcast: true,\n },\n {\n id: 'ai_assistant.conversation.unshared',\n label: 'AI Conversation Unshared',\n entity: 'ai_chat_conversation',\n category: 'lifecycle' as const,\n clientBroadcast: true,\n },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'ai_assistant',\n events,\n})\n\n/** Type-safe event emitter for the ai_assistant module. */\nexport const emitAiAssistantEvent = eventsConfig.emit\n\n/** Event IDs declared by the ai_assistant module. */\nexport type AiAssistantEventId = (typeof events)[number]['id']\n\n/**\n * Typed payload contracts for each ai_assistant event. Payloads are\n * additive-only \u2014 extend existing fields rather than renaming/removing.\n */\nexport interface AiActionFailedRecordPayload {\n recordId: string\n error: { code: string; message: string }\n}\n\nexport interface AiActionExecutionResultPayload {\n recordId?: string\n commandName?: string\n error?: { code: string; message: string }\n}\n\nexport interface AiActionConfirmedPayload {\n pendingActionId: string\n agentId: string\n toolName: string\n status: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n resolvedByUserId: string\n resolvedAt: string\n executionResult: AiActionExecutionResultPayload | null\n failedRecords?: AiActionFailedRecordPayload[] | null\n}\n\nexport interface AiActionCancelledPayload {\n pendingActionId: string\n agentId: string\n toolName: string\n status: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n resolvedByUserId: string\n resolvedAt: string\n executionResult: AiActionExecutionResultPayload | null\n reason?: string\n}\n\nexport interface AiActionExpiredPayload {\n pendingActionId: string\n agentId: string\n toolName: string\n status: string\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n resolvedByUserId: null\n resolvedAt: string\n expiresAt?: string\n expiredAt?: string\n}\n\nexport interface AiConversationSharedPayload {\n conversationId: string\n tenantId: string\n organizationId: string | null\n ownerUserId: string\n participantUserId: string\n role: string\n}\n\nexport interface AiConversationUnsharedPayload {\n conversationId: string\n tenantId: string\n organizationId: string | null\n ownerUserId: string\n participantUserId: string\n}\n\nexport default eventsConfig\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0BAA0B;AAsBnC,MAAM,SAAS;AAAA,EACb;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,EACnB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,iBAAiB;AAAA,EACnB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,iBAAiB;AAAA,EACnB;AACF;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAGM,MAAM,uBAAuB,aAAa;AA+EjD,IAAO,iBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -216,9 +216,11 @@
|
|
|
216
216
|
"ai_assistant.chat.mutation_cards.result.successWithCommand": "Abgeschlossen",
|
|
217
217
|
"ai_assistant.chat.mutation_cards.result.viewRecord": "Datensatz anzeigen",
|
|
218
218
|
"ai_assistant.chat.newConversation": "Start new conversation",
|
|
219
|
+
"ai_assistant.chat.ownerRoleLabel": "Owner",
|
|
219
220
|
"ai_assistant.chat.pending_phase3.body": "Diese interaktive Karte kommt in Phase 3 des vereinheitlichten KI-Frameworks.",
|
|
220
221
|
"ai_assistant.chat.pending_phase3.title": "Mutations-Freigabe-Karte ausstehend",
|
|
221
222
|
"ai_assistant.chat.placeholder": "Fragen Sie mich etwas...",
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "Dies ist eine geteilte Konversation. Sie können lesen, aber nicht antworten.",
|
|
222
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
223
225
|
"ai_assistant.chat.records.fields.amount": "Betrag",
|
|
224
226
|
"ai_assistant.chat.records.fields.category": "Kategorie",
|
|
@@ -282,6 +284,7 @@
|
|
|
282
284
|
"ai_assistant.dock.left": "Links andocken",
|
|
283
285
|
"ai_assistant.dock.minimize": "Minimieren",
|
|
284
286
|
"ai_assistant.dock.right": "Rechts andocken",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Fragen Sie mich etwas…",
|
|
285
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
286
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
287
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -323,6 +326,9 @@
|
|
|
323
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "KI-Modell auswählen",
|
|
324
327
|
"ai_assistant.modelPicker.useDefault": "Standard des Agenten verwenden",
|
|
325
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Standard des Agenten verwenden: {{model}}",
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "Eine KI-Konversation wurde mit Ihnen geteilt.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Konversation mit Ihnen geteilt",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "Konversation öffnen",
|
|
326
332
|
"ai_assistant.playground.agentPickerLabel": "Agent",
|
|
327
333
|
"ai_assistant.playground.chat.notSupportedBody": "Wählen Sie einen Agenten, dessen Ausführungsmodus \"chat\" ist, oder wechseln Sie zum Objekt-Modus-Tab.",
|
|
328
334
|
"ai_assistant.playground.chat.notSupportedTitle": "Chat-Modus ist für diesen Agenten nicht verfügbar.",
|
|
@@ -428,6 +434,17 @@
|
|
|
428
434
|
"ai_assistant.settings.visibilityEnabled": "Im Header sichtbar mit aktiviertem Cmd+J-Shortcut.",
|
|
429
435
|
"ai_assistant.settings.visibilityTitle": "KI-Assistent",
|
|
430
436
|
"ai_assistant.settings.visibilityToggleLabel": "Sichtbarkeit",
|
|
437
|
+
"ai_assistant.share.addParticipant": "Teilnehmer hinzufügen",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "Alle Benutzer bereits hinzugefügt",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Teilen Sie diese Konversation mit anderen Benutzern. Sie erhalten nur Lesezugriff.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Konversation teilen",
|
|
441
|
+
"ai_assistant.share.noParticipants": "Noch keine Teilnehmer. Fügen Sie jemanden hinzu, um diese Konversation zu teilen.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Nach Benutzer suchen...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Entfernen",
|
|
444
|
+
"ai_assistant.share.saved": "Gespeichert",
|
|
445
|
+
"ai_assistant.share.saving": "Wird gespeichert...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Benutzer auswählen...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Teilen",
|
|
431
448
|
"ai_assistant.status.analyzing": "Anfrage wird analysiert...",
|
|
432
449
|
"ai_assistant.status.executing": "Tools werden ausgeführt...",
|
|
433
450
|
"ai_assistant.status.responding": "Antwort wird erstellt...",
|
|
@@ -216,9 +216,11 @@
|
|
|
216
216
|
"ai_assistant.chat.mutation_cards.result.successWithCommand": "Completed",
|
|
217
217
|
"ai_assistant.chat.mutation_cards.result.viewRecord": "View record",
|
|
218
218
|
"ai_assistant.chat.newConversation": "Start new conversation",
|
|
219
|
+
"ai_assistant.chat.ownerRoleLabel": "Owner",
|
|
219
220
|
"ai_assistant.chat.pending_phase3.body": "This interactive card will land in Phase 3 of the unified AI framework.",
|
|
220
221
|
"ai_assistant.chat.pending_phase3.title": "Mutation approval card pending",
|
|
221
222
|
"ai_assistant.chat.placeholder": "Ask me anything...",
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "This is a shared conversation. You can read but not reply.",
|
|
222
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
223
225
|
"ai_assistant.chat.records.fields.amount": "Amount",
|
|
224
226
|
"ai_assistant.chat.records.fields.category": "Category",
|
|
@@ -282,6 +284,7 @@
|
|
|
282
284
|
"ai_assistant.dock.left": "Dock Left",
|
|
283
285
|
"ai_assistant.dock.minimize": "Minimize",
|
|
284
286
|
"ai_assistant.dock.right": "Dock Right",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Ask anything…",
|
|
285
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
286
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
287
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -323,6 +326,9 @@
|
|
|
323
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "Select AI model",
|
|
324
327
|
"ai_assistant.modelPicker.useDefault": "Use agent default",
|
|
325
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Use agent default: {{model}}",
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "An AI conversation has been shared with you.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Conversation shared with you",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "View Conversation",
|
|
326
332
|
"ai_assistant.playground.agentPickerLabel": "Agent",
|
|
327
333
|
"ai_assistant.playground.chat.notSupportedBody": "Pick an agent whose execution mode is \"chat\", or switch to the object-mode tab.",
|
|
328
334
|
"ai_assistant.playground.chat.notSupportedTitle": "Chat mode is not available for this agent.",
|
|
@@ -428,6 +434,17 @@
|
|
|
428
434
|
"ai_assistant.settings.visibilityEnabled": "Visible in header with Cmd+J shortcut enabled.",
|
|
429
435
|
"ai_assistant.settings.visibilityTitle": "AI Assistant",
|
|
430
436
|
"ai_assistant.settings.visibilityToggleLabel": "Visibility",
|
|
437
|
+
"ai_assistant.share.addParticipant": "Add participant",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "All users already added",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Share this conversation with other users. They will get read-only access.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Share Conversation",
|
|
441
|
+
"ai_assistant.share.noParticipants": "No participants yet. Add someone to share this conversation.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Search by user...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Remove",
|
|
444
|
+
"ai_assistant.share.saved": "Saved",
|
|
445
|
+
"ai_assistant.share.saving": "Saving...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Select a user...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Share",
|
|
431
448
|
"ai_assistant.status.analyzing": "Analyzing request...",
|
|
432
449
|
"ai_assistant.status.executing": "Executing tools...",
|
|
433
450
|
"ai_assistant.status.responding": "Responding...",
|