@open-mercato/ai-assistant 0.8.1-develop.7296.1.2111d779db → 0.8.1-develop.7441.1.b42958b98c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +5 -0
  3. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +2 -2
  4. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +1 -1
  5. package/dist/modules/ai_assistant/encryption.js +31 -0
  6. package/dist/modules/ai_assistant/encryption.js.map +7 -0
  7. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +20 -9
  8. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +2 -2
  9. package/dist/modules/ai_assistant/lib/generated-registry-loader.js +30 -2
  10. package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
  11. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +2 -1
  12. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
  13. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +1 -0
  14. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
  15. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js +2 -2
  16. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js.map +2 -2
  17. package/package.json +6 -6
  18. package/src/modules/ai_assistant/__tests__/encryption-map.test.ts +175 -0
  19. package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +5 -4
  20. package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +3 -3
  21. package/src/modules/ai_assistant/encryption.ts +29 -0
  22. package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +154 -0
  23. package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +107 -0
  24. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +73 -0
  25. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +22 -11
  26. package/src/modules/ai_assistant/lib/generated-registry-loader.ts +48 -4
  27. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +20 -2
  28. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +1 -0
  29. package/src/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.tsx +2 -2
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 237 entry points
1
+ [build:ai-assistant] found 238 entry points
2
2
  [build:ai-assistant] built successfully
package/README.md CHANGED
@@ -206,6 +206,11 @@ The custom provider is immediately available under its id in
206
206
  `OPENCODE_PROVIDER`, the backend settings dropdown, and the routing
207
207
  layer — no changes needed in core.
208
208
 
209
+ Presets speak the Chat Completions protocol (`POST {baseURL}/chat/completions`)
210
+ by default, which every OpenAI-compatible backend implements. Set
211
+ `apiMode: 'responses'` only when the backend implements OpenAI's Responses API
212
+ (`POST {baseURL}/responses`) — the built-in `openai` preset does so.
213
+
209
214
  ### .mcp.json Configuration
210
215
 
211
216
  For Claude Code integration, create `.mcp.json` in the project root:
@@ -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, 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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",
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` because the\n * conversation `title` and the message `content` / `ui_parts` /\n * `files_metadata` / `metadata` columns are declared in the module's\n * `encryption.ts` and are encrypted at rest when tenant data encryption is\n * enabled.\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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport { AiPendingAction } from '../entities'\nimport {\n AI_PENDING_ACTION_ALLOWED_TRANSITIONS,\n AiPendingActionStateError,\n resolveAiPendingActionTtlSeconds,\n type AiPendingActionExecutionResult,\n type AiPendingActionFailedRecord,\n type AiPendingActionFieldDiff,\n type AiPendingActionQueueMode,\n type AiPendingActionRecordDiff,\n type AiPendingActionStatus,\n} from '../../lib/pending-action-types'\n\nexport interface AiPendingActionContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface AiPendingActionCreateInput {\n agentId: string\n toolName: string\n idempotencyKey: string\n createdByUserId: string\n normalizedInput: Record<string, unknown>\n conversationId?: string | null\n targetEntityType?: string | null\n targetRecordId?: string | null\n fieldDiff?: AiPendingActionFieldDiff[]\n records?: AiPendingActionRecordDiff[] | null\n sideEffectsSummary?: string | null\n recordVersion?: string | null\n attachmentIds?: string[]\n queueMode?: AiPendingActionQueueMode\n /** Optional explicit TTL in seconds; overrides the env/default TTL. */\n ttlSeconds?: number\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiPendingActionSetStatusExtra {\n resolvedByUserId?: string | null\n executionResult?: AiPendingActionExecutionResult | null\n failedRecords?: AiPendingActionFailedRecord[] | null\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\n/**\n * Persistent store for the Phase 3 WS-C mutation approval gate (Step 5.5).\n *\n * Responsibilities:\n * - Create new pending rows with a TTL-derived `expiresAt`, honoring\n * idempotency within the window (same `idempotencyKey` returns the same\n * row as long as it is still `pending`; any terminal state mints a new row).\n * - Tenant-scoped lookups for the confirm/cancel/reconnect routes and the\n * in-app UI's \"open actions\" list.\n * - State-machine enforcement: `setStatus` rejects illegal transitions via\n * {@link AiPendingActionStateError}. The runtime callers translate this\n * to a 409 Conflict response.\n * - `listExpired` for the cleanup worker (Step 5.12).\n *\n * Every read goes through `findOneWithDecryption` / `findWithDecryption`\n * even though today no column is GDPR-flagged. This keeps the repo\n * consistent with the rest of the module and preps for a future encrypted\n * `normalizedInput` without a second refactor.\n */\nexport class AiPendingActionRepository {\n constructor(private readonly em: EntityManager) {}\n\n async create(\n input: AiPendingActionCreateInput,\n ctx: AiPendingActionContext,\n ): Promise<AiPendingAction> {\n if (!ctx?.tenantId) {\n throw new Error('AiPendingActionRepository.create requires tenantId')\n }\n if (!input?.agentId) {\n throw new Error('AiPendingActionRepository.create requires agentId')\n }\n if (!input?.toolName) {\n throw new Error('AiPendingActionRepository.create requires toolName')\n }\n if (!input?.idempotencyKey) {\n throw new Error(\n 'AiPendingActionRepository.create requires idempotencyKey',\n )\n }\n if (!input?.createdByUserId) {\n throw new Error(\n 'AiPendingActionRepository.create requires createdByUserId',\n )\n }\n\n const now = input.now ?? new Date()\n const ttlSeconds = Math.max(\n 1,\n Math.floor(\n typeof input.ttlSeconds === 'number' && Number.isFinite(input.ttlSeconds)\n ? input.ttlSeconds\n : resolveAiPendingActionTtlSeconds(),\n ),\n )\n const expiresAt = new Date(now.getTime() + ttlSeconds * 1000)\n\n return this.em.transactional(async (tx) => {\n const existing = await findOneWithDecryption<AiPendingAction>(\n tx as unknown as EntityManager,\n AiPendingAction,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n idempotencyKey: input.idempotencyKey,\n } as any,\n { orderBy: { createdAt: 'desc' } as any },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (existing && existing.status === 'pending') {\n return existing\n }\n // Terminal stale row would collide on the unique\n // `(tenantId, organizationId, idempotencyKey)` constraint when we\n // try to insert a fresh one with the same hash \u2014 and that exact\n // collision happens whenever the operator clicks \"Fix with AI\"\n // and the model retries the SAME tool with the SAME args. Remove\n // the stale row first so a retry can always proceed; success rows\n // stay (they represent a real, applied change), and failed /\n // cancelled / expired rows are cleared because they're blocking\n // exactly the recovery flow they were created to enable.\n if (\n existing &&\n (existing.status === 'failed' ||\n existing.status === 'cancelled' ||\n existing.status === 'expired')\n ) {\n await tx.remove(existing).flush()\n }\n const row = tx.create(AiPendingAction, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n agentId: input.agentId,\n toolName: input.toolName,\n conversationId: input.conversationId ?? null,\n targetEntityType: input.targetEntityType ?? null,\n targetRecordId: input.targetRecordId ?? null,\n normalizedInput: input.normalizedInput ?? {},\n fieldDiff: Array.isArray(input.fieldDiff) ? input.fieldDiff : [],\n records: normalizeRecords(input.records),\n failedRecords: null,\n sideEffectsSummary: input.sideEffectsSummary ?? null,\n recordVersion: input.recordVersion ?? null,\n attachmentIds: Array.isArray(input.attachmentIds)\n ? input.attachmentIds\n : [],\n idempotencyKey: input.idempotencyKey,\n createdByUserId: input.createdByUserId,\n status: 'pending' as AiPendingActionStatus,\n queueMode: (input.queueMode ?? 'inline') as AiPendingActionQueueMode,\n executionResult: null,\n createdAt: now,\n expiresAt,\n resolvedAt: null,\n resolvedByUserId: null,\n } as unknown as AiPendingAction)\n await tx.persist(row).flush()\n return row\n })\n }\n\n async getById(\n id: string,\n ctx: AiPendingActionContext,\n ): Promise<AiPendingAction | null> {\n if (!id || !ctx?.tenantId) return null\n const row = await findOneWithDecryption<AiPendingAction>(\n this.em,\n AiPendingAction,\n {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return row ?? null\n }\n\n async listPendingForAgent(\n agentId: string,\n ctx: AiPendingActionContext,\n limit: number = 50,\n ): Promise<AiPendingAction[]> {\n if (!agentId || !ctx?.tenantId) return []\n const capped = Math.max(1, Math.min(Math.floor(limit), 200))\n const rows = await findWithDecryption<AiPendingAction>(\n this.em,\n AiPendingAction,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n agentId,\n status: 'pending',\n } as any,\n {\n orderBy: { createdAt: 'desc' } as any,\n limit: capped,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return rows\n }\n\n async setStatus(\n id: string,\n nextStatus: AiPendingActionStatus,\n ctx: AiPendingActionContext,\n extra?: AiPendingActionSetStatusExtra,\n ): Promise<AiPendingAction> {\n if (!ctx?.tenantId) {\n throw new Error('AiPendingActionRepository.setStatus requires tenantId')\n }\n if (!id) {\n throw new Error('AiPendingActionRepository.setStatus requires id')\n }\n return this.em.transactional(async (tx) => {\n const existing = await findOneWithDecryption<AiPendingAction>(\n tx as unknown as EntityManager,\n AiPendingAction,\n {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!existing) {\n throw new Error(`AiPendingAction not found: ${id}`)\n }\n if (existing.status === nextStatus) {\n return existing\n }\n const allowed = AI_PENDING_ACTION_ALLOWED_TRANSITIONS[existing.status] ?? []\n if (!allowed.includes(nextStatus)) {\n throw new AiPendingActionStateError(existing.status, nextStatus)\n }\n const now = extra?.now ?? new Date()\n existing.status = nextStatus\n if (\n nextStatus === 'confirmed' ||\n nextStatus === 'cancelled' ||\n nextStatus === 'expired' ||\n nextStatus === 'failed'\n ) {\n existing.resolvedAt = existing.resolvedAt ?? now\n if (extra && Object.prototype.hasOwnProperty.call(extra, 'resolvedByUserId')) {\n existing.resolvedByUserId = extra.resolvedByUserId ?? null\n } else if (nextStatus === 'expired') {\n existing.resolvedByUserId = null\n }\n }\n if (extra && Object.prototype.hasOwnProperty.call(extra, 'executionResult')) {\n existing.executionResult = extra.executionResult ?? null\n }\n if (extra && Object.prototype.hasOwnProperty.call(extra, 'failedRecords')) {\n existing.failedRecords = normalizeFailedRecords(extra.failedRecords)\n }\n await tx.persist(existing).flush()\n return existing\n })\n }\n\n async listExpired(\n ctx: AiPendingActionContext,\n now: Date,\n limit: number = 100,\n ): Promise<AiPendingAction[]> {\n if (!ctx?.tenantId) return []\n const capped = Math.max(1, Math.min(Math.floor(limit), 500))\n const rows = await findWithDecryption<AiPendingAction>(\n this.em,\n AiPendingAction,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n status: 'pending',\n expiresAt: { $lt: now } as any,\n } as any,\n {\n orderBy: { expiresAt: 'asc' } as any,\n limit: capped,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return rows\n }\n}\n\nfunction normalizeRecords(\n records: AiPendingActionRecordDiff[] | null | undefined,\n): AiPendingActionRecordDiff[] | null {\n if (!Array.isArray(records) || records.length === 0) return null\n return records\n}\n\nfunction normalizeFailedRecords(\n failed: AiPendingActionFailedRecord[] | null | undefined,\n): AiPendingActionFailedRecord[] | null {\n if (!Array.isArray(failed) || failed.length === 0) return null\n return failed\n}\n\nexport default AiPendingActionRepository\n"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport { AiPendingAction } from '../entities'\nimport {\n AI_PENDING_ACTION_ALLOWED_TRANSITIONS,\n AiPendingActionStateError,\n resolveAiPendingActionTtlSeconds,\n type AiPendingActionExecutionResult,\n type AiPendingActionFailedRecord,\n type AiPendingActionFieldDiff,\n type AiPendingActionQueueMode,\n type AiPendingActionRecordDiff,\n type AiPendingActionStatus,\n} from '../../lib/pending-action-types'\n\nexport interface AiPendingActionContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface AiPendingActionCreateInput {\n agentId: string\n toolName: string\n idempotencyKey: string\n createdByUserId: string\n normalizedInput: Record<string, unknown>\n conversationId?: string | null\n targetEntityType?: string | null\n targetRecordId?: string | null\n fieldDiff?: AiPendingActionFieldDiff[]\n records?: AiPendingActionRecordDiff[] | null\n sideEffectsSummary?: string | null\n recordVersion?: string | null\n attachmentIds?: string[]\n queueMode?: AiPendingActionQueueMode\n /** Optional explicit TTL in seconds; overrides the env/default TTL. */\n ttlSeconds?: number\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiPendingActionSetStatusExtra {\n resolvedByUserId?: string | null\n executionResult?: AiPendingActionExecutionResult | null\n failedRecords?: AiPendingActionFailedRecord[] | null\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\n/**\n * Persistent store for the Phase 3 WS-C mutation approval gate (Step 5.5).\n *\n * Responsibilities:\n * - Create new pending rows with a TTL-derived `expiresAt`, honoring\n * idempotency within the window (same `idempotencyKey` returns the same\n * row as long as it is still `pending`; any terminal state mints a new row).\n * - Tenant-scoped lookups for the confirm/cancel/reconnect routes and the\n * in-app UI's \"open actions\" list.\n * - State-machine enforcement: `setStatus` rejects illegal transitions via\n * {@link AiPendingActionStateError}. The runtime callers translate this\n * to a 409 Conflict response.\n * - `listExpired` for the cleanup worker (Step 5.12).\n *\n * Every read goes through `findOneWithDecryption` / `findWithDecryption`\n * because `normalized_input`, `field_diff` and `records` are declared in the\n * module's `encryption.ts` and are encrypted at rest when tenant data\n * encryption is enabled.\n */\nexport class AiPendingActionRepository {\n constructor(private readonly em: EntityManager) {}\n\n async create(\n input: AiPendingActionCreateInput,\n ctx: AiPendingActionContext,\n ): Promise<AiPendingAction> {\n if (!ctx?.tenantId) {\n throw new Error('AiPendingActionRepository.create requires tenantId')\n }\n if (!input?.agentId) {\n throw new Error('AiPendingActionRepository.create requires agentId')\n }\n if (!input?.toolName) {\n throw new Error('AiPendingActionRepository.create requires toolName')\n }\n if (!input?.idempotencyKey) {\n throw new Error(\n 'AiPendingActionRepository.create requires idempotencyKey',\n )\n }\n if (!input?.createdByUserId) {\n throw new Error(\n 'AiPendingActionRepository.create requires createdByUserId',\n )\n }\n\n const now = input.now ?? new Date()\n const ttlSeconds = Math.max(\n 1,\n Math.floor(\n typeof input.ttlSeconds === 'number' && Number.isFinite(input.ttlSeconds)\n ? input.ttlSeconds\n : resolveAiPendingActionTtlSeconds(),\n ),\n )\n const expiresAt = new Date(now.getTime() + ttlSeconds * 1000)\n\n return this.em.transactional(async (tx) => {\n const existing = await findOneWithDecryption<AiPendingAction>(\n tx as unknown as EntityManager,\n AiPendingAction,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n idempotencyKey: input.idempotencyKey,\n } as any,\n { orderBy: { createdAt: 'desc' } as any },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (existing && existing.status === 'pending') {\n return existing\n }\n // Terminal stale row would collide on the unique\n // `(tenantId, organizationId, idempotencyKey)` constraint when we\n // try to insert a fresh one with the same hash \u2014 and that exact\n // collision happens whenever the operator clicks \"Fix with AI\"\n // and the model retries the SAME tool with the SAME args. Remove\n // the stale row first so a retry can always proceed; success rows\n // stay (they represent a real, applied change), and failed /\n // cancelled / expired rows are cleared because they're blocking\n // exactly the recovery flow they were created to enable.\n if (\n existing &&\n (existing.status === 'failed' ||\n existing.status === 'cancelled' ||\n existing.status === 'expired')\n ) {\n await tx.remove(existing).flush()\n }\n const row = tx.create(AiPendingAction, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n agentId: input.agentId,\n toolName: input.toolName,\n conversationId: input.conversationId ?? null,\n targetEntityType: input.targetEntityType ?? null,\n targetRecordId: input.targetRecordId ?? null,\n normalizedInput: input.normalizedInput ?? {},\n fieldDiff: Array.isArray(input.fieldDiff) ? input.fieldDiff : [],\n records: normalizeRecords(input.records),\n failedRecords: null,\n sideEffectsSummary: input.sideEffectsSummary ?? null,\n recordVersion: input.recordVersion ?? null,\n attachmentIds: Array.isArray(input.attachmentIds)\n ? input.attachmentIds\n : [],\n idempotencyKey: input.idempotencyKey,\n createdByUserId: input.createdByUserId,\n status: 'pending' as AiPendingActionStatus,\n queueMode: (input.queueMode ?? 'inline') as AiPendingActionQueueMode,\n executionResult: null,\n createdAt: now,\n expiresAt,\n resolvedAt: null,\n resolvedByUserId: null,\n } as unknown as AiPendingAction)\n await tx.persist(row).flush()\n return row\n })\n }\n\n async getById(\n id: string,\n ctx: AiPendingActionContext,\n ): Promise<AiPendingAction | null> {\n if (!id || !ctx?.tenantId) return null\n const row = await findOneWithDecryption<AiPendingAction>(\n this.em,\n AiPendingAction,\n {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return row ?? null\n }\n\n async listPendingForAgent(\n agentId: string,\n ctx: AiPendingActionContext,\n limit: number = 50,\n ): Promise<AiPendingAction[]> {\n if (!agentId || !ctx?.tenantId) return []\n const capped = Math.max(1, Math.min(Math.floor(limit), 200))\n const rows = await findWithDecryption<AiPendingAction>(\n this.em,\n AiPendingAction,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n agentId,\n status: 'pending',\n } as any,\n {\n orderBy: { createdAt: 'desc' } as any,\n limit: capped,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return rows\n }\n\n async setStatus(\n id: string,\n nextStatus: AiPendingActionStatus,\n ctx: AiPendingActionContext,\n extra?: AiPendingActionSetStatusExtra,\n ): Promise<AiPendingAction> {\n if (!ctx?.tenantId) {\n throw new Error('AiPendingActionRepository.setStatus requires tenantId')\n }\n if (!id) {\n throw new Error('AiPendingActionRepository.setStatus requires id')\n }\n return this.em.transactional(async (tx) => {\n const existing = await findOneWithDecryption<AiPendingAction>(\n tx as unknown as EntityManager,\n AiPendingAction,\n {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!existing) {\n throw new Error(`AiPendingAction not found: ${id}`)\n }\n if (existing.status === nextStatus) {\n return existing\n }\n const allowed = AI_PENDING_ACTION_ALLOWED_TRANSITIONS[existing.status] ?? []\n if (!allowed.includes(nextStatus)) {\n throw new AiPendingActionStateError(existing.status, nextStatus)\n }\n const now = extra?.now ?? new Date()\n existing.status = nextStatus\n if (\n nextStatus === 'confirmed' ||\n nextStatus === 'cancelled' ||\n nextStatus === 'expired' ||\n nextStatus === 'failed'\n ) {\n existing.resolvedAt = existing.resolvedAt ?? now\n if (extra && Object.prototype.hasOwnProperty.call(extra, 'resolvedByUserId')) {\n existing.resolvedByUserId = extra.resolvedByUserId ?? null\n } else if (nextStatus === 'expired') {\n existing.resolvedByUserId = null\n }\n }\n if (extra && Object.prototype.hasOwnProperty.call(extra, 'executionResult')) {\n existing.executionResult = extra.executionResult ?? null\n }\n if (extra && Object.prototype.hasOwnProperty.call(extra, 'failedRecords')) {\n existing.failedRecords = normalizeFailedRecords(extra.failedRecords)\n }\n await tx.persist(existing).flush()\n return existing\n })\n }\n\n async listExpired(\n ctx: AiPendingActionContext,\n now: Date,\n limit: number = 100,\n ): Promise<AiPendingAction[]> {\n if (!ctx?.tenantId) return []\n const capped = Math.max(1, Math.min(Math.floor(limit), 500))\n const rows = await findWithDecryption<AiPendingAction>(\n this.em,\n AiPendingAction,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n status: 'pending',\n expiresAt: { $lt: now } as any,\n } as any,\n {\n orderBy: { expiresAt: 'asc' } as any,\n limit: capped,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return rows\n }\n}\n\nfunction normalizeRecords(\n records: AiPendingActionRecordDiff[] | null | undefined,\n): AiPendingActionRecordDiff[] | null {\n if (!Array.isArray(records) || records.length === 0) return null\n return records\n}\n\nfunction normalizeFailedRecords(\n failed: AiPendingActionFailedRecord[] | null | undefined,\n): AiPendingActionFailedRecord[] | null {\n if (!Array.isArray(failed) || failed.length === 0) return null\n return failed\n}\n\nexport default AiPendingActionRepository\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,31 @@
1
+ const defaultEncryptionMaps = [
2
+ {
3
+ entityId: "ai_assistant:ai_chat_message",
4
+ fields: [
5
+ { field: "content" },
6
+ { field: "ui_parts" },
7
+ { field: "files_metadata" },
8
+ { field: "metadata" }
9
+ ]
10
+ },
11
+ {
12
+ entityId: "ai_assistant:ai_chat_conversation",
13
+ fields: [
14
+ { field: "title" }
15
+ ]
16
+ },
17
+ {
18
+ entityId: "ai_assistant:ai_pending_action",
19
+ fields: [
20
+ { field: "normalized_input" },
21
+ { field: "field_diff" },
22
+ { field: "records" }
23
+ ]
24
+ }
25
+ ];
26
+ var encryption_default = defaultEncryptionMaps;
27
+ export {
28
+ encryption_default as default,
29
+ defaultEncryptionMaps
30
+ };
31
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/ai_assistant/encryption.ts"],
4
+ "sourcesContent": ["import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'\n\nexport const defaultEncryptionMaps: ModuleEncryptionMap[] = [\n {\n entityId: 'ai_assistant:ai_chat_message',\n fields: [\n { field: 'content' },\n { field: 'ui_parts' },\n { field: 'files_metadata' },\n { field: 'metadata' },\n ],\n },\n {\n entityId: 'ai_assistant:ai_chat_conversation',\n fields: [\n { field: 'title' },\n ],\n },\n {\n entityId: 'ai_assistant:ai_pending_action',\n fields: [\n { field: 'normalized_input' },\n { field: 'field_diff' },\n { field: 'records' },\n ],\n },\n]\n\nexport default defaultEncryptionMaps\n"],
5
+ "mappings": "AAEO,MAAM,wBAA+C;AAAA,EAC1D;AAAA,IACE,UAAU;AAAA,IACV,QAAQ;AAAA,MACN,EAAE,OAAO,UAAU;AAAA,MACnB,EAAE,OAAO,WAAW;AAAA,MACpB,EAAE,OAAO,iBAAiB;AAAA,MAC1B,EAAE,OAAO,WAAW;AAAA,IACtB;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,QAAQ;AAAA,MACN,EAAE,OAAO,QAAQ;AAAA,IACnB;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,QAAQ;AAAA,MACN,EAAE,OAAO,mBAAmB;AAAA,MAC5B,EAAE,OAAO,aAAa;AAAA,MACtB,EAAE,OAAO,UAAU;AAAA,IACrB;AAAA,EACF;AACF;AAEA,IAAO,qBAAQ;",
6
+ "names": []
7
+ }
@@ -32,6 +32,13 @@ function buildUrl(path, query) {
32
32
  }
33
33
  return url;
34
34
  }
35
+ function decodePathForManifestMatch(path) {
36
+ try {
37
+ return path.split("/").map((segment) => decodeURIComponent(segment)).join("/");
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
35
42
  function buildAuthEnvelope(ctx) {
36
43
  const userId = ctx.userId;
37
44
  if (!userId) {
@@ -139,6 +146,18 @@ function createAiApiOperationRunner(ctx, options = {}) {
139
146
  return failure(400, message);
140
147
  }
141
148
  const path = normalizePath(request.path);
149
+ const manifestPath = decodePathForManifestMatch(path);
150
+ if (manifestPath === null) {
151
+ return failure(400, `Invalid path encoding for ${method} ${path}`);
152
+ }
153
+ const url = buildUrl(path, request.query);
154
+ const headers = new Headers();
155
+ const requestInit = { method, headers };
156
+ if (request.body !== void 0 && method !== "GET") {
157
+ headers.set("content-type", "application/json");
158
+ requestInit.body = JSON.stringify(request.body);
159
+ }
160
+ const syntheticRequest = new Request(url, requestInit);
142
161
  let routes;
143
162
  try {
144
163
  routes = await loadManifest();
@@ -146,7 +165,7 @@ function createAiApiOperationRunner(ctx, options = {}) {
146
165
  const message = error instanceof Error ? error.message : "Failed to load API route manifest";
147
166
  return failure(500, `Operation runner manifest unavailable: ${message}`);
148
167
  }
149
- const match = findApiRouteManifestMatch(routes, method, path);
168
+ const match = findApiRouteManifestMatch(routes, method, manifestPath, syntheticRequest);
150
169
  if (!match) {
151
170
  return failure(
152
171
  404,
@@ -190,14 +209,6 @@ function createAiApiOperationRunner(ctx, options = {}) {
190
209
  { toolFeatures, routeFeatures }
191
210
  );
192
211
  }
193
- const url = buildUrl(path, request.query);
194
- const headers = new Headers();
195
- const requestInit = { method, headers };
196
- if (request.body !== void 0 && method !== "GET") {
197
- headers.set("content-type", "application/json");
198
- requestInit.body = JSON.stringify(request.body);
199
- }
200
- const syntheticRequest = new Request(url, requestInit);
201
212
  attachTrustedAuthContext(syntheticRequest, buildAuthEnvelope(ctx));
202
213
  let response;
203
214
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/ai-api-operation-runner.ts"],
4
- "sourcesContent": ["// Phase 1 of spec 2026-04-27-ai-tools-api-backed-dry-refactor.md.\n//\n// In-process API operation runner used by typed AI tools to reuse existing\n// API route handlers without HTTP, fetch, or a second RBAC pass. The runner\n// resolves the matched route entry from the generated `apiRoutes` manifest,\n// validates that the route is documented (`openApi`) and that mutation routes\n// declare `requiredFeatures`, asserts the route's required features are\n// covered by the tool definition, then invokes the route handler directly with\n// a synthetic Request that carries a Symbol-keyed trusted-auth envelope so the\n// shared auth resolver short-circuits cookie/JWT/API-key parsing.\nimport {\n attachTrustedAuthContext,\n type AuthContext,\n type TrustedAuthContextEnvelope,\n} from '@open-mercato/shared/lib/auth/server'\nimport {\n findApiRouteManifestMatch,\n getApiRouteManifests,\n type ApiRouteManifestEntry,\n type RouteMatchParams,\n} from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { AiToolDefinition, McpToolContext } from './types'\n\nexport type AiApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\nconst MUTATION_METHODS: ReadonlySet<AiApiHttpMethod> = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nconst SYNTHETIC_ORIGIN = 'http://internal.local'\n\nexport type AiApiOperationRequest = {\n method: AiApiHttpMethod\n path: string\n query?: Record<string, string | number | boolean | null | undefined>\n body?: Record<string, unknown>\n allowFeaturelessMutation?: boolean\n}\n\nexport type AiApiOperationResponse<T = unknown> = {\n success: boolean\n statusCode: number\n data?: T\n error?: string\n details?: unknown\n}\n\nexport interface AiToolExecutionContext extends McpToolContext {\n tool: AiToolDefinition\n}\n\nexport type AiApiOperationRunnerOptions = {\n /** Optional override of the API route manifest (used by tests). */\n apiRoutes?: ApiRouteManifestEntry[]\n /** Custom loader for the API route manifest (used by tests). */\n loadApiRoutes?: () => Promise<ApiRouteManifestEntry[]>\n}\n\nexport type AiApiOperationRunner = {\n run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>>\n}\n\ntype ResolvedHandler = (req: Request, ctx?: { params: RouteMatchParams }) => Promise<Response> | Response\n\ntype LoadedRouteModule = Record<string, unknown>\n\ntype MethodMetadata = {\n requireAuth?: boolean\n requireFeatures?: string[]\n}\n\nfunction normalizeMethod(method: string): AiApiHttpMethod {\n const upper = method.toUpperCase()\n if (upper === 'GET' || upper === 'POST' || upper === 'PUT' || upper === 'PATCH' || upper === 'DELETE') {\n return upper\n }\n throw new Error(`Unsupported method \"${method}\"`)\n}\n\n// Exported for unit testing \u2014 see __tests__/ai-api-operation-runner.test.ts.\n// `path` arrives from agent tool inputs (LLM-controlled), so trailing-slash\n// stripping is implemented as a linear character-code scan rather than the\n// /\\/+$/ regex (CodeQL js/polynomial-redos: regex exhibits polynomial\n// backtracking on long runs of `/`). The loop is O(n) regardless of input.\nexport function normalizePath(path: string): string {\n if (typeof path !== 'string' || path.length === 0) return '/'\n const trimmed = path.startsWith('/') ? path : `/${path}`\n let end = trimmed.length\n while (end > 1 && trimmed.charCodeAt(end - 1) === 47 /* '/' */) end--\n return trimmed.slice(0, end)\n}\n\nfunction buildUrl(path: string, query?: AiApiOperationRequest['query']): URL {\n const url = new URL(`${SYNTHETIC_ORIGIN}/api${normalizePath(path)}`)\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined) continue\n url.searchParams.append(key, String(value))\n }\n }\n return url\n}\n\nfunction buildAuthEnvelope(ctx: AiToolExecutionContext): TrustedAuthContextEnvelope {\n const userId = ctx.userId\n if (!userId) {\n return { auth: null, status: 'invalid' }\n }\n // MCP api-key contexts use an `api_key:<uuid>` subject (see auth.ts). Surface\n // the bare key UUID as `keyId` so downstream actor logging resolves to a valid\n // UUID \u2014 the CRUD factory computes `actorUserId = auth.keyId ?? auth.sub`, and\n // the access-log schema rejects a non-UUID `actorUserId`. This matches real\n // HTTP api-key auth, where `keyId` is the api key record id.\n const apiKeyId = userId.startsWith('api_key:')\n ? userId.slice('api_key:'.length)\n : undefined\n const auth: AuthContext = {\n sub: userId,\n userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n roles: [],\n isSuperAdmin: ctx.isSuperAdmin,\n ...(apiKeyId ? { keyId: apiKeyId } : {}),\n }\n return { auth, status: 'authenticated' }\n}\n\nfunction pickHandler(entry: ApiRouteManifestEntry, mod: LoadedRouteModule, method: AiApiHttpMethod): ResolvedHandler | null {\n const direct = mod[method]\n if (typeof direct === 'function') return direct as ResolvedHandler\n if (entry.kind === 'legacy') {\n const fallback = mod.default ?? mod.handler\n if (typeof fallback === 'function') return fallback as ResolvedHandler\n }\n return null\n}\n\nfunction extractMethodMetadata(metadata: unknown, method: AiApiHttpMethod): MethodMetadata | null {\n if (!metadata || typeof metadata !== 'object') return null\n const record = metadata as Record<string, unknown>\n const perMethod = record[method]\n const source: Record<string, unknown> = perMethod && typeof perMethod === 'object'\n ? (perMethod as Record<string, unknown>)\n : record\n const result: MethodMetadata = {}\n if (typeof source.requireAuth === 'boolean') {\n result.requireAuth = source.requireAuth\n }\n if (Array.isArray(source.requireFeatures)) {\n const features = source.requireFeatures.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n if (features.length > 0) result.requireFeatures = features\n }\n return result\n}\n\nasync function readResponseBody(res: Response): Promise<unknown> {\n const contentType = res.headers.get('content-type') ?? ''\n if (contentType.includes('application/json')) {\n try {\n return await res.json()\n } catch {\n return null\n }\n }\n try {\n const text = await res.text()\n if (!text) return null\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n } catch {\n return null\n }\n}\n\nfunction failure(statusCode: number, error: string, details?: unknown): AiApiOperationResponse {\n const response: AiApiOperationResponse = { success: false, statusCode, error }\n if (details !== undefined) response.details = details\n return response\n}\n\nfunction normalizeError(body: unknown): { error: string; details?: unknown } {\n if (body && typeof body === 'object' && !Array.isArray(body)) {\n const record = body as Record<string, unknown>\n const message = typeof record.error === 'string'\n ? record.error\n : typeof record.message === 'string'\n ? record.message\n : null\n if (message) {\n const { error: _ignored, message: _ignoredMessage, ...rest } = record\n void _ignored\n void _ignoredMessage\n return Object.keys(rest).length > 0 ? { error: message, details: rest } : { error: message }\n }\n }\n if (typeof body === 'string' && body.length > 0) {\n return { error: body }\n }\n return { error: 'Request failed' }\n}\n\nexport function createAiApiOperationRunner(\n ctx: AiToolExecutionContext,\n options: AiApiOperationRunnerOptions = {},\n): AiApiOperationRunner {\n let manifestPromise: Promise<ApiRouteManifestEntry[]> | null = null\n\n const loadManifest = async (): Promise<ApiRouteManifestEntry[]> => {\n if (options.apiRoutes) return options.apiRoutes\n if (manifestPromise) return manifestPromise\n const loader = options.loadApiRoutes ?? defaultLoadApiRoutes\n manifestPromise = loader()\n try {\n return await manifestPromise\n } catch (error) {\n manifestPromise = null\n throw error\n }\n }\n\n return {\n async run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>> {\n let method: AiApiHttpMethod\n try {\n method = normalizeMethod(request.method)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Invalid method'\n return failure(400, message) as AiApiOperationResponse<T>\n }\n const path = normalizePath(request.path)\n\n let routes: ApiRouteManifestEntry[]\n try {\n routes = await loadManifest()\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load API route manifest'\n return failure(500, `Operation runner manifest unavailable: ${message}`) as AiApiOperationResponse<T>\n }\n\n const match = findApiRouteManifestMatch(routes, method, path)\n if (!match) {\n return failure(\n 404,\n `No documented API route matches ${method} ${path}`,\n ) as AiApiOperationResponse<T>\n }\n\n let mod: LoadedRouteModule\n try {\n mod = (await match.route.load()) as LoadedRouteModule\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load route module'\n return failure(500, `Failed to load route module: ${message}`) as AiApiOperationResponse<T>\n }\n\n if (!('openApi' in mod) || mod.openApi === undefined || mod.openApi === null) {\n return failure(\n 501,\n `Route ${method} ${path} is undocumented (missing openApi export); refusing to call from AI tool`,\n ) as AiApiOperationResponse<T>\n }\n\n const handler = pickHandler(match.route, mod, method)\n if (!handler) {\n return failure(\n 405,\n `Route ${path} does not export a handler for method ${method}`,\n ) as AiApiOperationResponse<T>\n }\n\n const methodMetadata = extractMethodMetadata(mod.metadata, method)\n const routeFeatures = methodMetadata?.requireFeatures ?? []\n const isMutation = MUTATION_METHODS.has(method)\n\n if (isMutation && routeFeatures.length === 0 && !request.allowFeaturelessMutation) {\n return failure(\n 403,\n `Mutation route ${method} ${path} declares no requiredFeatures; refusing to call without allowFeaturelessMutation opt-in`,\n ) as AiApiOperationResponse<T>\n }\n\n const toolFeatures = ctx.tool.requiredFeatures ?? []\n if (routeFeatures.length > 0 && !hasAllFeatures(toolFeatures, routeFeatures)) {\n return failure(\n 403,\n `AI tool \"${ctx.tool.name}\" requiredFeatures do not cover route ${method} ${path} requiredFeatures`,\n { toolFeatures, routeFeatures },\n ) as AiApiOperationResponse<T>\n }\n\n const url = buildUrl(path, request.query)\n const headers = new Headers()\n const requestInit: RequestInit = { method, headers }\n if (request.body !== undefined && method !== 'GET') {\n headers.set('content-type', 'application/json')\n requestInit.body = JSON.stringify(request.body)\n }\n\n const syntheticRequest = new Request(url, requestInit)\n attachTrustedAuthContext(syntheticRequest, buildAuthEnvelope(ctx))\n\n let response: Response\n try {\n response = await handler(syntheticRequest, { params: match.params })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Route handler threw'\n return failure(500, message) as AiApiOperationResponse<T>\n }\n\n const status = response.status\n const body = await readResponseBody(response)\n if (status >= 200 && status < 300) {\n return {\n success: true,\n statusCode: status,\n data: body as T,\n }\n }\n const normalized = normalizeError(body)\n return {\n success: false,\n statusCode: status,\n error: normalized.error,\n ...(normalized.details !== undefined ? { details: normalized.details } : {}),\n }\n },\n }\n}\n\nasync function defaultLoadApiRoutes(): Promise<ApiRouteManifestEntry[]> {\n const registered = getApiRouteManifests()\n if (registered.length === 0) {\n throw new Error(\n 'No API route manifest registered. Call registerApiRouteManifests(...) at app bootstrap or pass apiRoutes/loadApiRoutes to createAiApiOperationRunner.',\n )\n }\n return registered\n}\n\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// Phase 1 of spec 2026-04-27-ai-tools-api-backed-dry-refactor.md.\n//\n// In-process API operation runner used by typed AI tools to reuse existing\n// API route handlers without HTTP, fetch, or a second RBAC pass. The runner\n// resolves the matched route entry from the generated `apiRoutes` manifest,\n// validates that the route is documented (`openApi`) and that mutation routes\n// declare `requiredFeatures`, asserts the route's required features are\n// covered by the tool definition, then invokes the route handler directly with\n// a synthetic Request that carries a Symbol-keyed trusted-auth envelope so the\n// shared auth resolver short-circuits cookie/JWT/API-key parsing.\nimport {\n attachTrustedAuthContext,\n type AuthContext,\n type TrustedAuthContextEnvelope,\n} from '@open-mercato/shared/lib/auth/server'\nimport {\n findApiRouteManifestMatch,\n getApiRouteManifests,\n type ApiRouteManifestEntry,\n type RouteMatchParams,\n} from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { AiToolDefinition, McpToolContext } from './types'\n\nexport type AiApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\nconst MUTATION_METHODS: ReadonlySet<AiApiHttpMethod> = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nconst SYNTHETIC_ORIGIN = 'http://internal.local'\n\nexport type AiApiOperationRequest = {\n method: AiApiHttpMethod\n path: string\n query?: Record<string, string | number | boolean | null | undefined>\n body?: Record<string, unknown>\n allowFeaturelessMutation?: boolean\n}\n\nexport type AiApiOperationResponse<T = unknown> = {\n success: boolean\n statusCode: number\n data?: T\n error?: string\n details?: unknown\n}\n\nexport interface AiToolExecutionContext extends McpToolContext {\n tool: AiToolDefinition\n}\n\nexport type AiApiOperationRunnerOptions = {\n /** Optional override of the API route manifest (used by tests). */\n apiRoutes?: ApiRouteManifestEntry[]\n /** Custom loader for the API route manifest (used by tests). */\n loadApiRoutes?: () => Promise<ApiRouteManifestEntry[]>\n}\n\nexport type AiApiOperationRunner = {\n run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>>\n}\n\ntype ResolvedHandler = (req: Request, ctx?: { params: RouteMatchParams }) => Promise<Response> | Response\n\ntype LoadedRouteModule = Record<string, unknown>\n\ntype MethodMetadata = {\n requireAuth?: boolean\n requireFeatures?: string[]\n}\n\nfunction normalizeMethod(method: string): AiApiHttpMethod {\n const upper = method.toUpperCase()\n if (upper === 'GET' || upper === 'POST' || upper === 'PUT' || upper === 'PATCH' || upper === 'DELETE') {\n return upper\n }\n throw new Error(`Unsupported method \"${method}\"`)\n}\n\n// Exported for unit testing \u2014 see __tests__/ai-api-operation-runner.test.ts.\n// `path` arrives from agent tool inputs (LLM-controlled), so trailing-slash\n// stripping is implemented as a linear character-code scan rather than the\n// /\\/+$/ regex (CodeQL js/polynomial-redos: regex exhibits polynomial\n// backtracking on long runs of `/`). The loop is O(n) regardless of input.\nexport function normalizePath(path: string): string {\n if (typeof path !== 'string' || path.length === 0) return '/'\n const trimmed = path.startsWith('/') ? path : `/${path}`\n let end = trimmed.length\n while (end > 1 && trimmed.charCodeAt(end - 1) === 47 /* '/' */) end--\n return trimmed.slice(0, end)\n}\n\nfunction buildUrl(path: string, query?: AiApiOperationRequest['query']): URL {\n const url = new URL(`${SYNTHETIC_ORIGIN}/api${normalizePath(path)}`)\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined) continue\n url.searchParams.append(key, String(value))\n }\n }\n return url\n}\n\nfunction decodePathForManifestMatch(path: string): string | null {\n try {\n return path.split('/').map((segment) => decodeURIComponent(segment)).join('/')\n } catch {\n return null\n }\n}\n\nfunction buildAuthEnvelope(ctx: AiToolExecutionContext): TrustedAuthContextEnvelope {\n const userId = ctx.userId\n if (!userId) {\n return { auth: null, status: 'invalid' }\n }\n // MCP api-key contexts use an `api_key:<uuid>` subject (see auth.ts). Surface\n // the bare key UUID as `keyId` so downstream actor logging resolves to a valid\n // UUID \u2014 the CRUD factory computes `actorUserId = auth.keyId ?? auth.sub`, and\n // the access-log schema rejects a non-UUID `actorUserId`. This matches real\n // HTTP api-key auth, where `keyId` is the api key record id.\n const apiKeyId = userId.startsWith('api_key:')\n ? userId.slice('api_key:'.length)\n : undefined\n const auth: AuthContext = {\n sub: userId,\n userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n roles: [],\n isSuperAdmin: ctx.isSuperAdmin,\n ...(apiKeyId ? { keyId: apiKeyId } : {}),\n }\n return { auth, status: 'authenticated' }\n}\n\nfunction pickHandler(entry: ApiRouteManifestEntry, mod: LoadedRouteModule, method: AiApiHttpMethod): ResolvedHandler | null {\n const direct = mod[method]\n if (typeof direct === 'function') return direct as ResolvedHandler\n if (entry.kind === 'legacy') {\n const fallback = mod.default ?? mod.handler\n if (typeof fallback === 'function') return fallback as ResolvedHandler\n }\n return null\n}\n\nfunction extractMethodMetadata(metadata: unknown, method: AiApiHttpMethod): MethodMetadata | null {\n if (!metadata || typeof metadata !== 'object') return null\n const record = metadata as Record<string, unknown>\n const perMethod = record[method]\n const source: Record<string, unknown> = perMethod && typeof perMethod === 'object'\n ? (perMethod as Record<string, unknown>)\n : record\n const result: MethodMetadata = {}\n if (typeof source.requireAuth === 'boolean') {\n result.requireAuth = source.requireAuth\n }\n if (Array.isArray(source.requireFeatures)) {\n const features = source.requireFeatures.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n if (features.length > 0) result.requireFeatures = features\n }\n return result\n}\n\nasync function readResponseBody(res: Response): Promise<unknown> {\n const contentType = res.headers.get('content-type') ?? ''\n if (contentType.includes('application/json')) {\n try {\n return await res.json()\n } catch {\n return null\n }\n }\n try {\n const text = await res.text()\n if (!text) return null\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n } catch {\n return null\n }\n}\n\nfunction failure(statusCode: number, error: string, details?: unknown): AiApiOperationResponse {\n const response: AiApiOperationResponse = { success: false, statusCode, error }\n if (details !== undefined) response.details = details\n return response\n}\n\nfunction normalizeError(body: unknown): { error: string; details?: unknown } {\n if (body && typeof body === 'object' && !Array.isArray(body)) {\n const record = body as Record<string, unknown>\n const message = typeof record.error === 'string'\n ? record.error\n : typeof record.message === 'string'\n ? record.message\n : null\n if (message) {\n const { error: _ignored, message: _ignoredMessage, ...rest } = record\n void _ignored\n void _ignoredMessage\n return Object.keys(rest).length > 0 ? { error: message, details: rest } : { error: message }\n }\n }\n if (typeof body === 'string' && body.length > 0) {\n return { error: body }\n }\n return { error: 'Request failed' }\n}\n\nexport function createAiApiOperationRunner(\n ctx: AiToolExecutionContext,\n options: AiApiOperationRunnerOptions = {},\n): AiApiOperationRunner {\n let manifestPromise: Promise<ApiRouteManifestEntry[]> | null = null\n\n const loadManifest = async (): Promise<ApiRouteManifestEntry[]> => {\n if (options.apiRoutes) return options.apiRoutes\n if (manifestPromise) return manifestPromise\n const loader = options.loadApiRoutes ?? defaultLoadApiRoutes\n manifestPromise = loader()\n try {\n return await manifestPromise\n } catch (error) {\n manifestPromise = null\n throw error\n }\n }\n\n return {\n async run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>> {\n let method: AiApiHttpMethod\n try {\n method = normalizeMethod(request.method)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Invalid method'\n return failure(400, message) as AiApiOperationResponse<T>\n }\n const path = normalizePath(request.path)\n const manifestPath = decodePathForManifestMatch(path)\n if (manifestPath === null) {\n return failure(400, `Invalid path encoding for ${method} ${path}`) as AiApiOperationResponse<T>\n }\n\n const url = buildUrl(path, request.query)\n const headers = new Headers()\n const requestInit: RequestInit = { method, headers }\n if (request.body !== undefined && method !== 'GET') {\n headers.set('content-type', 'application/json')\n requestInit.body = JSON.stringify(request.body)\n }\n const syntheticRequest = new Request(url, requestInit)\n\n let routes: ApiRouteManifestEntry[]\n try {\n routes = await loadManifest()\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load API route manifest'\n return failure(500, `Operation runner manifest unavailable: ${message}`) as AiApiOperationResponse<T>\n }\n\n const match = findApiRouteManifestMatch(routes, method, manifestPath, syntheticRequest)\n if (!match) {\n return failure(\n 404,\n `No documented API route matches ${method} ${path}`,\n ) as AiApiOperationResponse<T>\n }\n\n let mod: LoadedRouteModule\n try {\n mod = (await match.route.load()) as LoadedRouteModule\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load route module'\n return failure(500, `Failed to load route module: ${message}`) as AiApiOperationResponse<T>\n }\n\n if (!('openApi' in mod) || mod.openApi === undefined || mod.openApi === null) {\n return failure(\n 501,\n `Route ${method} ${path} is undocumented (missing openApi export); refusing to call from AI tool`,\n ) as AiApiOperationResponse<T>\n }\n\n const handler = pickHandler(match.route, mod, method)\n if (!handler) {\n return failure(\n 405,\n `Route ${path} does not export a handler for method ${method}`,\n ) as AiApiOperationResponse<T>\n }\n\n const methodMetadata = extractMethodMetadata(mod.metadata, method)\n const routeFeatures = methodMetadata?.requireFeatures ?? []\n const isMutation = MUTATION_METHODS.has(method)\n\n if (isMutation && routeFeatures.length === 0 && !request.allowFeaturelessMutation) {\n return failure(\n 403,\n `Mutation route ${method} ${path} declares no requiredFeatures; refusing to call without allowFeaturelessMutation opt-in`,\n ) as AiApiOperationResponse<T>\n }\n\n const toolFeatures = ctx.tool.requiredFeatures ?? []\n if (routeFeatures.length > 0 && !hasAllFeatures(toolFeatures, routeFeatures)) {\n return failure(\n 403,\n `AI tool \"${ctx.tool.name}\" requiredFeatures do not cover route ${method} ${path} requiredFeatures`,\n { toolFeatures, routeFeatures },\n ) as AiApiOperationResponse<T>\n }\n\n attachTrustedAuthContext(syntheticRequest, buildAuthEnvelope(ctx))\n\n let response: Response\n try {\n response = await handler(syntheticRequest, { params: match.params })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Route handler threw'\n return failure(500, message) as AiApiOperationResponse<T>\n }\n\n const status = response.status\n const body = await readResponseBody(response)\n if (status >= 200 && status < 300) {\n return {\n success: true,\n statusCode: status,\n data: body as T,\n }\n }\n const normalized = normalizeError(body)\n return {\n success: false,\n statusCode: status,\n error: normalized.error,\n ...(normalized.details !== undefined ? { details: normalized.details } : {}),\n }\n },\n }\n}\n\nasync function defaultLoadApiRoutes(): Promise<ApiRouteManifestEntry[]> {\n const registered = getApiRouteManifests()\n if (registered.length === 0) {\n throw new Error(\n 'No API route manifest registered. Call registerApiRouteManifests(...) at app bootstrap or pass apiRoutes/loadApiRoutes to createAiApiOperationRunner.',\n )\n }\n return registered\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -58,7 +58,7 @@ async function compileAndImportGenerated(tsPath, options = {}) {
58
58
  sourcemap: false,
59
59
  write: false,
60
60
  logLevel: "silent",
61
- alias: { "@": appRoot }
61
+ plugins: [createAppAliasPlugin(appRoot)]
62
62
  });
63
63
  fs.writeFileSync(jsPath, result.outputFiles[0].text);
64
64
  } else {
@@ -87,6 +87,28 @@ async function compileAndImportGenerated(tsPath, options = {}) {
87
87
  pathToFileURL(jsPath).href
88
88
  );
89
89
  }
90
+ const APP_ALIAS_SUFFIXES = [".ts", ".tsx", "/index.ts", "/index.tsx"];
91
+ function resolveAppAliasPath(appRoot, rest) {
92
+ const bases = rest.startsWith(".mercato/") ? [path.join(appRoot, rest)] : [path.join(appRoot, "src", rest), path.join(appRoot, rest)];
93
+ for (const base of bases) {
94
+ if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
95
+ for (const suffix of APP_ALIAS_SUFFIXES) {
96
+ if (fs.existsSync(base + suffix)) return base + suffix;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ function createAppAliasPlugin(appRoot) {
102
+ return {
103
+ name: "app-alias",
104
+ setup(build) {
105
+ build.onResolve({ filter: /^@\// }, (args) => {
106
+ const rest = args.path.slice("@/".length);
107
+ return { path: resolveAppAliasPath(appRoot, rest) ?? path.join(appRoot, rest) };
108
+ });
109
+ }
110
+ };
111
+ }
90
112
  function isJestRuntime() {
91
113
  return typeof process.env.JEST_WORKER_ID === "string";
92
114
  }
@@ -159,7 +181,11 @@ function rewriteGeneratedAliasImportsForRuntime(source, appRoot, runtime, appLoc
159
181
  const candidate = fs.existsSync(target) ? target : fs.existsSync(target + ".ts") ? target + ".ts" : target;
160
182
  return toRuntimeLiteral(candidate);
161
183
  };
162
- const resolveAlias = (relativePath) => toResolvedLiteral(path.join(appRoot, relativePath));
184
+ const resolveAlias = (relativePath) => {
185
+ const resolved = resolveAppAliasPath(appRoot, relativePath);
186
+ if (resolved !== null) return toRuntimeLiteral(resolved);
187
+ return toResolvedLiteral(path.join(appRoot, relativePath));
188
+ };
163
189
  const resolveRelative = (specifier) => {
164
190
  const artifact = appLocalArtifacts.get(specifier);
165
191
  if (artifact !== void 0) return toRuntimeLiteral(artifact);
@@ -197,9 +223,11 @@ async function ensureApiRouteManifestsRegistered() {
197
223
  export {
198
224
  collectAppLocalSpecifiers,
199
225
  compileAndImportGenerated,
226
+ createAppAliasPlugin,
200
227
  ensureApiRouteManifestsRegistered,
201
228
  escapeUnsafeJsStringChars,
202
229
  findGeneratedFile,
230
+ resolveAppAliasPath,
203
231
  rewriteGeneratedAliasImports
204
232
  };
205
233
  //# sourceMappingURL=generated-registry-loader.js.map