@open-mercato/ai-assistant 0.6.5-develop.5382.1.f542de69af → 0.6.6-develop.5412.1.e2a52b14f0

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.
@@ -46,10 +46,17 @@ function resolveSigner(container) {
46
46
  }
47
47
  async function loadAttachmentRow(em, attachmentId, authContext) {
48
48
  const { Attachment } = await import("@open-mercato/core/modules/attachments/data/entities");
49
+ const where = { id: attachmentId };
50
+ if (!authContext.isSuperAdmin) {
51
+ where.tenantId = authContext.tenantId;
52
+ if (authContext.organizationId != null) {
53
+ where.organizationId = authContext.organizationId;
54
+ }
55
+ }
49
56
  const record = await findOneWithDecryption(
50
57
  em,
51
58
  Attachment,
52
- { id: attachmentId },
59
+ where,
53
60
  void 0,
54
61
  {
55
62
  tenantId: authContext.tenantId,
@@ -74,8 +81,8 @@ async function loadAttachmentRow(em, attachmentId, authContext) {
74
81
  }
75
82
  function rowBelongsToCaller(row, authContext) {
76
83
  if (authContext.isSuperAdmin) return true;
77
- if (row.tenantId && row.tenantId !== authContext.tenantId) return false;
78
- if (row.organizationId && row.organizationId !== authContext.organizationId) return false;
84
+ if (authContext.tenantId == null || row.tenantId !== authContext.tenantId) return false;
85
+ if (authContext.organizationId != null && row.organizationId !== authContext.organizationId) return false;
79
86
  return true;
80
87
  }
81
88
  async function readAttachmentBytes(row) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/attachment-parts.ts"],
4
- "sourcesContent": ["import { promises as fs } from 'fs'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type {\n AiAgentAcceptedMediaType,\n AiAgentDefinition,\n} from './ai-agent-definition'\nimport type {\n AiChatRequestContext,\n AiResolvedAttachmentPart,\n} from './attachment-bridge-types'\n\n// Provider-native inline byte limit. Most AI providers accept inline image/PDF\n// payloads comfortably under 4 MB; anything larger SHOULD travel as a short-lived\n// signed URL (see AttachmentSigner below). Above this ceiling and with no signer\n// configured, the helper downgrades to `metadata-only` so the model at least sees\n// that the attachment exists.\nconst DEFAULT_MAX_INLINE_BYTES = 4 * 1024 * 1024\n\n// Extracted text cap. The `content` column on the `attachments` table is the\n// OCR/text-extraction output; we forward it verbatim up to this character count\n// so the system prompt + messages combined do not blow past model context\n// limits. Truncation is signaled to the model via a trailing `[... truncated]`\n// marker.\nconst DEFAULT_MAX_TEXT_CHARS = 64 * 1024\n\n/**\n * Optional attachment-signer. When the DI container resolves a value under\n * `attachmentSigner`, the resolver uses it to mint a short-lived URL for\n * images/PDFs that exceed the inline-bytes threshold. Phase 1 does not ship a\n * concrete signer; the hook exists so the `signed-url` branch of\n * {@link AiResolvedAttachmentPart} is reachable as soon as a provider wires one\n * up without requiring another runtime change.\n */\nexport interface AttachmentSigner {\n sign(input: {\n attachmentId: string\n fileName: string\n mediaType: string\n tenantId: string | null\n organizationId: string | null\n }): Promise<string | null>\n}\n\nexport interface ResolveAttachmentPartsInput {\n attachmentIds: readonly string[]\n authContext: AiChatRequestContext\n acceptedMediaTypes?: readonly AiAgentAcceptedMediaType[]\n container?: AwilixContainer\n /**\n * Optional override for the inline bytes threshold. Callers SHOULD leave\n * this untouched; the default tracks a safe cross-provider ceiling.\n */\n maxInlineBytes?: number\n /**\n * Optional override for the extracted-text character cap.\n */\n maxTextChars?: number\n}\n\nfunction classifyMediaType(mimeType: string | null | undefined): AiAgentAcceptedMediaType {\n const normalized = (mimeType ?? '').toLowerCase().trim()\n if (normalized.startsWith('image/')) return 'image'\n if (normalized === 'application/pdf') return 'pdf'\n return 'file'\n}\n\nfunction isTextLikeMime(mimeType: string | null | undefined): boolean {\n const normalized = (mimeType ?? '').toLowerCase().trim()\n if (!normalized) return false\n if (normalized.startsWith('text/')) return true\n if (normalized === 'application/json') return true\n if (normalized === 'application/xml') return true\n if (normalized === 'application/x-yaml' || normalized === 'text/yaml') return true\n if (normalized === 'application/csv') return true\n return false\n}\n\nfunction truncateText(value: string, maxChars: number): string {\n if (value.length <= maxChars) return value\n return `${value.slice(0, Math.max(0, maxChars - 16))}\\n[... truncated]`\n}\n\nfunction resolveEm(container: AwilixContainer | undefined): EntityManager | null {\n if (!container) return null\n try {\n const candidate = container.resolve('em') as EntityManager | undefined\n return candidate ?? null\n } catch {\n return null\n }\n}\n\nfunction resolveSigner(container: AwilixContainer | undefined): AttachmentSigner | null {\n if (!container) return null\n try {\n const candidate = container.resolve('attachmentSigner') as AttachmentSigner | undefined\n if (candidate && typeof candidate.sign === 'function') {\n return candidate\n }\n } catch {\n return null\n }\n return null\n}\n\ntype AttachmentRow = {\n id: string\n entityId: string\n fileName: string\n mimeType: string\n fileSize: number\n storagePath: string\n storageDriver: string\n partitionCode: string\n tenantId: string | null\n organizationId: string | null\n content: string | null\n}\n\nasync function loadAttachmentRow(\n em: EntityManager,\n attachmentId: string,\n authContext: AiChatRequestContext,\n): Promise<AttachmentRow | null> {\n // Attachment entity is imported lazily to keep ai-assistant isomorphic \u2014 the\n // core package owns the MikroORM metadata and is the only place tests would\n // need to bootstrap for real DB access.\n const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')\n const record = await findOneWithDecryption(\n em,\n Attachment as never,\n { id: attachmentId } as never,\n undefined,\n {\n tenantId: authContext.tenantId,\n organizationId: authContext.organizationId,\n },\n )\n if (!record) return null\n const row = record as unknown as AttachmentRow\n return {\n id: row.id,\n entityId: row.entityId,\n fileName: row.fileName,\n mimeType: row.mimeType,\n fileSize: row.fileSize,\n storagePath: row.storagePath,\n storageDriver: row.storageDriver,\n partitionCode: row.partitionCode,\n tenantId: row.tenantId ?? null,\n organizationId: row.organizationId ?? null,\n content: row.content ?? null,\n }\n}\n\nfunction rowBelongsToCaller(row: AttachmentRow, authContext: AiChatRequestContext): boolean {\n if (authContext.isSuperAdmin) return true\n // Tenant scope: if the record is tenant-scoped, it MUST match the caller tenant.\n if (row.tenantId && row.tenantId !== authContext.tenantId) return false\n // Organization scope: if the record is org-scoped, it MUST match the caller org.\n if (row.organizationId && row.organizationId !== authContext.organizationId) return false\n return true\n}\n\nasync function readAttachmentBytes(row: AttachmentRow): Promise<Uint8Array | null> {\n const { resolveAttachmentAbsolutePath } = await import(\n '@open-mercato/core/modules/attachments/lib/storage'\n )\n const absolutePath = resolveAttachmentAbsolutePath(\n row.partitionCode,\n row.storagePath,\n row.storageDriver,\n )\n try {\n const buffer = await fs.readFile(absolutePath)\n return new Uint8Array(buffer)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to read attachment ${row.id} from storage; falling back to metadata-only:`,\n error,\n )\n return null\n }\n}\n\nasync function classifyAndBuildPart(\n row: AttachmentRow,\n mediaClass: AiAgentAcceptedMediaType,\n maxInlineBytes: number,\n maxTextChars: number,\n signer: AttachmentSigner | null,\n authContext: AiChatRequestContext,\n): Promise<AiResolvedAttachmentPart> {\n const base: Pick<AiResolvedAttachmentPart, 'attachmentId' | 'fileName' | 'mediaType'> = {\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType || 'application/octet-stream',\n }\n\n // Text-like generic files \u2014 use the pre-extracted content column if present.\n if (mediaClass === 'file' && isTextLikeMime(row.mimeType) && typeof row.content === 'string' && row.content.length > 0) {\n return {\n ...base,\n source: 'text',\n textContent: truncateText(row.content, maxTextChars),\n }\n }\n\n // Images + PDFs \u2014 prefer inline bytes when small enough; otherwise signed URL\n // if the container registered an attachmentSigner; otherwise metadata-only.\n if (mediaClass === 'image' || mediaClass === 'pdf') {\n if (row.fileSize > 0 && row.fileSize <= maxInlineBytes) {\n const bytes = await readAttachmentBytes(row)\n if (bytes) {\n return {\n ...base,\n source: 'bytes',\n data: bytes,\n }\n }\n }\n if (signer) {\n try {\n const url = await signer.sign({\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType,\n tenantId: authContext.tenantId,\n organizationId: authContext.organizationId,\n })\n if (typeof url === 'string' && url.length > 0) {\n return {\n ...base,\n source: 'signed-url',\n url,\n }\n }\n } catch (error) {\n console.warn(\n `[AI Agents] attachmentSigner failed for ${row.id}; falling back to metadata-only:`,\n error,\n )\n }\n }\n return { ...base, source: 'metadata-only' }\n }\n\n // Generic file without extracted text \u2014 metadata-only so the model at least\n // knows the attachment is present.\n return { ...base, source: 'metadata-only' }\n}\n\n/**\n * Resolves each `attachmentId` into a model-ready {@link AiResolvedAttachmentPart}.\n *\n * Contract:\n *\n * - Tenant/org scope is enforced: records that don't belong to the caller are\n * dropped with a `console.warn`. Super-admin callers bypass the scope check.\n * - When the agent declares `acceptedMediaTypes`, parts whose classified media\n * type is not in the whitelist are dropped with a `console.warn`.\n * `acceptedMediaTypes: undefined` means \"no filter\".\n * - When the DI container is missing or the attachments service is\n * unavailable, the helper returns `[]` with a single `console.warn` and\n * does NOT throw \u2014 the caller's `attachmentIds` pass-through to\n * {@link resolveAiAgentTools} remains the Step 3.6 parity behavior.\n * - The returned parts are ordered to match `attachmentIds`. Any id that\n * cannot be resolved (not found, out-of-scope, unreadable) is silently\n * dropped from the result \u2014 the caller observes a shorter list.\n */\nexport async function resolveAttachmentParts(\n input: ResolveAttachmentPartsInput,\n): Promise<AiResolvedAttachmentPart[]> {\n const ids = Array.from(input.attachmentIds ?? [])\n if (ids.length === 0) return []\n\n const em = resolveEm(input.container)\n if (!em) {\n console.warn(\n '[AI Agents] resolveAttachmentParts called without a DI container exposing `em`; skipping attachment resolution.',\n )\n return []\n }\n\n const maxInlineBytes = input.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES\n const maxTextChars = input.maxTextChars ?? DEFAULT_MAX_TEXT_CHARS\n const signer = resolveSigner(input.container)\n const acceptedSet = input.acceptedMediaTypes\n ? new Set<AiAgentAcceptedMediaType>(input.acceptedMediaTypes)\n : null\n\n const parts: AiResolvedAttachmentPart[] = []\n for (const id of ids) {\n if (typeof id !== 'string' || id.length === 0) continue\n let row: AttachmentRow | null\n try {\n row = await loadAttachmentRow(em, id, input.authContext)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to load attachment ${id}; skipping:`,\n error,\n )\n continue\n }\n if (!row) {\n console.warn(`[AI Agents] Attachment ${id} not found; skipping.`)\n continue\n }\n if (!rowBelongsToCaller(row, input.authContext)) {\n console.warn(\n `[AI Agents] Attachment ${id} is out of scope for caller (tenant=${input.authContext.tenantId}, org=${input.authContext.organizationId}); skipping.`,\n )\n continue\n }\n const mediaClass = classifyMediaType(row.mimeType)\n if (acceptedSet && !acceptedSet.has(mediaClass)) {\n console.warn(\n `[AI Agents] Attachment ${id} (${row.mimeType}) is not in agent acceptedMediaTypes=${[...acceptedSet].join(',')}; skipping.`,\n )\n continue\n }\n try {\n const part = await classifyAndBuildPart(\n row,\n mediaClass,\n maxInlineBytes,\n maxTextChars,\n signer,\n input.authContext,\n )\n parts.push(part)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to build attachment part for ${id}; skipping:`,\n error,\n )\n }\n }\n\n return parts\n}\n\n/**\n * Helper used by {@link ./agent-runtime} to fan out attachment resolution for\n * an agent. Kept separate so the runtime helpers share identical semantics\n * (Step 3.6 parity invariant #7 widened: resolved parts flow into both the\n * chat and object paths through the same code).\n */\nexport async function resolveAttachmentPartsForAgent(input: {\n agent: AiAgentDefinition\n attachmentIds: readonly string[] | undefined\n authContext: AiChatRequestContext\n container?: AwilixContainer\n}): Promise<AiResolvedAttachmentPart[]> {\n if (!input.attachmentIds || input.attachmentIds.length === 0) return []\n return resolveAttachmentParts({\n attachmentIds: input.attachmentIds,\n authContext: input.authContext,\n acceptedMediaTypes: input.agent.acceptedMediaTypes,\n container: input.container,\n })\n}\n\n/**\n * Converts resolved attachment parts into AI SDK v6 `FileUIPart` shapes so\n * they can be appended to the last user `UIMessage.parts`. `metadata-only`\n * parts are dropped \u2014 there is no provider-safe file-part shape for them;\n * their presence is surfaced through the system prompt instead by\n * {@link summarizeAttachmentPartsForPrompt}.\n */\nexport function attachmentPartsToUiFileParts(\n parts: readonly AiResolvedAttachmentPart[],\n): Array<{ type: 'file'; mediaType: string; filename: string; url: string }> {\n const output: Array<{ type: 'file'; mediaType: string; filename: string; url: string }> = []\n for (const part of parts) {\n if (part.source === 'bytes' && part.data) {\n const base64 = toBase64(part.data)\n if (base64) {\n output.push({\n type: 'file',\n mediaType: part.mediaType,\n filename: part.fileName,\n url: `data:${part.mediaType};base64,${base64}`,\n })\n }\n continue\n }\n if (part.source === 'signed-url' && typeof part.url === 'string' && part.url.length > 0) {\n output.push({\n type: 'file',\n mediaType: part.mediaType,\n filename: part.fileName,\n url: part.url,\n })\n }\n }\n return output\n}\n\n/**\n * Renders a compact, human-readable attachment summary to append to the\n * system prompt. Covers `text`, `metadata-only`, and as a fallback the\n * `bytes`/`signed-url` kinds so the model can always reason about which\n * attachments are in scope. Keeping this as a string keeps provider-agnostic\n * behavior \u2014 object-mode and chat-mode both consume the same surface.\n */\nexport function summarizeAttachmentPartsForPrompt(\n parts: readonly AiResolvedAttachmentPart[],\n): string | null {\n if (parts.length === 0) return null\n const lines: string[] = ['[ATTACHMENTS]']\n for (const part of parts) {\n const header = `- ${part.fileName} (${part.mediaType}, source=${part.source})`\n if (part.source === 'text' && typeof part.textContent === 'string' && part.textContent.length > 0) {\n lines.push(header)\n lines.push(part.textContent)\n } else {\n lines.push(header)\n }\n }\n return lines.join('\\n')\n}\n\nfunction toBase64(data: Uint8Array | string): string | null {\n if (typeof data === 'string') return data\n try {\n return Buffer.from(data).toString('base64')\n } catch {\n return null\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type {\n AiAgentAcceptedMediaType,\n AiAgentDefinition,\n} from './ai-agent-definition'\nimport type {\n AiChatRequestContext,\n AiResolvedAttachmentPart,\n} from './attachment-bridge-types'\n\n// Provider-native inline byte limit. Most AI providers accept inline image/PDF\n// payloads comfortably under 4 MB; anything larger SHOULD travel as a short-lived\n// signed URL (see AttachmentSigner below). Above this ceiling and with no signer\n// configured, the helper downgrades to `metadata-only` so the model at least sees\n// that the attachment exists.\nconst DEFAULT_MAX_INLINE_BYTES = 4 * 1024 * 1024\n\n// Extracted text cap. The `content` column on the `attachments` table is the\n// OCR/text-extraction output; we forward it verbatim up to this character count\n// so the system prompt + messages combined do not blow past model context\n// limits. Truncation is signaled to the model via a trailing `[... truncated]`\n// marker.\nconst DEFAULT_MAX_TEXT_CHARS = 64 * 1024\n\n/**\n * Optional attachment-signer. When the DI container resolves a value under\n * `attachmentSigner`, the resolver uses it to mint a short-lived URL for\n * images/PDFs that exceed the inline-bytes threshold. Phase 1 does not ship a\n * concrete signer; the hook exists so the `signed-url` branch of\n * {@link AiResolvedAttachmentPart} is reachable as soon as a provider wires one\n * up without requiring another runtime change.\n */\nexport interface AttachmentSigner {\n sign(input: {\n attachmentId: string\n fileName: string\n mediaType: string\n tenantId: string | null\n organizationId: string | null\n }): Promise<string | null>\n}\n\nexport interface ResolveAttachmentPartsInput {\n attachmentIds: readonly string[]\n authContext: AiChatRequestContext\n acceptedMediaTypes?: readonly AiAgentAcceptedMediaType[]\n container?: AwilixContainer\n /**\n * Optional override for the inline bytes threshold. Callers SHOULD leave\n * this untouched; the default tracks a safe cross-provider ceiling.\n */\n maxInlineBytes?: number\n /**\n * Optional override for the extracted-text character cap.\n */\n maxTextChars?: number\n}\n\nfunction classifyMediaType(mimeType: string | null | undefined): AiAgentAcceptedMediaType {\n const normalized = (mimeType ?? '').toLowerCase().trim()\n if (normalized.startsWith('image/')) return 'image'\n if (normalized === 'application/pdf') return 'pdf'\n return 'file'\n}\n\nfunction isTextLikeMime(mimeType: string | null | undefined): boolean {\n const normalized = (mimeType ?? '').toLowerCase().trim()\n if (!normalized) return false\n if (normalized.startsWith('text/')) return true\n if (normalized === 'application/json') return true\n if (normalized === 'application/xml') return true\n if (normalized === 'application/x-yaml' || normalized === 'text/yaml') return true\n if (normalized === 'application/csv') return true\n return false\n}\n\nfunction truncateText(value: string, maxChars: number): string {\n if (value.length <= maxChars) return value\n return `${value.slice(0, Math.max(0, maxChars - 16))}\\n[... truncated]`\n}\n\nfunction resolveEm(container: AwilixContainer | undefined): EntityManager | null {\n if (!container) return null\n try {\n const candidate = container.resolve('em') as EntityManager | undefined\n return candidate ?? null\n } catch {\n return null\n }\n}\n\nfunction resolveSigner(container: AwilixContainer | undefined): AttachmentSigner | null {\n if (!container) return null\n try {\n const candidate = container.resolve('attachmentSigner') as AttachmentSigner | undefined\n if (candidate && typeof candidate.sign === 'function') {\n return candidate\n }\n } catch {\n return null\n }\n return null\n}\n\ntype AttachmentRow = {\n id: string\n entityId: string\n fileName: string\n mimeType: string\n fileSize: number\n storagePath: string\n storageDriver: string\n partitionCode: string\n tenantId: string | null\n organizationId: string | null\n content: string | null\n}\n\nasync function loadAttachmentRow(\n em: EntityManager,\n attachmentId: string,\n authContext: AiChatRequestContext,\n): Promise<AttachmentRow | null> {\n // Attachment entity is imported lazily to keep ai-assistant isomorphic \u2014 the\n // core package owns the MikroORM metadata and is the only place tests would\n // need to bootstrap for real DB access.\n const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')\n // Tenant isolation is enforced in the SQL WHERE clause, not just the JS\n // post-filter below: the decryption scope (5th arg) only drives field\n // decryption, so omitting tenant/org from `where` would let `em.findOne`\n // return a row from another tenant. Super-admins bypass the scope. Org-scoped\n // callers also constrain by org; tenant-wide callers (organizationId === null)\n // may read any org within their tenant.\n const where: Record<string, unknown> = { id: attachmentId }\n if (!authContext.isSuperAdmin) {\n where.tenantId = authContext.tenantId\n if (authContext.organizationId != null) {\n where.organizationId = authContext.organizationId\n }\n }\n const record = await findOneWithDecryption(\n em,\n Attachment as never,\n where as never,\n undefined,\n {\n tenantId: authContext.tenantId,\n organizationId: authContext.organizationId,\n },\n )\n if (!record) return null\n const row = record as unknown as AttachmentRow\n return {\n id: row.id,\n entityId: row.entityId,\n fileName: row.fileName,\n mimeType: row.mimeType,\n fileSize: row.fileSize,\n storagePath: row.storagePath,\n storageDriver: row.storageDriver,\n partitionCode: row.partitionCode,\n tenantId: row.tenantId ?? null,\n organizationId: row.organizationId ?? null,\n content: row.content ?? null,\n }\n}\n\nfunction rowBelongsToCaller(row: AttachmentRow, authContext: AiChatRequestContext): boolean {\n if (authContext.isSuperAdmin) return true\n // Tenant scope: fail closed. The record MUST carry the caller's tenant.\n // A null `tenant_id` (a supported \"global\"/unscoped attachment state) is NOT\n // accessible through the AI path: it has no `partition.isPublic` gate, so a\n // truthiness short-circuit here would leak the bytes / extracted text into a\n // different tenant's LLM context (cross-tenant IDOR, issue #2663). Requiring\n // strict equality also rejects a non-super-admin caller with a null tenant.\n if (authContext.tenantId == null || row.tenantId !== authContext.tenantId) return false\n // Organization scope: when the caller is org-scoped, the record MUST match\n // that organization (this also rejects null-org rows for an org-scoped\n // caller). Tenant-wide callers (organizationId === null) may read any org\n // within their tenant.\n if (authContext.organizationId != null && row.organizationId !== authContext.organizationId) return false\n return true\n}\n\nasync function readAttachmentBytes(row: AttachmentRow): Promise<Uint8Array | null> {\n const { resolveAttachmentAbsolutePath } = await import(\n '@open-mercato/core/modules/attachments/lib/storage'\n )\n const absolutePath = resolveAttachmentAbsolutePath(\n row.partitionCode,\n row.storagePath,\n row.storageDriver,\n )\n try {\n const buffer = await fs.readFile(absolutePath)\n return new Uint8Array(buffer)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to read attachment ${row.id} from storage; falling back to metadata-only:`,\n error,\n )\n return null\n }\n}\n\nasync function classifyAndBuildPart(\n row: AttachmentRow,\n mediaClass: AiAgentAcceptedMediaType,\n maxInlineBytes: number,\n maxTextChars: number,\n signer: AttachmentSigner | null,\n authContext: AiChatRequestContext,\n): Promise<AiResolvedAttachmentPart> {\n const base: Pick<AiResolvedAttachmentPart, 'attachmentId' | 'fileName' | 'mediaType'> = {\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType || 'application/octet-stream',\n }\n\n // Text-like generic files \u2014 use the pre-extracted content column if present.\n if (mediaClass === 'file' && isTextLikeMime(row.mimeType) && typeof row.content === 'string' && row.content.length > 0) {\n return {\n ...base,\n source: 'text',\n textContent: truncateText(row.content, maxTextChars),\n }\n }\n\n // Images + PDFs \u2014 prefer inline bytes when small enough; otherwise signed URL\n // if the container registered an attachmentSigner; otherwise metadata-only.\n if (mediaClass === 'image' || mediaClass === 'pdf') {\n if (row.fileSize > 0 && row.fileSize <= maxInlineBytes) {\n const bytes = await readAttachmentBytes(row)\n if (bytes) {\n return {\n ...base,\n source: 'bytes',\n data: bytes,\n }\n }\n }\n if (signer) {\n try {\n const url = await signer.sign({\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType,\n tenantId: authContext.tenantId,\n organizationId: authContext.organizationId,\n })\n if (typeof url === 'string' && url.length > 0) {\n return {\n ...base,\n source: 'signed-url',\n url,\n }\n }\n } catch (error) {\n console.warn(\n `[AI Agents] attachmentSigner failed for ${row.id}; falling back to metadata-only:`,\n error,\n )\n }\n }\n return { ...base, source: 'metadata-only' }\n }\n\n // Generic file without extracted text \u2014 metadata-only so the model at least\n // knows the attachment is present.\n return { ...base, source: 'metadata-only' }\n}\n\n/**\n * Resolves each `attachmentId` into a model-ready {@link AiResolvedAttachmentPart}.\n *\n * Contract:\n *\n * - Tenant/org scope is enforced: records that don't belong to the caller are\n * dropped with a `console.warn`. Super-admin callers bypass the scope check.\n * - When the agent declares `acceptedMediaTypes`, parts whose classified media\n * type is not in the whitelist are dropped with a `console.warn`.\n * `acceptedMediaTypes: undefined` means \"no filter\".\n * - When the DI container is missing or the attachments service is\n * unavailable, the helper returns `[]` with a single `console.warn` and\n * does NOT throw \u2014 the caller's `attachmentIds` pass-through to\n * {@link resolveAiAgentTools} remains the Step 3.6 parity behavior.\n * - The returned parts are ordered to match `attachmentIds`. Any id that\n * cannot be resolved (not found, out-of-scope, unreadable) is silently\n * dropped from the result \u2014 the caller observes a shorter list.\n */\nexport async function resolveAttachmentParts(\n input: ResolveAttachmentPartsInput,\n): Promise<AiResolvedAttachmentPart[]> {\n const ids = Array.from(input.attachmentIds ?? [])\n if (ids.length === 0) return []\n\n const em = resolveEm(input.container)\n if (!em) {\n console.warn(\n '[AI Agents] resolveAttachmentParts called without a DI container exposing `em`; skipping attachment resolution.',\n )\n return []\n }\n\n const maxInlineBytes = input.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES\n const maxTextChars = input.maxTextChars ?? DEFAULT_MAX_TEXT_CHARS\n const signer = resolveSigner(input.container)\n const acceptedSet = input.acceptedMediaTypes\n ? new Set<AiAgentAcceptedMediaType>(input.acceptedMediaTypes)\n : null\n\n const parts: AiResolvedAttachmentPart[] = []\n for (const id of ids) {\n if (typeof id !== 'string' || id.length === 0) continue\n let row: AttachmentRow | null\n try {\n row = await loadAttachmentRow(em, id, input.authContext)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to load attachment ${id}; skipping:`,\n error,\n )\n continue\n }\n if (!row) {\n console.warn(`[AI Agents] Attachment ${id} not found; skipping.`)\n continue\n }\n if (!rowBelongsToCaller(row, input.authContext)) {\n console.warn(\n `[AI Agents] Attachment ${id} is out of scope for caller (tenant=${input.authContext.tenantId}, org=${input.authContext.organizationId}); skipping.`,\n )\n continue\n }\n const mediaClass = classifyMediaType(row.mimeType)\n if (acceptedSet && !acceptedSet.has(mediaClass)) {\n console.warn(\n `[AI Agents] Attachment ${id} (${row.mimeType}) is not in agent acceptedMediaTypes=${[...acceptedSet].join(',')}; skipping.`,\n )\n continue\n }\n try {\n const part = await classifyAndBuildPart(\n row,\n mediaClass,\n maxInlineBytes,\n maxTextChars,\n signer,\n input.authContext,\n )\n parts.push(part)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to build attachment part for ${id}; skipping:`,\n error,\n )\n }\n }\n\n return parts\n}\n\n/**\n * Helper used by {@link ./agent-runtime} to fan out attachment resolution for\n * an agent. Kept separate so the runtime helpers share identical semantics\n * (Step 3.6 parity invariant #7 widened: resolved parts flow into both the\n * chat and object paths through the same code).\n */\nexport async function resolveAttachmentPartsForAgent(input: {\n agent: AiAgentDefinition\n attachmentIds: readonly string[] | undefined\n authContext: AiChatRequestContext\n container?: AwilixContainer\n}): Promise<AiResolvedAttachmentPart[]> {\n if (!input.attachmentIds || input.attachmentIds.length === 0) return []\n return resolveAttachmentParts({\n attachmentIds: input.attachmentIds,\n authContext: input.authContext,\n acceptedMediaTypes: input.agent.acceptedMediaTypes,\n container: input.container,\n })\n}\n\n/**\n * Converts resolved attachment parts into AI SDK v6 `FileUIPart` shapes so\n * they can be appended to the last user `UIMessage.parts`. `metadata-only`\n * parts are dropped \u2014 there is no provider-safe file-part shape for them;\n * their presence is surfaced through the system prompt instead by\n * {@link summarizeAttachmentPartsForPrompt}.\n */\nexport function attachmentPartsToUiFileParts(\n parts: readonly AiResolvedAttachmentPart[],\n): Array<{ type: 'file'; mediaType: string; filename: string; url: string }> {\n const output: Array<{ type: 'file'; mediaType: string; filename: string; url: string }> = []\n for (const part of parts) {\n if (part.source === 'bytes' && part.data) {\n const base64 = toBase64(part.data)\n if (base64) {\n output.push({\n type: 'file',\n mediaType: part.mediaType,\n filename: part.fileName,\n url: `data:${part.mediaType};base64,${base64}`,\n })\n }\n continue\n }\n if (part.source === 'signed-url' && typeof part.url === 'string' && part.url.length > 0) {\n output.push({\n type: 'file',\n mediaType: part.mediaType,\n filename: part.fileName,\n url: part.url,\n })\n }\n }\n return output\n}\n\n/**\n * Renders a compact, human-readable attachment summary to append to the\n * system prompt. Covers `text`, `metadata-only`, and as a fallback the\n * `bytes`/`signed-url` kinds so the model can always reason about which\n * attachments are in scope. Keeping this as a string keeps provider-agnostic\n * behavior \u2014 object-mode and chat-mode both consume the same surface.\n */\nexport function summarizeAttachmentPartsForPrompt(\n parts: readonly AiResolvedAttachmentPart[],\n): string | null {\n if (parts.length === 0) return null\n const lines: string[] = ['[ATTACHMENTS]']\n for (const part of parts) {\n const header = `- ${part.fileName} (${part.mediaType}, source=${part.source})`\n if (part.source === 'text' && typeof part.textContent === 'string' && part.textContent.length > 0) {\n lines.push(header)\n lines.push(part.textContent)\n } else {\n lines.push(header)\n }\n }\n return lines.join('\\n')\n}\n\nfunction toBase64(data: Uint8Array | string): string | null {\n if (typeof data === 'string') return data\n try {\n return Buffer.from(data).toString('base64')\n } catch {\n return null\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -615,6 +615,13 @@ function createApiRequestFn(ctx, onCall) {
615
615
  }
616
616
  async function authorizeCodeModeApiRequest(ctx, method, path) {
617
617
  const normalizedMethod = method.toUpperCase();
618
+ if (isUnsafeApiRequestPath(path)) {
619
+ return {
620
+ allowed: false,
621
+ statusCode: 403,
622
+ error: `Code Mode rejected unsafe API path: ${normalizedMethod} ${path}`
623
+ };
624
+ }
618
625
  const normalizedPath = normalizeApiRequestPath(path);
619
626
  const endpoint = await findCodeModeApiEndpoint(normalizedMethod, normalizedPath);
620
627
  if (!endpoint) {
@@ -688,6 +695,24 @@ function normalizeApiRequestPath(path) {
688
695
  }
689
696
  return normalizedPath;
690
697
  }
698
+ const SINGLE_DOT_SEGMENTS = /* @__PURE__ */ new Set([".", "%2e"]);
699
+ const DOUBLE_DOT_SEGMENTS = /* @__PURE__ */ new Set(["..", ".%2e", "%2e.", "%2e%2e"]);
700
+ function isUnsafeApiRequestPath(path) {
701
+ const [rawPath] = String(path ?? "").split("?");
702
+ if (/[\u0000-\u001f]/.test(rawPath)) {
703
+ return true;
704
+ }
705
+ if (rawPath.includes("\\")) {
706
+ return true;
707
+ }
708
+ if (/%2f/i.test(rawPath) || /%5c/i.test(rawPath)) {
709
+ return true;
710
+ }
711
+ return rawPath.split("/").some((segment) => {
712
+ const lowered = segment.toLowerCase();
713
+ return SINGLE_DOT_SEGMENTS.has(lowered) || DOUBLE_DOT_SEGMENTS.has(lowered);
714
+ });
715
+ }
691
716
  function isPathParameterSegment(segment) {
692
717
  return segment.startsWith("{") && segment.endsWith("}") || segment.startsWith("[") && segment.endsWith("]") || segment.startsWith(":");
693
718
  }
@@ -707,6 +732,7 @@ export {
707
732
  CODE_MODE_REQUIRED_FEATURES,
708
733
  authorizeCodeModeApiRequest,
709
734
  createApiRequestFn,
735
+ isUnsafeApiRequestPath,
710
736
  isUnsafeHttpMethod,
711
737
  loadCodeModeTools,
712
738
  matchApiEndpointPath
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/codemode-tools.ts"],
4
- "sourcesContent": ["/**\n * Code Mode Tools\n *\n * Two meta-tools that replace all individual API/schema/module tools:\n * - search: Query the OpenAPI spec + entity graph programmatically\n * - execute: Make API calls via a sandboxed api.request() wrapper\n *\n * The AI writes JavaScript that runs in a node:vm sandbox with injected globals.\n */\n\nimport { z } from 'zod'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { registerMcpTool } from './tool-registry'\nimport type { McpToolContext } from './types'\nimport { createSandbox } from './sandbox'\nimport { truncateResult } from './truncate'\nimport { hasRequiredFeatures } from './auth'\nimport { getApiEndpoints, getRawOpenApiSpec, type ApiEndpoint } from './api-endpoint-index'\nimport {\n getCachedEntityGraph,\n inferModuleFromEntity,\n type EntityGraph,\n} from './entity-graph'\nimport {\n lookupSearchCache,\n storeSearchResult,\n buildMemoryContext,\n buildSearchLabel,\n incrementToolCallCount,\n} from './session-memory'\nimport { fetchWithTimeout, resolveTimeoutMs } from '@open-mercato/shared/lib/http/fetchWithTimeout'\n\nconst DEFAULT_AI_API_REQUEST_TIMEOUT_MS = 30_000\n\nfunction resolveAiApiRequestTimeoutMs(): number {\n const raw = process.env.AI_API_REQUEST_TIMEOUT_MS\n const parsed = raw ? Number.parseInt(raw, 10) : undefined\n return resolveTimeoutMs(parsed, DEFAULT_AI_API_REQUEST_TIMEOUT_MS)\n}\n\n/**\n * Cached spec object combining OpenAPI paths + entity schemas.\n */\nlet cachedCodeModeSpec: Record<string, unknown> | null = null\n\n/**\n * Cached TypeScript type stubs for common CRUD endpoints.\n * Generated once at startup from the OpenAPI spec.\n */\nlet cachedCommonTypes: string | null = null\n\nexport const CODE_MODE_REQUIRED_FEATURES = ['ai_assistant.view'] as const\n\n/**\n * Build the merged spec object for the search tool.\n */\nasync function getCodeModeSpec(): Promise<Record<string, unknown>> {\n if (cachedCodeModeSpec) return cachedCodeModeSpec\n\n const rawSpec = await getRawOpenApiSpec()\n const graph = getCachedEntityGraph()\n\n const paths = (rawSpec?.paths ?? {}) as Record<string, Record<string, unknown>>\n const entitySchemas = graph ? buildEntitySchemas(graph) : []\n\n const spec: Record<string, unknown> = {\n paths,\n info: rawSpec?.info,\n components: rawSpec?.components,\n entitySchemas,\n }\n\n // --- Helper functions injected into sandbox ---\n\n /**\n * spec.findEndpoints(keyword) \u2014 find all endpoints matching a keyword.\n * Returns compact list: [{ path, methods }]\n */\n spec.findEndpoints = (keyword: string) => {\n const kw = keyword.toLowerCase()\n return Object.entries(paths)\n .filter(([path]) => path.toLowerCase().includes(kw))\n .map(([path, methods]) => ({\n path,\n methods: Object.keys(methods).filter((m) => m !== 'parameters'),\n }))\n }\n\n /**\n * spec.describeEndpoint(path, method) \u2014 compact endpoint profile with working example.\n * Returns: { path, method, summary, requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }\n * For full schema access, use: spec.paths[path][method].requestBody\n */\n spec.describeEndpoint = (path: string, method: string) => {\n const pathObj = paths[path] as Record<string, unknown> | undefined\n if (!pathObj) return null\n\n const endpoint = pathObj[method.toLowerCase()] as Record<string, unknown> | undefined\n if (!endpoint) return null\n\n // Extract requestBody JSON Schema\n const bodySchema = extractRequestBodySchema(endpoint)\n const bodyProps = (bodySchema?.properties ?? {}) as Record<string, Record<string, unknown>>\n const bodyRequired = (bodySchema?.required ?? []) as string[]\n\n // Split fields into required (with types) vs optional (names only)\n const requiredFields: Array<{ name: string; type: string; format?: string }> = []\n const optionalFields: string[] = []\n const nestedCollections: Array<{\n field: string\n type: string\n requiredFields: Array<{ name: string; type: string }>\n commonFields: string[]\n }> = []\n\n for (const [name, prop] of Object.entries(bodyProps)) {\n const propType = (prop.type as string) || 'string'\n\n // Detect nested array collections (e.g. lines, items, addresses)\n if (propType === 'array' && prop.items && (prop.items as Record<string, unknown>).type === 'object') {\n const itemSchema = prop.items as Record<string, unknown>\n const itemProps = (itemSchema.properties ?? {}) as Record<string, Record<string, unknown>>\n const itemRequired = (itemSchema.required ?? []) as string[]\n\n const nestedRequired = itemRequired.map((n) => ({\n name: n,\n type: ((itemProps[n]?.type as string) || 'string'),\n }))\n\n // Common fields: first few non-required fields that are likely user-provided\n const nestedOptional = Object.keys(itemProps).filter((n) => !itemRequired.includes(n))\n const commonFields = nestedOptional.slice(0, 6)\n\n nestedCollections.push({\n field: name,\n type: 'array',\n requiredFields: nestedRequired,\n commonFields,\n })\n continue\n }\n\n if (bodyRequired.includes(name)) {\n const field: { name: string; type: string; format?: string } = { name, type: propType }\n if (prop.format) field.format = prop.format as string\n requiredFields.push(field)\n } else {\n optionalFields.push(name)\n }\n }\n\n // Generate minimal working example from required fields + nested collections\n const example: Record<string, unknown> = {}\n for (const field of requiredFields) {\n example[field.name] = generatePlaceholder(field.type, field.format)\n }\n for (const collection of nestedCollections) {\n const itemExample: Record<string, unknown> = {}\n for (const field of collection.requiredFields) {\n itemExample[field.name] = generatePlaceholder(field.type)\n }\n // Add first 2 common fields to the example\n for (const name of collection.commonFields.slice(0, 2)) {\n itemExample[name] = '<value>'\n }\n example[collection.field] = [itemExample]\n }\n\n // Find related endpoints sharing the same module prefix\n const segments = path.replace('/api/', '').split('/')\n const moduleSegment = segments[0]\n const resourceName = segments[1] || segments[0]\n const modulePrefix = `/api/${moduleSegment}/`\n const relatedEndpoints = Object.entries(paths)\n .filter(([p]) => p.startsWith(modulePrefix) && p !== path && !p.includes('{'))\n .map(([p, methods]) => ({\n path: p,\n methods: Object.keys(methods as Record<string, unknown>).filter((m) => m !== 'parameters'),\n }))\n .slice(0, 8)\n\n // Compact entity: className + relationship summary\n const resourceNorm = resourceName.replace(/-/g, '_')\n const resourceSingular = resourceNorm.endsWith('s') ? resourceNorm.slice(0, -1) : resourceNorm\n const moduleSingular = moduleSegment.endsWith('s') ? moduleSegment.slice(0, -1) : moduleSegment\n const prefixedTable = `${moduleSingular}_${resourceNorm}`\n\n const entity = entitySchemas.find((e: Record<string, unknown>) => {\n const table = ((e.tableName as string) || '').toLowerCase()\n const cls = ((e.className as string) || '').toLowerCase()\n const mod = ((e.module as string) || '').toLowerCase()\n if (table === resourceNorm || table === prefixedTable) return true\n if (cls.includes(moduleSingular) && cls.includes(resourceSingular)) return true\n if (mod === moduleSegment && cls.includes(resourceSingular)) return true\n if (cls === resourceSingular || cls.includes(resourceSingular)) return true\n return false\n }) || null\n\n let relatedEntity: string | null = null\n if (entity) {\n const ent = entity as Record<string, unknown>\n const rels = (ent.relationships as Array<{ relationship: string; target: string }>) || []\n const relSummary = rels.map((r) => `${r.relationship}: ${r.target}`).join(', ')\n relatedEntity = `${ent.className}${relSummary ? ` (${relSummary})` : ''}`\n }\n\n // GET endpoints: include query parameters compactly\n const parameters = method.toLowerCase() === 'get'\n ? (endpoint.parameters as Array<Record<string, unknown>> || [])\n .filter((p) => p.in === 'query')\n .map((p) => p.name as string)\n : undefined\n\n return {\n path,\n method: method.toUpperCase(),\n summary: endpoint.summary || endpoint.description,\n ...(parameters && parameters.length > 0 ? { queryParams: parameters } : {}),\n ...(requiredFields.length > 0 ? { requiredFields } : {}),\n ...(optionalFields.length > 0 ? { optionalFields } : {}),\n ...(nestedCollections.length > 0 ? { nestedCollections } : {}),\n ...(Object.keys(example).length > 0 ? { example } : {}),\n ...(relatedEndpoints.length > 0 ? { relatedEndpoints } : {}),\n relatedEntity,\n }\n }\n\n /**\n * spec.describeEntity(keyword) \u2014 find entity by keyword and return its full schema.\n * Returns: { className, tableName, module, fields, relationships }\n */\n spec.describeEntity = (keyword: string) => {\n const kw = keyword.toLowerCase()\n return entitySchemas.find((e: Record<string, unknown>) => {\n const cls = (e.className as string || '').toLowerCase()\n const table = (e.tableName as string || '').toLowerCase()\n return cls.includes(kw) || table.includes(kw)\n }) || null\n }\n\n cachedCodeModeSpec = spec\n return spec\n}\n\n/**\n * Extract the JSON Schema from an OpenAPI endpoint's requestBody.\n * Handles the common `content['application/json'].schema` path.\n */\nfunction extractRequestBodySchema(\n endpoint: Record<string, unknown>\n): Record<string, unknown> | null {\n const requestBody = endpoint.requestBody as Record<string, unknown> | undefined\n if (!requestBody) return null\n\n const content = requestBody.content as Record<string, Record<string, unknown>> | undefined\n if (!content) return null\n\n const jsonContent = content['application/json']\n if (!jsonContent) return null\n\n return (jsonContent.schema as Record<string, unknown>) || null\n}\n\n/**\n * Generate a placeholder value for a given JSON Schema type.\n */\nfunction generatePlaceholder(type: string, format?: string): unknown {\n if (format === 'uuid' || format === 'objectId') return '<uuid>'\n if (format === 'date-time' || format === 'date') return '<date>'\n if (format === 'email') return '<email>'\n switch (type) {\n case 'string': return '<string>'\n case 'number':\n case 'integer': return 0\n case 'boolean': return false\n case 'array': return []\n default: return '<value>'\n }\n}\n\n/**\n * Common CRUD endpoints to pre-generate types for.\n * These are the endpoints the agent uses most and where debug spirals happen.\n */\nconst COMMON_ENDPOINTS: Array<{ path: string; method: string; typeName: string }> = [\n { path: '/api/sales/quotes', method: 'post', typeName: 'CreateQuote' },\n { path: '/api/sales/orders', method: 'post', typeName: 'CreateOrder' },\n { path: '/api/sales/invoices', method: 'post', typeName: 'CreateInvoice' },\n { path: '/api/customers/companies', method: 'post', typeName: 'CreateCompany' },\n { path: '/api/customers/people', method: 'post', typeName: 'CreatePerson' },\n { path: '/api/customers/deals', method: 'post', typeName: 'CreateDeal' },\n { path: '/api/catalog/products', method: 'post', typeName: 'CreateProduct' },\n { path: '/api/customers/companies', method: 'put', typeName: 'UpdateCompany' },\n { path: '/api/customers/people', method: 'put', typeName: 'UpdatePerson' },\n { path: '/api/sales/quotes', method: 'put', typeName: 'UpdateQuote' },\n]\n\n/**\n * Generate TypeScript-like type stubs from the OpenAPI spec for common endpoints.\n * This runs once at startup and injects types into the execute tool description\n * so the LLM sees the correct payload shape without needing to call describeEndpoint.\n */\nasync function generateCommonTypes(): Promise<string> {\n if (cachedCommonTypes) return cachedCommonTypes\n\n const rawSpec = await getRawOpenApiSpec()\n if (!rawSpec?.paths) {\n cachedCommonTypes = ''\n return ''\n }\n\n const paths = rawSpec.paths as Record<string, Record<string, unknown>>\n const typeLines: string[] = ['Available types for api.request() body:\\n']\n\n for (const { path, method, typeName } of COMMON_ENDPOINTS) {\n const pathObj = paths[path] as Record<string, unknown> | undefined\n if (!pathObj) continue\n\n const endpoint = pathObj[method] as Record<string, unknown> | undefined\n if (!endpoint) continue\n\n const bodySchema = extractRequestBodySchema(endpoint)\n if (!bodySchema?.properties) continue\n\n const typeStr = schemaToTypeString(\n typeName,\n bodySchema,\n `${method.toUpperCase()} ${path}`,\n )\n if (typeStr) typeLines.push(typeStr)\n }\n\n if (typeLines.length <= 1) {\n cachedCommonTypes = ''\n return ''\n }\n\n cachedCommonTypes = typeLines.join('\\n')\n console.error(`[Code Mode] Generated ${typeLines.length - 1} common type stubs`)\n return cachedCommonTypes\n}\n\n/**\n * Convert a JSON Schema object to a compact TypeScript-like type string.\n * Produces a single-line or multi-line type declaration the LLM can use directly.\n */\nfunction schemaToTypeString(\n typeName: string,\n schema: Record<string, unknown>,\n comment: string,\n): string | null {\n const props = schema.properties as Record<string, Record<string, unknown>> | undefined\n if (!props) return null\n\n const required = new Set((schema.required as string[]) || [])\n\n // Skip internal fields that the sandbox injects automatically\n const skipFields = new Set(['tenantId', 'organizationId'])\n\n const fields: string[] = []\n const nestedTypes: string[] = []\n\n for (const [name, prop] of Object.entries(props)) {\n if (skipFields.has(name)) continue\n if (!prop || typeof prop !== 'object') continue\n\n const isRequired = required.has(name)\n const optMark = isRequired ? '' : '?'\n\n // Detect nested array of objects \u2192 extract as separate type\n if (\n prop.type === 'array' &&\n prop.items &&\n (prop.items as Record<string, unknown>).type === 'object'\n ) {\n const itemTypeName = `${typeName}${capitalize(singularize(name))}`\n const itemSchema = prop.items as Record<string, unknown>\n const nestedType = schemaToTypeString(itemTypeName, itemSchema, '')\n if (nestedType) nestedTypes.push(nestedType)\n fields.push(`${name}${optMark}: ${itemTypeName}[]`)\n continue\n }\n\n const propType = resolvePropertyType(prop)\n fields.push(`${name}${optMark}: ${propType}`)\n }\n\n if (fields.length === 0) return null\n\n const commentLine = comment ? `// ${comment}\\n` : ''\n const nested = nestedTypes.length > 0 ? nestedTypes.join('\\n') + '\\n' : ''\n return `${nested}${commentLine}type ${typeName} = { ${fields.join('; ')} }`\n}\n\n/**\n * Resolve a JSON Schema property to a compact TypeScript type string.\n */\nfunction resolvePropertyType(prop: Record<string, unknown>): string {\n // Handle anyOf (nullable types)\n if (prop.anyOf && Array.isArray(prop.anyOf)) {\n const variants = (prop.anyOf as Array<Record<string, unknown> | null>).filter(\n (s): s is Record<string, unknown> => s != null,\n )\n const nonNull = variants.filter((s) => s.type !== 'null')\n if (nonNull.length === 1) {\n return resolvePropertyType(nonNull[0]) + ' | null'\n }\n if (nonNull.length > 1) {\n return nonNull.map((s) => resolvePropertyType(s)).join(' | ')\n }\n }\n\n // Handle enum\n if (prop.enum && Array.isArray(prop.enum)) {\n return (prop.enum as string[]).map((v) => `'${v}'`).join(' | ')\n }\n\n const type = prop.type as string\n const format = prop.format as string | undefined\n\n if (type === 'array') {\n const items = prop.items as Record<string, unknown> | undefined\n if (items) return `${resolvePropertyType(items)}[]`\n return 'unknown[]'\n }\n\n if (type === 'object') return 'object'\n\n if (format === 'uuid') return 'string /*uuid*/'\n if (format === 'date-time') return 'string /*ISO date*/'\n if (format === 'date') return 'string /*date*/'\n if (format === 'email') return 'string /*email*/'\n\n switch (type) {\n case 'string': return 'string'\n case 'number':\n case 'integer': return 'number'\n case 'boolean': return 'boolean'\n default: return 'unknown'\n }\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1)\n}\n\nfunction singularize(s: string): string {\n if (s.endsWith('ies')) return s.slice(0, -3) + 'y'\n if (s.endsWith('ses')) return s.slice(0, -2)\n if (s.endsWith('s') && !s.endsWith('ss')) return s.slice(0, -1)\n return s\n}\n\n/**\n * Format a 400 API error response into a human-readable fix instruction.\n * Parses Zod-style validation errors and produces a concise message the LLM can act on.\n */\nfunction formatValidationError(data: unknown): string {\n if (!data || typeof data !== 'object') {\n return `Validation error: ${JSON.stringify(data)}`\n }\n\n // Raw Zod v4 array format: [{ expected, code, path, message }]\n if (Array.isArray(data)) {\n const issues = data as Array<Record<string, unknown>>\n const parts = issues.slice(0, 5).map((issue) => {\n const path = Array.isArray(issue.path) ? issue.path.join('.') : ''\n const msg = issue.message as string || `expected ${issue.expected}` || issue.code as string || 'invalid'\n return path ? `${path}: ${msg}` : msg\n })\n if (parts.length > 0) {\n return `Validation failed \u2014 ${parts.join('; ')}. Fix the listed fields and retry.`\n }\n }\n\n const obj = data as Record<string, unknown>\n\n // Zod v4 flat format: { fieldErrors: { field: [messages] }, formErrors: [messages] }\n if (obj.fieldErrors && typeof obj.fieldErrors === 'object') {\n const fieldErrors = obj.fieldErrors as Record<string, string[]>\n const parts: string[] = []\n for (const [field, messages] of Object.entries(fieldErrors)) {\n if (Array.isArray(messages) && messages.length > 0) {\n parts.push(`${field}: ${messages[0]}`)\n }\n }\n const formErrors = obj.formErrors as string[] | undefined\n if (Array.isArray(formErrors) && formErrors.length > 0) {\n parts.push(formErrors[0])\n }\n if (parts.length > 0) {\n return `Validation failed \u2014 ${parts.join('; ')}. Fix the listed fields and retry.`\n }\n }\n\n // Zod v3 format: { issues: [{ path: [...], message, code }] }\n if (obj.issues && Array.isArray(obj.issues)) {\n const issues = obj.issues as Array<Record<string, unknown>>\n const parts = issues.slice(0, 5).map((issue) => {\n const path = Array.isArray(issue.path) ? issue.path.join('.') : ''\n const msg = issue.message as string || issue.code as string || 'invalid'\n return path ? `${path}: ${msg}` : msg\n })\n return `Validation failed \u2014 ${parts.join('; ')}. Fix the listed fields and retry.`\n }\n\n // Our API error format: { error: string, details: ... }\n if (obj.error && typeof obj.error === 'string') {\n const details = obj.details\n if (details && typeof details === 'object') {\n return formatValidationError(details)\n }\n return obj.error\n }\n\n // Generic: { message: string }\n if (obj.message && typeof obj.message === 'string') {\n return obj.message\n }\n\n // Fallback: compact JSON\n const json = JSON.stringify(data)\n if (json.length > 500) {\n return `Validation error (truncated): ${json.slice(0, 500)}...`\n }\n return `Validation error: ${json}`\n}\n\n/**\n * Build entity schema array from the entity graph.\n */\nfunction buildEntitySchemas(graph: EntityGraph) {\n return graph.nodes.map((node) => {\n const relationships = graph.edges\n .filter((edge) => edge.source === node.className)\n .map((edge) => ({\n relationship: edge.relationship,\n target: edge.target,\n property: edge.property,\n nullable: edge.nullable,\n }))\n\n return {\n className: node.className,\n tableName: node.tableName,\n module: inferModuleFromEntity(node.className, node.tableName),\n fields: node.properties,\n relationships,\n }\n })\n}\n\n/** Maximum api.request() calls allowed per execute() run, regardless of method. */\nexport const CODE_MODE_MAX_API_CALLS = 50\n/** Maximum mutation (non-GET/HEAD/OPTIONS) api.request() calls allowed per execute() run. */\nexport const CODE_MODE_MAX_MUTATION_CALLS = 20\n\n/**\n * Load and register the two Code Mode tools.\n * Generates TypeScript type stubs for common endpoints at startup.\n * @returns Number of tools registered (always 2)\n */\nexport async function loadCodeModeTools(): Promise<number> {\n const commonTypes = await generateCommonTypes()\n registerSearchTool()\n registerExecuteTool(commonTypes)\n return 2\n}\n\n/**\n * search \u2014 Query the OpenAPI spec and entity graph programmatically.\n */\nfunction registerSearchTool(): void {\n registerMcpTool(\n {\n name: 'search',\n description: `Query the OpenAPI spec and entity schemas. READ-ONLY, no side effects.\nGlobals: spec.findEndpoints(keyword), spec.describeEndpoint(path, method), spec.describeEntity(keyword), spec.paths, spec.entitySchemas.\nUse BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common paths (companies, people, orders, quotes, products).`,\n inputSchema: z.object({\n code: z\n .string()\n .describe(\n 'An async arrow function that queries spec, e.g. async () => spec.paths[\"/api/customers/companies\"]'\n ),\n }),\n requiredFeatures: [...CODE_MODE_REQUIRED_FEATURES],\n handler: async (input: { code: string }, ctx: McpToolContext) => {\n const codePreview = input.code.slice(0, 120).replace(/\\n/g, ' ')\n console.error(`[AI Usage] search: code=\"${codePreview}${input.code.length > 120 ? '...' : ''}\"`)\n\n // Check session memory for cached result\n if (ctx.sessionId) {\n const cached = lookupSearchCache(ctx.sessionId, input.code)\n if (cached) {\n console.error(`[AI Usage] search: CACHE HIT (label=\"${cached.label}\")`)\n const memoryContext = buildMemoryContext(ctx.sessionId)\n return {\n success: true,\n result: cached.result,\n fromCache: true,\n _memoryContext: memoryContext,\n }\n }\n\n // Enforce tool call limit\n const { count, exceeded } = incrementToolCallCount(ctx.sessionId)\n if (exceeded) {\n console.error(`[AI Usage] search: TOOL CALL LIMIT EXCEEDED (count=${count})`)\n return {\n success: false,\n error: 'Tool call limit exceeded. Summarize what you know and respond to the user.',\n }\n }\n }\n\n const spec = await getCodeModeSpec()\n const sandbox = createSandbox({ spec })\n const result = await sandbox.execute(input.code)\n\n if (result.error) {\n console.error(`[AI Usage] search: ERROR in ${result.durationMs}ms \u2014 ${result.error}`)\n return {\n success: false,\n error: result.error,\n logs: result.logs,\n durationMs: result.durationMs,\n }\n }\n\n const truncated = truncateResult(result.result)\n console.error(`[AI Usage] search: OK in ${result.durationMs}ms \u2014 ${truncated.length} chars`)\n\n // Store in session memory\n if (ctx.sessionId) {\n const label = buildSearchLabel(input.code)\n storeSearchResult(ctx.sessionId, input.code, truncated, label)\n }\n\n const memoryContext = ctx.sessionId ? buildMemoryContext(ctx.sessionId) : undefined\n return {\n success: true,\n result: truncated,\n logs: result.logs,\n durationMs: result.durationMs,\n _memoryContext: memoryContext,\n }\n },\n },\n { moduleId: 'codemode' }\n )\n}\n\n/**\n * execute \u2014 Run JavaScript that can make API calls via api.request().\n */\nfunction registerExecuteTool(commonTypes: string): void {\n const typesBlock = commonTypes\n ? `\\n\\n${commonTypes}`\n : ''\n\n registerMcpTool(\n {\n name: 'execute',\n description: `Make API calls. Returns JSON.\nGlobals: api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }, context { tenantId, organizationId, userId }.\nRULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collection path with id in BODY. NEVER PUT/POST/DELETE unless user explicitly asked to change data. Before ANY write operation (POST/PUT/DELETE), you MUST use the AskUserQuestion tool to get explicit user confirmation. Do NOT just ask in text \u2014 use the tool so execution pauses until the user responds.${typesBlock}`,\n inputSchema: z.object({\n code: z\n .string()\n .describe(\n 'Async arrow function. For reads: async () => api.request({ method: \"GET\", path: \"/api/customers/companies\" }). For updates: async () => api.request({ method: \"PUT\", path: \"/api/customers/companies\", body: { id: \"<uuid>\", name: \"New Name\" } }). id goes in BODY not URL.'\n ),\n }),\n requiredFeatures: [...CODE_MODE_REQUIRED_FEATURES],\n handler: async (input: { code: string }, ctx: McpToolContext) => {\n const codePreview = input.code.slice(0, 120).replace(/\\n/g, ' ')\n console.error(`[AI Usage] execute: code=\"${codePreview}${input.code.length > 120 ? '...' : ''}\" user=${ctx.userId || 'unknown'}`)\n\n // Enforce tool call limit\n if (ctx.sessionId) {\n const { count, exceeded } = incrementToolCallCount(ctx.sessionId)\n if (exceeded) {\n console.error(`[AI Usage] execute: TOOL CALL LIMIT EXCEEDED (count=${count})`)\n return {\n success: false,\n error: 'Tool call limit exceeded. Summarize what you know and respond to the user.',\n }\n }\n }\n\n // Cap API calls for safety. The mutation cap is enforced against the\n // actually-observed HTTP method, not a static scan of the source \u2014 so a\n // dynamically-built method (e.g. 'PO' + 'ST') can never escape it.\n const maxApiCalls = CODE_MODE_MAX_API_CALLS\n let apiCallCount = 0\n let mutationCallCount = 0\n\n const apiRequestFn = createApiRequestFn(ctx, (normalizedMethod) => {\n apiCallCount++\n if (apiCallCount > maxApiCalls) {\n throw new Error(`API call limit exceeded (max ${maxApiCalls})`)\n }\n if (isUnsafeHttpMethod(normalizedMethod)) {\n mutationCallCount++\n if (mutationCallCount > CODE_MODE_MAX_MUTATION_CALLS) {\n throw new Error(`Mutation API call limit exceeded (max ${CODE_MODE_MAX_MUTATION_CALLS})`)\n }\n }\n })\n\n const context = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n\n const sandbox = createSandbox(\n { api: { request: apiRequestFn }, context },\n { maxApiCalls }\n )\n\n const result = await sandbox.execute(input.code)\n\n if (result.error) {\n console.error(`[AI Usage] execute: ERROR in ${result.durationMs}ms \u2014 apiCalls=${apiCallCount} \u2014 ${result.error}`)\n return {\n success: false,\n error: result.error,\n logs: result.logs,\n durationMs: result.durationMs,\n apiCallCount,\n }\n }\n\n const truncated = truncateResult(result.result)\n console.error(`[AI Usage] execute: OK in ${result.durationMs}ms \u2014 apiCalls=${apiCallCount} \u2014 ${truncated.length} chars`)\n\n const memoryContext = ctx.sessionId ? buildMemoryContext(ctx.sessionId) : undefined\n return {\n success: true,\n result: truncated,\n logs: result.logs,\n durationMs: result.durationMs,\n apiCallCount,\n _memoryContext: memoryContext,\n }\n },\n },\n { moduleId: 'codemode' }\n )\n}\n\n/**\n * Create the api.request() function for the execute sandbox.\n */\nexport function createApiRequestFn(\n ctx: McpToolContext,\n onCall: (normalizedMethod: string) => void\n): (params: {\n method: string\n path: string\n query?: Record<string, string>\n body?: Record<string, unknown>\n}) => Promise<unknown> {\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n return async (params) => {\n const { method, path, query, body } = params\n const callStart = Date.now()\n const normalizedMethod = String(method ?? '').toUpperCase()\n onCall(normalizedMethod)\n const apiPath = normalizeApiRequestPath(path)\n const authorization = await authorizeCodeModeApiRequest(ctx, normalizedMethod, apiPath)\n\n if (!authorization.allowed) {\n const callDuration = Date.now() - callStart\n console.error(\n `[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${authorization.statusCode} in ${callDuration}ms (blocked by Code Mode RBAC)`\n )\n return {\n success: false,\n statusCode: authorization.statusCode,\n error: authorization.error,\n details: authorization.details,\n }\n }\n\n let url = `${baseUrl}${apiPath}`\n\n // Build query parameters\n const queryParams: Record<string, string> = { ...query }\n\n if (normalizedMethod === 'GET') {\n if (ctx.tenantId) queryParams.tenantId = ctx.tenantId\n if (ctx.organizationId) queryParams.organizationId = ctx.organizationId\n }\n\n if (Object.keys(queryParams).length > 0) {\n const separator = url.includes('?') ? '&' : '?'\n url += separator + new URLSearchParams(queryParams).toString()\n }\n\n // Build request body with context injection\n let requestBody: Record<string, unknown> | undefined\n if (['POST', 'PUT', 'PATCH'].includes(normalizedMethod)) {\n requestBody = { ...body }\n if (ctx.tenantId) requestBody.tenantId = ctx.tenantId\n if (ctx.organizationId) requestBody.organizationId = ctx.organizationId\n }\n\n // Build headers\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n }\n if (ctx.apiKeySecret) headers['X-API-Key'] = ctx.apiKeySecret\n if (ctx.tenantId) headers['X-Tenant-Id'] = ctx.tenantId\n if (ctx.organizationId) headers['X-Organization-Id'] = ctx.organizationId\n\n // Execute request using host fetch (not sandbox)\n const response = await fetchWithTimeout(url, {\n method: normalizedMethod,\n headers,\n body: requestBody ? JSON.stringify(requestBody) : undefined,\n timeoutMs: resolveAiApiRequestTimeoutMs(),\n })\n\n const responseText = await response.text()\n const data = tryParseJson(responseText)\n const callDuration = Date.now() - callStart\n\n if (!response.ok) {\n console.error(`[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${response.status} in ${callDuration}ms`)\n\n // Format 400 validation errors into a clear fix instruction for the LLM\n if (response.status === 400) {\n return {\n success: false,\n statusCode: 400,\n error: formatValidationError(data),\n }\n }\n\n return {\n success: false,\n statusCode: response.status,\n error: `API error ${response.status}`,\n details: data,\n }\n }\n\n console.error(`[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${response.status} in ${callDuration}ms (${responseText.length} bytes)`)\n\n // Add mutation warning for non-GET calls\n if (!['GET', 'HEAD', 'OPTIONS'].includes(normalizedMethod)) {\n return {\n success: true,\n statusCode: response.status,\n data,\n _note: 'WRITE operation performed. Only do writes when user explicitly requested data modification.',\n }\n }\n\n return {\n success: true,\n statusCode: response.status,\n data,\n }\n }\n}\n\ntype CodeModeApiAuthorization =\n | { allowed: true; endpoint: ApiEndpoint }\n | { allowed: false; statusCode: number; error: string; details?: Record<string, unknown> }\n\nexport async function authorizeCodeModeApiRequest(\n ctx: McpToolContext,\n method: string,\n path: string\n): Promise<CodeModeApiAuthorization> {\n const normalizedMethod = method.toUpperCase()\n const normalizedPath = normalizeApiRequestPath(path)\n const endpoint = await findCodeModeApiEndpoint(normalizedMethod, normalizedPath)\n\n if (!endpoint) {\n return {\n allowed: false,\n statusCode: 403,\n error: `Code Mode cannot call undocumented API endpoint ${normalizedMethod} ${normalizedPath}`,\n }\n }\n\n const rbacService = resolveRbacService(ctx)\n const requiredFeatures = endpoint.requiredFeatures ?? []\n\n if (requiredFeatures.length > 0) {\n if (hasRequiredFeatures(requiredFeatures, ctx.userFeatures, ctx.isSuperAdmin, rbacService)) {\n return { allowed: true, endpoint }\n }\n\n return {\n allowed: false,\n statusCode: 403,\n error: `Insufficient permissions for ${normalizedMethod} ${normalizedPath}`,\n details: { requiredFeatures, operationId: endpoint.operationId },\n }\n }\n\n if (isUnsafeHttpMethod(normalizedMethod)) {\n return {\n allowed: false,\n statusCode: 403,\n error: `Code Mode cannot call mutation endpoint without declared required features: ${normalizedMethod} ${normalizedPath}`,\n details: { operationId: endpoint.operationId },\n }\n }\n\n return { allowed: true, endpoint }\n}\n\nfunction resolveRbacService(ctx: McpToolContext): RbacService | undefined {\n try {\n return ctx.container.resolve('rbacService') as RbacService\n } catch {\n return undefined\n }\n}\n\nasync function findCodeModeApiEndpoint(\n method: string,\n path: string\n): Promise<ApiEndpoint | null> {\n const endpoints = await getApiEndpoints()\n const exactMatch = endpoints.find((endpoint) => endpoint.method === method && endpoint.path === path)\n if (exactMatch) {\n return exactMatch\n }\n\n return endpoints.find((endpoint) => endpoint.method === method && matchApiEndpointPath(endpoint.path, path)) ?? null\n}\n\nexport function matchApiEndpointPath(endpointPath: string, requestPath: string): boolean {\n const normalizedEndpointPath = normalizeApiRequestPath(endpointPath)\n const normalizedRequestPath = normalizeApiRequestPath(requestPath)\n\n if (normalizedEndpointPath === normalizedRequestPath) {\n return true\n }\n\n const endpointSegments = normalizedEndpointPath.split('/').filter(Boolean)\n const requestSegments = normalizedRequestPath.split('/').filter(Boolean)\n\n if (endpointSegments.length !== requestSegments.length) {\n return false\n }\n\n return endpointSegments.every((segment, index) => {\n if (isPathParameterSegment(segment)) {\n return requestSegments[index].length > 0\n }\n return segment === requestSegments[index]\n })\n}\n\nfunction normalizeApiRequestPath(path: string): string {\n const [rawPath] = path.split('?')\n const normalizedPath = rawPath.startsWith('/api')\n ? rawPath\n : `/api${rawPath.startsWith('/') ? rawPath : `/${rawPath}`}`\n\n if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {\n return normalizedPath.slice(0, -1)\n }\n\n return normalizedPath\n}\n\nfunction isPathParameterSegment(segment: string): boolean {\n return (\n (segment.startsWith('{') && segment.endsWith('}')) ||\n (segment.startsWith('[') && segment.endsWith(']')) ||\n segment.startsWith(':')\n )\n}\n\nexport function isUnsafeHttpMethod(method: string): boolean {\n return !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase())\n}\n\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Code Mode Tools\n *\n * Two meta-tools that replace all individual API/schema/module tools:\n * - search: Query the OpenAPI spec + entity graph programmatically\n * - execute: Make API calls via a sandboxed api.request() wrapper\n *\n * The AI writes JavaScript that runs in a node:vm sandbox with injected globals.\n */\n\nimport { z } from 'zod'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { registerMcpTool } from './tool-registry'\nimport type { McpToolContext } from './types'\nimport { createSandbox } from './sandbox'\nimport { truncateResult } from './truncate'\nimport { hasRequiredFeatures } from './auth'\nimport { getApiEndpoints, getRawOpenApiSpec, type ApiEndpoint } from './api-endpoint-index'\nimport {\n getCachedEntityGraph,\n inferModuleFromEntity,\n type EntityGraph,\n} from './entity-graph'\nimport {\n lookupSearchCache,\n storeSearchResult,\n buildMemoryContext,\n buildSearchLabel,\n incrementToolCallCount,\n} from './session-memory'\nimport { fetchWithTimeout, resolveTimeoutMs } from '@open-mercato/shared/lib/http/fetchWithTimeout'\n\nconst DEFAULT_AI_API_REQUEST_TIMEOUT_MS = 30_000\n\nfunction resolveAiApiRequestTimeoutMs(): number {\n const raw = process.env.AI_API_REQUEST_TIMEOUT_MS\n const parsed = raw ? Number.parseInt(raw, 10) : undefined\n return resolveTimeoutMs(parsed, DEFAULT_AI_API_REQUEST_TIMEOUT_MS)\n}\n\n/**\n * Cached spec object combining OpenAPI paths + entity schemas.\n */\nlet cachedCodeModeSpec: Record<string, unknown> | null = null\n\n/**\n * Cached TypeScript type stubs for common CRUD endpoints.\n * Generated once at startup from the OpenAPI spec.\n */\nlet cachedCommonTypes: string | null = null\n\nexport const CODE_MODE_REQUIRED_FEATURES = ['ai_assistant.view'] as const\n\n/**\n * Build the merged spec object for the search tool.\n */\nasync function getCodeModeSpec(): Promise<Record<string, unknown>> {\n if (cachedCodeModeSpec) return cachedCodeModeSpec\n\n const rawSpec = await getRawOpenApiSpec()\n const graph = getCachedEntityGraph()\n\n const paths = (rawSpec?.paths ?? {}) as Record<string, Record<string, unknown>>\n const entitySchemas = graph ? buildEntitySchemas(graph) : []\n\n const spec: Record<string, unknown> = {\n paths,\n info: rawSpec?.info,\n components: rawSpec?.components,\n entitySchemas,\n }\n\n // --- Helper functions injected into sandbox ---\n\n /**\n * spec.findEndpoints(keyword) \u2014 find all endpoints matching a keyword.\n * Returns compact list: [{ path, methods }]\n */\n spec.findEndpoints = (keyword: string) => {\n const kw = keyword.toLowerCase()\n return Object.entries(paths)\n .filter(([path]) => path.toLowerCase().includes(kw))\n .map(([path, methods]) => ({\n path,\n methods: Object.keys(methods).filter((m) => m !== 'parameters'),\n }))\n }\n\n /**\n * spec.describeEndpoint(path, method) \u2014 compact endpoint profile with working example.\n * Returns: { path, method, summary, requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }\n * For full schema access, use: spec.paths[path][method].requestBody\n */\n spec.describeEndpoint = (path: string, method: string) => {\n const pathObj = paths[path] as Record<string, unknown> | undefined\n if (!pathObj) return null\n\n const endpoint = pathObj[method.toLowerCase()] as Record<string, unknown> | undefined\n if (!endpoint) return null\n\n // Extract requestBody JSON Schema\n const bodySchema = extractRequestBodySchema(endpoint)\n const bodyProps = (bodySchema?.properties ?? {}) as Record<string, Record<string, unknown>>\n const bodyRequired = (bodySchema?.required ?? []) as string[]\n\n // Split fields into required (with types) vs optional (names only)\n const requiredFields: Array<{ name: string; type: string; format?: string }> = []\n const optionalFields: string[] = []\n const nestedCollections: Array<{\n field: string\n type: string\n requiredFields: Array<{ name: string; type: string }>\n commonFields: string[]\n }> = []\n\n for (const [name, prop] of Object.entries(bodyProps)) {\n const propType = (prop.type as string) || 'string'\n\n // Detect nested array collections (e.g. lines, items, addresses)\n if (propType === 'array' && prop.items && (prop.items as Record<string, unknown>).type === 'object') {\n const itemSchema = prop.items as Record<string, unknown>\n const itemProps = (itemSchema.properties ?? {}) as Record<string, Record<string, unknown>>\n const itemRequired = (itemSchema.required ?? []) as string[]\n\n const nestedRequired = itemRequired.map((n) => ({\n name: n,\n type: ((itemProps[n]?.type as string) || 'string'),\n }))\n\n // Common fields: first few non-required fields that are likely user-provided\n const nestedOptional = Object.keys(itemProps).filter((n) => !itemRequired.includes(n))\n const commonFields = nestedOptional.slice(0, 6)\n\n nestedCollections.push({\n field: name,\n type: 'array',\n requiredFields: nestedRequired,\n commonFields,\n })\n continue\n }\n\n if (bodyRequired.includes(name)) {\n const field: { name: string; type: string; format?: string } = { name, type: propType }\n if (prop.format) field.format = prop.format as string\n requiredFields.push(field)\n } else {\n optionalFields.push(name)\n }\n }\n\n // Generate minimal working example from required fields + nested collections\n const example: Record<string, unknown> = {}\n for (const field of requiredFields) {\n example[field.name] = generatePlaceholder(field.type, field.format)\n }\n for (const collection of nestedCollections) {\n const itemExample: Record<string, unknown> = {}\n for (const field of collection.requiredFields) {\n itemExample[field.name] = generatePlaceholder(field.type)\n }\n // Add first 2 common fields to the example\n for (const name of collection.commonFields.slice(0, 2)) {\n itemExample[name] = '<value>'\n }\n example[collection.field] = [itemExample]\n }\n\n // Find related endpoints sharing the same module prefix\n const segments = path.replace('/api/', '').split('/')\n const moduleSegment = segments[0]\n const resourceName = segments[1] || segments[0]\n const modulePrefix = `/api/${moduleSegment}/`\n const relatedEndpoints = Object.entries(paths)\n .filter(([p]) => p.startsWith(modulePrefix) && p !== path && !p.includes('{'))\n .map(([p, methods]) => ({\n path: p,\n methods: Object.keys(methods as Record<string, unknown>).filter((m) => m !== 'parameters'),\n }))\n .slice(0, 8)\n\n // Compact entity: className + relationship summary\n const resourceNorm = resourceName.replace(/-/g, '_')\n const resourceSingular = resourceNorm.endsWith('s') ? resourceNorm.slice(0, -1) : resourceNorm\n const moduleSingular = moduleSegment.endsWith('s') ? moduleSegment.slice(0, -1) : moduleSegment\n const prefixedTable = `${moduleSingular}_${resourceNorm}`\n\n const entity = entitySchemas.find((e: Record<string, unknown>) => {\n const table = ((e.tableName as string) || '').toLowerCase()\n const cls = ((e.className as string) || '').toLowerCase()\n const mod = ((e.module as string) || '').toLowerCase()\n if (table === resourceNorm || table === prefixedTable) return true\n if (cls.includes(moduleSingular) && cls.includes(resourceSingular)) return true\n if (mod === moduleSegment && cls.includes(resourceSingular)) return true\n if (cls === resourceSingular || cls.includes(resourceSingular)) return true\n return false\n }) || null\n\n let relatedEntity: string | null = null\n if (entity) {\n const ent = entity as Record<string, unknown>\n const rels = (ent.relationships as Array<{ relationship: string; target: string }>) || []\n const relSummary = rels.map((r) => `${r.relationship}: ${r.target}`).join(', ')\n relatedEntity = `${ent.className}${relSummary ? ` (${relSummary})` : ''}`\n }\n\n // GET endpoints: include query parameters compactly\n const parameters = method.toLowerCase() === 'get'\n ? (endpoint.parameters as Array<Record<string, unknown>> || [])\n .filter((p) => p.in === 'query')\n .map((p) => p.name as string)\n : undefined\n\n return {\n path,\n method: method.toUpperCase(),\n summary: endpoint.summary || endpoint.description,\n ...(parameters && parameters.length > 0 ? { queryParams: parameters } : {}),\n ...(requiredFields.length > 0 ? { requiredFields } : {}),\n ...(optionalFields.length > 0 ? { optionalFields } : {}),\n ...(nestedCollections.length > 0 ? { nestedCollections } : {}),\n ...(Object.keys(example).length > 0 ? { example } : {}),\n ...(relatedEndpoints.length > 0 ? { relatedEndpoints } : {}),\n relatedEntity,\n }\n }\n\n /**\n * spec.describeEntity(keyword) \u2014 find entity by keyword and return its full schema.\n * Returns: { className, tableName, module, fields, relationships }\n */\n spec.describeEntity = (keyword: string) => {\n const kw = keyword.toLowerCase()\n return entitySchemas.find((e: Record<string, unknown>) => {\n const cls = (e.className as string || '').toLowerCase()\n const table = (e.tableName as string || '').toLowerCase()\n return cls.includes(kw) || table.includes(kw)\n }) || null\n }\n\n cachedCodeModeSpec = spec\n return spec\n}\n\n/**\n * Extract the JSON Schema from an OpenAPI endpoint's requestBody.\n * Handles the common `content['application/json'].schema` path.\n */\nfunction extractRequestBodySchema(\n endpoint: Record<string, unknown>\n): Record<string, unknown> | null {\n const requestBody = endpoint.requestBody as Record<string, unknown> | undefined\n if (!requestBody) return null\n\n const content = requestBody.content as Record<string, Record<string, unknown>> | undefined\n if (!content) return null\n\n const jsonContent = content['application/json']\n if (!jsonContent) return null\n\n return (jsonContent.schema as Record<string, unknown>) || null\n}\n\n/**\n * Generate a placeholder value for a given JSON Schema type.\n */\nfunction generatePlaceholder(type: string, format?: string): unknown {\n if (format === 'uuid' || format === 'objectId') return '<uuid>'\n if (format === 'date-time' || format === 'date') return '<date>'\n if (format === 'email') return '<email>'\n switch (type) {\n case 'string': return '<string>'\n case 'number':\n case 'integer': return 0\n case 'boolean': return false\n case 'array': return []\n default: return '<value>'\n }\n}\n\n/**\n * Common CRUD endpoints to pre-generate types for.\n * These are the endpoints the agent uses most and where debug spirals happen.\n */\nconst COMMON_ENDPOINTS: Array<{ path: string; method: string; typeName: string }> = [\n { path: '/api/sales/quotes', method: 'post', typeName: 'CreateQuote' },\n { path: '/api/sales/orders', method: 'post', typeName: 'CreateOrder' },\n { path: '/api/sales/invoices', method: 'post', typeName: 'CreateInvoice' },\n { path: '/api/customers/companies', method: 'post', typeName: 'CreateCompany' },\n { path: '/api/customers/people', method: 'post', typeName: 'CreatePerson' },\n { path: '/api/customers/deals', method: 'post', typeName: 'CreateDeal' },\n { path: '/api/catalog/products', method: 'post', typeName: 'CreateProduct' },\n { path: '/api/customers/companies', method: 'put', typeName: 'UpdateCompany' },\n { path: '/api/customers/people', method: 'put', typeName: 'UpdatePerson' },\n { path: '/api/sales/quotes', method: 'put', typeName: 'UpdateQuote' },\n]\n\n/**\n * Generate TypeScript-like type stubs from the OpenAPI spec for common endpoints.\n * This runs once at startup and injects types into the execute tool description\n * so the LLM sees the correct payload shape without needing to call describeEndpoint.\n */\nasync function generateCommonTypes(): Promise<string> {\n if (cachedCommonTypes) return cachedCommonTypes\n\n const rawSpec = await getRawOpenApiSpec()\n if (!rawSpec?.paths) {\n cachedCommonTypes = ''\n return ''\n }\n\n const paths = rawSpec.paths as Record<string, Record<string, unknown>>\n const typeLines: string[] = ['Available types for api.request() body:\\n']\n\n for (const { path, method, typeName } of COMMON_ENDPOINTS) {\n const pathObj = paths[path] as Record<string, unknown> | undefined\n if (!pathObj) continue\n\n const endpoint = pathObj[method] as Record<string, unknown> | undefined\n if (!endpoint) continue\n\n const bodySchema = extractRequestBodySchema(endpoint)\n if (!bodySchema?.properties) continue\n\n const typeStr = schemaToTypeString(\n typeName,\n bodySchema,\n `${method.toUpperCase()} ${path}`,\n )\n if (typeStr) typeLines.push(typeStr)\n }\n\n if (typeLines.length <= 1) {\n cachedCommonTypes = ''\n return ''\n }\n\n cachedCommonTypes = typeLines.join('\\n')\n console.error(`[Code Mode] Generated ${typeLines.length - 1} common type stubs`)\n return cachedCommonTypes\n}\n\n/**\n * Convert a JSON Schema object to a compact TypeScript-like type string.\n * Produces a single-line or multi-line type declaration the LLM can use directly.\n */\nfunction schemaToTypeString(\n typeName: string,\n schema: Record<string, unknown>,\n comment: string,\n): string | null {\n const props = schema.properties as Record<string, Record<string, unknown>> | undefined\n if (!props) return null\n\n const required = new Set((schema.required as string[]) || [])\n\n // Skip internal fields that the sandbox injects automatically\n const skipFields = new Set(['tenantId', 'organizationId'])\n\n const fields: string[] = []\n const nestedTypes: string[] = []\n\n for (const [name, prop] of Object.entries(props)) {\n if (skipFields.has(name)) continue\n if (!prop || typeof prop !== 'object') continue\n\n const isRequired = required.has(name)\n const optMark = isRequired ? '' : '?'\n\n // Detect nested array of objects \u2192 extract as separate type\n if (\n prop.type === 'array' &&\n prop.items &&\n (prop.items as Record<string, unknown>).type === 'object'\n ) {\n const itemTypeName = `${typeName}${capitalize(singularize(name))}`\n const itemSchema = prop.items as Record<string, unknown>\n const nestedType = schemaToTypeString(itemTypeName, itemSchema, '')\n if (nestedType) nestedTypes.push(nestedType)\n fields.push(`${name}${optMark}: ${itemTypeName}[]`)\n continue\n }\n\n const propType = resolvePropertyType(prop)\n fields.push(`${name}${optMark}: ${propType}`)\n }\n\n if (fields.length === 0) return null\n\n const commentLine = comment ? `// ${comment}\\n` : ''\n const nested = nestedTypes.length > 0 ? nestedTypes.join('\\n') + '\\n' : ''\n return `${nested}${commentLine}type ${typeName} = { ${fields.join('; ')} }`\n}\n\n/**\n * Resolve a JSON Schema property to a compact TypeScript type string.\n */\nfunction resolvePropertyType(prop: Record<string, unknown>): string {\n // Handle anyOf (nullable types)\n if (prop.anyOf && Array.isArray(prop.anyOf)) {\n const variants = (prop.anyOf as Array<Record<string, unknown> | null>).filter(\n (s): s is Record<string, unknown> => s != null,\n )\n const nonNull = variants.filter((s) => s.type !== 'null')\n if (nonNull.length === 1) {\n return resolvePropertyType(nonNull[0]) + ' | null'\n }\n if (nonNull.length > 1) {\n return nonNull.map((s) => resolvePropertyType(s)).join(' | ')\n }\n }\n\n // Handle enum\n if (prop.enum && Array.isArray(prop.enum)) {\n return (prop.enum as string[]).map((v) => `'${v}'`).join(' | ')\n }\n\n const type = prop.type as string\n const format = prop.format as string | undefined\n\n if (type === 'array') {\n const items = prop.items as Record<string, unknown> | undefined\n if (items) return `${resolvePropertyType(items)}[]`\n return 'unknown[]'\n }\n\n if (type === 'object') return 'object'\n\n if (format === 'uuid') return 'string /*uuid*/'\n if (format === 'date-time') return 'string /*ISO date*/'\n if (format === 'date') return 'string /*date*/'\n if (format === 'email') return 'string /*email*/'\n\n switch (type) {\n case 'string': return 'string'\n case 'number':\n case 'integer': return 'number'\n case 'boolean': return 'boolean'\n default: return 'unknown'\n }\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1)\n}\n\nfunction singularize(s: string): string {\n if (s.endsWith('ies')) return s.slice(0, -3) + 'y'\n if (s.endsWith('ses')) return s.slice(0, -2)\n if (s.endsWith('s') && !s.endsWith('ss')) return s.slice(0, -1)\n return s\n}\n\n/**\n * Format a 400 API error response into a human-readable fix instruction.\n * Parses Zod-style validation errors and produces a concise message the LLM can act on.\n */\nfunction formatValidationError(data: unknown): string {\n if (!data || typeof data !== 'object') {\n return `Validation error: ${JSON.stringify(data)}`\n }\n\n // Raw Zod v4 array format: [{ expected, code, path, message }]\n if (Array.isArray(data)) {\n const issues = data as Array<Record<string, unknown>>\n const parts = issues.slice(0, 5).map((issue) => {\n const path = Array.isArray(issue.path) ? issue.path.join('.') : ''\n const msg = issue.message as string || `expected ${issue.expected}` || issue.code as string || 'invalid'\n return path ? `${path}: ${msg}` : msg\n })\n if (parts.length > 0) {\n return `Validation failed \u2014 ${parts.join('; ')}. Fix the listed fields and retry.`\n }\n }\n\n const obj = data as Record<string, unknown>\n\n // Zod v4 flat format: { fieldErrors: { field: [messages] }, formErrors: [messages] }\n if (obj.fieldErrors && typeof obj.fieldErrors === 'object') {\n const fieldErrors = obj.fieldErrors as Record<string, string[]>\n const parts: string[] = []\n for (const [field, messages] of Object.entries(fieldErrors)) {\n if (Array.isArray(messages) && messages.length > 0) {\n parts.push(`${field}: ${messages[0]}`)\n }\n }\n const formErrors = obj.formErrors as string[] | undefined\n if (Array.isArray(formErrors) && formErrors.length > 0) {\n parts.push(formErrors[0])\n }\n if (parts.length > 0) {\n return `Validation failed \u2014 ${parts.join('; ')}. Fix the listed fields and retry.`\n }\n }\n\n // Zod v3 format: { issues: [{ path: [...], message, code }] }\n if (obj.issues && Array.isArray(obj.issues)) {\n const issues = obj.issues as Array<Record<string, unknown>>\n const parts = issues.slice(0, 5).map((issue) => {\n const path = Array.isArray(issue.path) ? issue.path.join('.') : ''\n const msg = issue.message as string || issue.code as string || 'invalid'\n return path ? `${path}: ${msg}` : msg\n })\n return `Validation failed \u2014 ${parts.join('; ')}. Fix the listed fields and retry.`\n }\n\n // Our API error format: { error: string, details: ... }\n if (obj.error && typeof obj.error === 'string') {\n const details = obj.details\n if (details && typeof details === 'object') {\n return formatValidationError(details)\n }\n return obj.error\n }\n\n // Generic: { message: string }\n if (obj.message && typeof obj.message === 'string') {\n return obj.message\n }\n\n // Fallback: compact JSON\n const json = JSON.stringify(data)\n if (json.length > 500) {\n return `Validation error (truncated): ${json.slice(0, 500)}...`\n }\n return `Validation error: ${json}`\n}\n\n/**\n * Build entity schema array from the entity graph.\n */\nfunction buildEntitySchemas(graph: EntityGraph) {\n return graph.nodes.map((node) => {\n const relationships = graph.edges\n .filter((edge) => edge.source === node.className)\n .map((edge) => ({\n relationship: edge.relationship,\n target: edge.target,\n property: edge.property,\n nullable: edge.nullable,\n }))\n\n return {\n className: node.className,\n tableName: node.tableName,\n module: inferModuleFromEntity(node.className, node.tableName),\n fields: node.properties,\n relationships,\n }\n })\n}\n\n/** Maximum api.request() calls allowed per execute() run, regardless of method. */\nexport const CODE_MODE_MAX_API_CALLS = 50\n/** Maximum mutation (non-GET/HEAD/OPTIONS) api.request() calls allowed per execute() run. */\nexport const CODE_MODE_MAX_MUTATION_CALLS = 20\n\n/**\n * Load and register the two Code Mode tools.\n * Generates TypeScript type stubs for common endpoints at startup.\n * @returns Number of tools registered (always 2)\n */\nexport async function loadCodeModeTools(): Promise<number> {\n const commonTypes = await generateCommonTypes()\n registerSearchTool()\n registerExecuteTool(commonTypes)\n return 2\n}\n\n/**\n * search \u2014 Query the OpenAPI spec and entity graph programmatically.\n */\nfunction registerSearchTool(): void {\n registerMcpTool(\n {\n name: 'search',\n description: `Query the OpenAPI spec and entity schemas. READ-ONLY, no side effects.\nGlobals: spec.findEndpoints(keyword), spec.describeEndpoint(path, method), spec.describeEntity(keyword), spec.paths, spec.entitySchemas.\nUse BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common paths (companies, people, orders, quotes, products).`,\n inputSchema: z.object({\n code: z\n .string()\n .describe(\n 'An async arrow function that queries spec, e.g. async () => spec.paths[\"/api/customers/companies\"]'\n ),\n }),\n requiredFeatures: [...CODE_MODE_REQUIRED_FEATURES],\n handler: async (input: { code: string }, ctx: McpToolContext) => {\n const codePreview = input.code.slice(0, 120).replace(/\\n/g, ' ')\n console.error(`[AI Usage] search: code=\"${codePreview}${input.code.length > 120 ? '...' : ''}\"`)\n\n // Check session memory for cached result\n if (ctx.sessionId) {\n const cached = lookupSearchCache(ctx.sessionId, input.code)\n if (cached) {\n console.error(`[AI Usage] search: CACHE HIT (label=\"${cached.label}\")`)\n const memoryContext = buildMemoryContext(ctx.sessionId)\n return {\n success: true,\n result: cached.result,\n fromCache: true,\n _memoryContext: memoryContext,\n }\n }\n\n // Enforce tool call limit\n const { count, exceeded } = incrementToolCallCount(ctx.sessionId)\n if (exceeded) {\n console.error(`[AI Usage] search: TOOL CALL LIMIT EXCEEDED (count=${count})`)\n return {\n success: false,\n error: 'Tool call limit exceeded. Summarize what you know and respond to the user.',\n }\n }\n }\n\n const spec = await getCodeModeSpec()\n const sandbox = createSandbox({ spec })\n const result = await sandbox.execute(input.code)\n\n if (result.error) {\n console.error(`[AI Usage] search: ERROR in ${result.durationMs}ms \u2014 ${result.error}`)\n return {\n success: false,\n error: result.error,\n logs: result.logs,\n durationMs: result.durationMs,\n }\n }\n\n const truncated = truncateResult(result.result)\n console.error(`[AI Usage] search: OK in ${result.durationMs}ms \u2014 ${truncated.length} chars`)\n\n // Store in session memory\n if (ctx.sessionId) {\n const label = buildSearchLabel(input.code)\n storeSearchResult(ctx.sessionId, input.code, truncated, label)\n }\n\n const memoryContext = ctx.sessionId ? buildMemoryContext(ctx.sessionId) : undefined\n return {\n success: true,\n result: truncated,\n logs: result.logs,\n durationMs: result.durationMs,\n _memoryContext: memoryContext,\n }\n },\n },\n { moduleId: 'codemode' }\n )\n}\n\n/**\n * execute \u2014 Run JavaScript that can make API calls via api.request().\n */\nfunction registerExecuteTool(commonTypes: string): void {\n const typesBlock = commonTypes\n ? `\\n\\n${commonTypes}`\n : ''\n\n registerMcpTool(\n {\n name: 'execute',\n description: `Make API calls. Returns JSON.\nGlobals: api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }, context { tenantId, organizationId, userId }.\nRULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collection path with id in BODY. NEVER PUT/POST/DELETE unless user explicitly asked to change data. Before ANY write operation (POST/PUT/DELETE), you MUST use the AskUserQuestion tool to get explicit user confirmation. Do NOT just ask in text \u2014 use the tool so execution pauses until the user responds.${typesBlock}`,\n inputSchema: z.object({\n code: z\n .string()\n .describe(\n 'Async arrow function. For reads: async () => api.request({ method: \"GET\", path: \"/api/customers/companies\" }). For updates: async () => api.request({ method: \"PUT\", path: \"/api/customers/companies\", body: { id: \"<uuid>\", name: \"New Name\" } }). id goes in BODY not URL.'\n ),\n }),\n requiredFeatures: [...CODE_MODE_REQUIRED_FEATURES],\n handler: async (input: { code: string }, ctx: McpToolContext) => {\n const codePreview = input.code.slice(0, 120).replace(/\\n/g, ' ')\n console.error(`[AI Usage] execute: code=\"${codePreview}${input.code.length > 120 ? '...' : ''}\" user=${ctx.userId || 'unknown'}`)\n\n // Enforce tool call limit\n if (ctx.sessionId) {\n const { count, exceeded } = incrementToolCallCount(ctx.sessionId)\n if (exceeded) {\n console.error(`[AI Usage] execute: TOOL CALL LIMIT EXCEEDED (count=${count})`)\n return {\n success: false,\n error: 'Tool call limit exceeded. Summarize what you know and respond to the user.',\n }\n }\n }\n\n // Cap API calls for safety. The mutation cap is enforced against the\n // actually-observed HTTP method, not a static scan of the source \u2014 so a\n // dynamically-built method (e.g. 'PO' + 'ST') can never escape it.\n const maxApiCalls = CODE_MODE_MAX_API_CALLS\n let apiCallCount = 0\n let mutationCallCount = 0\n\n const apiRequestFn = createApiRequestFn(ctx, (normalizedMethod) => {\n apiCallCount++\n if (apiCallCount > maxApiCalls) {\n throw new Error(`API call limit exceeded (max ${maxApiCalls})`)\n }\n if (isUnsafeHttpMethod(normalizedMethod)) {\n mutationCallCount++\n if (mutationCallCount > CODE_MODE_MAX_MUTATION_CALLS) {\n throw new Error(`Mutation API call limit exceeded (max ${CODE_MODE_MAX_MUTATION_CALLS})`)\n }\n }\n })\n\n const context = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n\n const sandbox = createSandbox(\n { api: { request: apiRequestFn }, context },\n { maxApiCalls }\n )\n\n const result = await sandbox.execute(input.code)\n\n if (result.error) {\n console.error(`[AI Usage] execute: ERROR in ${result.durationMs}ms \u2014 apiCalls=${apiCallCount} \u2014 ${result.error}`)\n return {\n success: false,\n error: result.error,\n logs: result.logs,\n durationMs: result.durationMs,\n apiCallCount,\n }\n }\n\n const truncated = truncateResult(result.result)\n console.error(`[AI Usage] execute: OK in ${result.durationMs}ms \u2014 apiCalls=${apiCallCount} \u2014 ${truncated.length} chars`)\n\n const memoryContext = ctx.sessionId ? buildMemoryContext(ctx.sessionId) : undefined\n return {\n success: true,\n result: truncated,\n logs: result.logs,\n durationMs: result.durationMs,\n apiCallCount,\n _memoryContext: memoryContext,\n }\n },\n },\n { moduleId: 'codemode' }\n )\n}\n\n/**\n * Create the api.request() function for the execute sandbox.\n */\nexport function createApiRequestFn(\n ctx: McpToolContext,\n onCall: (normalizedMethod: string) => void\n): (params: {\n method: string\n path: string\n query?: Record<string, string>\n body?: Record<string, unknown>\n}) => Promise<unknown> {\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n return async (params) => {\n const { method, path, query, body } = params\n const callStart = Date.now()\n const normalizedMethod = String(method ?? '').toUpperCase()\n onCall(normalizedMethod)\n const apiPath = normalizeApiRequestPath(path)\n const authorization = await authorizeCodeModeApiRequest(ctx, normalizedMethod, apiPath)\n\n if (!authorization.allowed) {\n const callDuration = Date.now() - callStart\n console.error(\n `[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${authorization.statusCode} in ${callDuration}ms (blocked by Code Mode RBAC)`\n )\n return {\n success: false,\n statusCode: authorization.statusCode,\n error: authorization.error,\n details: authorization.details,\n }\n }\n\n let url = `${baseUrl}${apiPath}`\n\n // Build query parameters\n const queryParams: Record<string, string> = { ...query }\n\n if (normalizedMethod === 'GET') {\n if (ctx.tenantId) queryParams.tenantId = ctx.tenantId\n if (ctx.organizationId) queryParams.organizationId = ctx.organizationId\n }\n\n if (Object.keys(queryParams).length > 0) {\n const separator = url.includes('?') ? '&' : '?'\n url += separator + new URLSearchParams(queryParams).toString()\n }\n\n // Build request body with context injection\n let requestBody: Record<string, unknown> | undefined\n if (['POST', 'PUT', 'PATCH'].includes(normalizedMethod)) {\n requestBody = { ...body }\n if (ctx.tenantId) requestBody.tenantId = ctx.tenantId\n if (ctx.organizationId) requestBody.organizationId = ctx.organizationId\n }\n\n // Build headers\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n }\n if (ctx.apiKeySecret) headers['X-API-Key'] = ctx.apiKeySecret\n if (ctx.tenantId) headers['X-Tenant-Id'] = ctx.tenantId\n if (ctx.organizationId) headers['X-Organization-Id'] = ctx.organizationId\n\n // Execute request using host fetch (not sandbox)\n const response = await fetchWithTimeout(url, {\n method: normalizedMethod,\n headers,\n body: requestBody ? JSON.stringify(requestBody) : undefined,\n timeoutMs: resolveAiApiRequestTimeoutMs(),\n })\n\n const responseText = await response.text()\n const data = tryParseJson(responseText)\n const callDuration = Date.now() - callStart\n\n if (!response.ok) {\n console.error(`[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${response.status} in ${callDuration}ms`)\n\n // Format 400 validation errors into a clear fix instruction for the LLM\n if (response.status === 400) {\n return {\n success: false,\n statusCode: 400,\n error: formatValidationError(data),\n }\n }\n\n return {\n success: false,\n statusCode: response.status,\n error: `API error ${response.status}`,\n details: data,\n }\n }\n\n console.error(`[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${response.status} in ${callDuration}ms (${responseText.length} bytes)`)\n\n // Add mutation warning for non-GET calls\n if (!['GET', 'HEAD', 'OPTIONS'].includes(normalizedMethod)) {\n return {\n success: true,\n statusCode: response.status,\n data,\n _note: 'WRITE operation performed. Only do writes when user explicitly requested data modification.',\n }\n }\n\n return {\n success: true,\n statusCode: response.status,\n data,\n }\n }\n}\n\ntype CodeModeApiAuthorization =\n | { allowed: true; endpoint: ApiEndpoint }\n | { allowed: false; statusCode: number; error: string; details?: Record<string, unknown> }\n\nexport async function authorizeCodeModeApiRequest(\n ctx: McpToolContext,\n method: string,\n path: string\n): Promise<CodeModeApiAuthorization> {\n const normalizedMethod = method.toUpperCase()\n\n if (isUnsafeApiRequestPath(path)) {\n return {\n allowed: false,\n statusCode: 403,\n error: `Code Mode rejected unsafe API path: ${normalizedMethod} ${path}`,\n }\n }\n\n const normalizedPath = normalizeApiRequestPath(path)\n const endpoint = await findCodeModeApiEndpoint(normalizedMethod, normalizedPath)\n\n if (!endpoint) {\n return {\n allowed: false,\n statusCode: 403,\n error: `Code Mode cannot call undocumented API endpoint ${normalizedMethod} ${normalizedPath}`,\n }\n }\n\n const rbacService = resolveRbacService(ctx)\n const requiredFeatures = endpoint.requiredFeatures ?? []\n\n if (requiredFeatures.length > 0) {\n if (hasRequiredFeatures(requiredFeatures, ctx.userFeatures, ctx.isSuperAdmin, rbacService)) {\n return { allowed: true, endpoint }\n }\n\n return {\n allowed: false,\n statusCode: 403,\n error: `Insufficient permissions for ${normalizedMethod} ${normalizedPath}`,\n details: { requiredFeatures, operationId: endpoint.operationId },\n }\n }\n\n if (isUnsafeHttpMethod(normalizedMethod)) {\n return {\n allowed: false,\n statusCode: 403,\n error: `Code Mode cannot call mutation endpoint without declared required features: ${normalizedMethod} ${normalizedPath}`,\n details: { operationId: endpoint.operationId },\n }\n }\n\n return { allowed: true, endpoint }\n}\n\nfunction resolveRbacService(ctx: McpToolContext): RbacService | undefined {\n try {\n return ctx.container.resolve('rbacService') as RbacService\n } catch {\n return undefined\n }\n}\n\nasync function findCodeModeApiEndpoint(\n method: string,\n path: string\n): Promise<ApiEndpoint | null> {\n const endpoints = await getApiEndpoints()\n const exactMatch = endpoints.find((endpoint) => endpoint.method === method && endpoint.path === path)\n if (exactMatch) {\n return exactMatch\n }\n\n return endpoints.find((endpoint) => endpoint.method === method && matchApiEndpointPath(endpoint.path, path)) ?? null\n}\n\nexport function matchApiEndpointPath(endpointPath: string, requestPath: string): boolean {\n const normalizedEndpointPath = normalizeApiRequestPath(endpointPath)\n const normalizedRequestPath = normalizeApiRequestPath(requestPath)\n\n if (normalizedEndpointPath === normalizedRequestPath) {\n return true\n }\n\n const endpointSegments = normalizedEndpointPath.split('/').filter(Boolean)\n const requestSegments = normalizedRequestPath.split('/').filter(Boolean)\n\n if (endpointSegments.length !== requestSegments.length) {\n return false\n }\n\n return endpointSegments.every((segment, index) => {\n if (isPathParameterSegment(segment)) {\n return requestSegments[index].length > 0\n }\n return segment === requestSegments[index]\n })\n}\n\nfunction normalizeApiRequestPath(path: string): string {\n const [rawPath] = path.split('?')\n const normalizedPath = rawPath.startsWith('/api')\n ? rawPath\n : `/api${rawPath.startsWith('/') ? rawPath : `/${rawPath}`}`\n\n if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {\n return normalizedPath.slice(0, -1)\n }\n\n return normalizedPath\n}\n\nconst SINGLE_DOT_SEGMENTS = new Set(['.', '%2e'])\nconst DOUBLE_DOT_SEGMENTS = new Set(['..', '.%2e', '%2e.', '%2e%2e'])\n\n/**\n * Rejects request paths that the WHATWG URL parser would rewrite before the\n * actual fetch (`..`/`.` path segments \u2014 including their percent-encoded forms\n * \u2014 backslashes, and percent-encoded separators). Code Mode authorizes the\n * literal path it was given, but `new URL()` collapses dot segments and\n * normalizes backslashes for http(s) URLs, so without this guard the wire\n * request can resolve to a different endpoint than the one that was authorized.\n */\nexport function isUnsafeApiRequestPath(path: string): boolean {\n const [rawPath] = String(path ?? '').split('?')\n\n // The WHATWG URL parser strips ASCII tab/newline/carriage-return from the URL\n // before parsing, so a smuggled `.<TAB>.` segment collapses to `..` on the\n // wire even though the literal segment never equals a dot segment here. Raw\n // control characters never appear in legitimate REST paths, so reject them.\n if (/[\\u0000-\\u001f]/.test(rawPath)) {\n return true\n }\n\n // http(s) URLs treat backslashes as path separators, so they can smuggle\n // separators past the segment-based authorizer.\n if (rawPath.includes('\\\\')) {\n return true\n }\n\n // Percent-encoded separators never appear in legitimate REST paths and let\n // the literal-'/' segment split desync from the parsed request URL.\n if (/%2f/i.test(rawPath) || /%5c/i.test(rawPath)) {\n return true\n }\n\n return rawPath.split('/').some((segment) => {\n const lowered = segment.toLowerCase()\n return SINGLE_DOT_SEGMENTS.has(lowered) || DOUBLE_DOT_SEGMENTS.has(lowered)\n })\n}\n\nfunction isPathParameterSegment(segment: string): boolean {\n return (\n (segment.startsWith('{') && segment.endsWith('}')) ||\n (segment.startsWith('[') && segment.endsWith(']')) ||\n segment.startsWith(':')\n )\n}\n\nexport function isUnsafeHttpMethod(method: string): boolean {\n return !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase())\n}\n\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n}\n"],
5
+ "mappings": "AAUA,SAAS,SAAS;AAElB,SAAS,uBAAuB;AAEhC,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,2BAA2B;AACpC,SAAS,iBAAiB,yBAA2C;AACrE;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB,wBAAwB;AAEnD,MAAM,oCAAoC;AAE1C,SAAS,+BAAuC;AAC9C,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,OAAO,SAAS,KAAK,EAAE,IAAI;AAChD,SAAO,iBAAiB,QAAQ,iCAAiC;AACnE;AAKA,IAAI,qBAAqD;AAMzD,IAAI,oBAAmC;AAEhC,MAAM,8BAA8B,CAAC,mBAAmB;AAK/D,eAAe,kBAAoD;AACjE,MAAI,mBAAoB,QAAO;AAE/B,QAAM,UAAU,MAAM,kBAAkB;AACxC,QAAM,QAAQ,qBAAqB;AAEnC,QAAM,QAAS,SAAS,SAAS,CAAC;AAClC,QAAM,gBAAgB,QAAQ,mBAAmB,KAAK,IAAI,CAAC;AAE3D,QAAM,OAAgC;AAAA,IACpC;AAAA,IACA,MAAM,SAAS;AAAA,IACf,YAAY,SAAS;AAAA,IACrB;AAAA,EACF;AAQA,OAAK,gBAAgB,CAAC,YAAoB;AACxC,UAAM,KAAK,QAAQ,YAAY;AAC/B,WAAO,OAAO,QAAQ,KAAK,EACxB,OAAO,CAAC,CAAC,IAAI,MAAM,KAAK,YAAY,EAAE,SAAS,EAAE,CAAC,EAClD,IAAI,CAAC,CAAC,MAAM,OAAO,OAAO;AAAA,MACzB;AAAA,MACA,SAAS,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,MAAM,YAAY;AAAA,IAChE,EAAE;AAAA,EACN;AAOA,OAAK,mBAAmB,CAAC,MAAc,WAAmB;AACxD,UAAM,UAAU,MAAM,IAAI;AAC1B,QAAI,CAAC,QAAS,QAAO;AAErB,UAAM,WAAW,QAAQ,OAAO,YAAY,CAAC;AAC7C,QAAI,CAAC,SAAU,QAAO;AAGtB,UAAM,aAAa,yBAAyB,QAAQ;AACpD,UAAM,YAAa,YAAY,cAAc,CAAC;AAC9C,UAAM,eAAgB,YAAY,YAAY,CAAC;AAG/C,UAAM,iBAAyE,CAAC;AAChF,UAAM,iBAA2B,CAAC;AAClC,UAAM,oBAKD,CAAC;AAEN,eAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,SAAS,GAAG;AACpD,YAAM,WAAY,KAAK,QAAmB;AAG1C,UAAI,aAAa,WAAW,KAAK,SAAU,KAAK,MAAkC,SAAS,UAAU;AACnG,cAAM,aAAa,KAAK;AACxB,cAAM,YAAa,WAAW,cAAc,CAAC;AAC7C,cAAM,eAAgB,WAAW,YAAY,CAAC;AAE9C,cAAM,iBAAiB,aAAa,IAAI,CAAC,OAAO;AAAA,UAC9C,MAAM;AAAA,UACN,MAAQ,UAAU,CAAC,GAAG,QAAmB;AAAA,QAC3C,EAAE;AAGF,cAAM,iBAAiB,OAAO,KAAK,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,SAAS,CAAC,CAAC;AACrF,cAAM,eAAe,eAAe,MAAM,GAAG,CAAC;AAE9C,0BAAkB,KAAK;AAAA,UACrB,OAAO;AAAA,UACP,MAAM;AAAA,UACN,gBAAgB;AAAA,UAChB;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAEA,UAAI,aAAa,SAAS,IAAI,GAAG;AAC/B,cAAM,QAAyD,EAAE,MAAM,MAAM,SAAS;AACtF,YAAI,KAAK,OAAQ,OAAM,SAAS,KAAK;AACrC,uBAAe,KAAK,KAAK;AAAA,MAC3B,OAAO;AACL,uBAAe,KAAK,IAAI;AAAA,MAC1B;AAAA,IACF;AAGA,UAAM,UAAmC,CAAC;AAC1C,eAAW,SAAS,gBAAgB;AAClC,cAAQ,MAAM,IAAI,IAAI,oBAAoB,MAAM,MAAM,MAAM,MAAM;AAAA,IACpE;AACA,eAAW,cAAc,mBAAmB;AAC1C,YAAM,cAAuC,CAAC;AAC9C,iBAAW,SAAS,WAAW,gBAAgB;AAC7C,oBAAY,MAAM,IAAI,IAAI,oBAAoB,MAAM,IAAI;AAAA,MAC1D;AAEA,iBAAW,QAAQ,WAAW,aAAa,MAAM,GAAG,CAAC,GAAG;AACtD,oBAAY,IAAI,IAAI;AAAA,MACtB;AACA,cAAQ,WAAW,KAAK,IAAI,CAAC,WAAW;AAAA,IAC1C;AAGA,UAAM,WAAW,KAAK,QAAQ,SAAS,EAAE,EAAE,MAAM,GAAG;AACpD,UAAM,gBAAgB,SAAS,CAAC;AAChC,UAAM,eAAe,SAAS,CAAC,KAAK,SAAS,CAAC;AAC9C,UAAM,eAAe,QAAQ,aAAa;AAC1C,UAAM,mBAAmB,OAAO,QAAQ,KAAK,EAC1C,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,WAAW,YAAY,KAAK,MAAM,QAAQ,CAAC,EAAE,SAAS,GAAG,CAAC,EAC5E,IAAI,CAAC,CAAC,GAAG,OAAO,OAAO;AAAA,MACtB,MAAM;AAAA,MACN,SAAS,OAAO,KAAK,OAAkC,EAAE,OAAO,CAAC,MAAM,MAAM,YAAY;AAAA,IAC3F,EAAE,EACD,MAAM,GAAG,CAAC;AAGb,UAAM,eAAe,aAAa,QAAQ,MAAM,GAAG;AACnD,UAAM,mBAAmB,aAAa,SAAS,GAAG,IAAI,aAAa,MAAM,GAAG,EAAE,IAAI;AAClF,UAAM,iBAAiB,cAAc,SAAS,GAAG,IAAI,cAAc,MAAM,GAAG,EAAE,IAAI;AAClF,UAAM,gBAAgB,GAAG,cAAc,IAAI,YAAY;AAEvD,UAAM,SAAS,cAAc,KAAK,CAAC,MAA+B;AAChE,YAAM,SAAU,EAAE,aAAwB,IAAI,YAAY;AAC1D,YAAM,OAAQ,EAAE,aAAwB,IAAI,YAAY;AACxD,YAAM,OAAQ,EAAE,UAAqB,IAAI,YAAY;AACrD,UAAI,UAAU,gBAAgB,UAAU,cAAe,QAAO;AAC9D,UAAI,IAAI,SAAS,cAAc,KAAK,IAAI,SAAS,gBAAgB,EAAG,QAAO;AAC3E,UAAI,QAAQ,iBAAiB,IAAI,SAAS,gBAAgB,EAAG,QAAO;AACpE,UAAI,QAAQ,oBAAoB,IAAI,SAAS,gBAAgB,EAAG,QAAO;AACvE,aAAO;AAAA,IACT,CAAC,KAAK;AAEN,QAAI,gBAA+B;AACnC,QAAI,QAAQ;AACV,YAAM,MAAM;AACZ,YAAM,OAAQ,IAAI,iBAAqE,CAAC;AACxF,YAAM,aAAa,KAAK,IAAI,CAAC,MAAM,GAAG,EAAE,YAAY,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI;AAC9E,sBAAgB,GAAG,IAAI,SAAS,GAAG,aAAa,KAAK,UAAU,MAAM,EAAE;AAAA,IACzE;AAGA,UAAM,aAAa,OAAO,YAAY,MAAM,SACvC,SAAS,cAAgD,CAAC,GACxD,OAAO,CAAC,MAAM,EAAE,OAAO,OAAO,EAC9B,IAAI,CAAC,MAAM,EAAE,IAAc,IAC9B;AAEJ,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,OAAO,YAAY;AAAA,MAC3B,SAAS,SAAS,WAAW,SAAS;AAAA,MACtC,GAAI,cAAc,WAAW,SAAS,IAAI,EAAE,aAAa,WAAW,IAAI,CAAC;AAAA,MACzE,GAAI,eAAe,SAAS,IAAI,EAAE,eAAe,IAAI,CAAC;AAAA,MACtD,GAAI,eAAe,SAAS,IAAI,EAAE,eAAe,IAAI,CAAC;AAAA,MACtD,GAAI,kBAAkB,SAAS,IAAI,EAAE,kBAAkB,IAAI,CAAC;AAAA,MAC5D,GAAI,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;AAAA,MACrD,GAAI,iBAAiB,SAAS,IAAI,EAAE,iBAAiB,IAAI,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAMA,OAAK,iBAAiB,CAAC,YAAoB;AACzC,UAAM,KAAK,QAAQ,YAAY;AAC/B,WAAO,cAAc,KAAK,CAAC,MAA+B;AACxD,YAAM,OAAO,EAAE,aAAuB,IAAI,YAAY;AACtD,YAAM,SAAS,EAAE,aAAuB,IAAI,YAAY;AACxD,aAAO,IAAI,SAAS,EAAE,KAAK,MAAM,SAAS,EAAE;AAAA,IAC9C,CAAC,KAAK;AAAA,EACR;AAEA,uBAAqB;AACrB,SAAO;AACT;AAMA,SAAS,yBACP,UACgC;AAChC,QAAM,cAAc,SAAS;AAC7B,MAAI,CAAC,YAAa,QAAO;AAEzB,QAAM,UAAU,YAAY;AAC5B,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,cAAc,QAAQ,kBAAkB;AAC9C,MAAI,CAAC,YAAa,QAAO;AAEzB,SAAQ,YAAY,UAAsC;AAC5D;AAKA,SAAS,oBAAoB,MAAc,QAA0B;AACnE,MAAI,WAAW,UAAU,WAAW,WAAY,QAAO;AACvD,MAAI,WAAW,eAAe,WAAW,OAAQ,QAAO;AACxD,MAAI,WAAW,QAAS,QAAO;AAC/B,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAA,IACL,KAAK;AAAW,aAAO;AAAA,IACvB,KAAK;AAAW,aAAO;AAAA,IACvB,KAAK;AAAS,aAAO,CAAC;AAAA,IACtB;AAAS,aAAO;AAAA,EAClB;AACF;AAMA,MAAM,mBAA8E;AAAA,EAClF,EAAE,MAAM,qBAAqB,QAAQ,QAAQ,UAAU,cAAc;AAAA,EACrE,EAAE,MAAM,qBAAqB,QAAQ,QAAQ,UAAU,cAAc;AAAA,EACrE,EAAE,MAAM,uBAAuB,QAAQ,QAAQ,UAAU,gBAAgB;AAAA,EACzE,EAAE,MAAM,4BAA4B,QAAQ,QAAQ,UAAU,gBAAgB;AAAA,EAC9E,EAAE,MAAM,yBAAyB,QAAQ,QAAQ,UAAU,eAAe;AAAA,EAC1E,EAAE,MAAM,wBAAwB,QAAQ,QAAQ,UAAU,aAAa;AAAA,EACvE,EAAE,MAAM,yBAAyB,QAAQ,QAAQ,UAAU,gBAAgB;AAAA,EAC3E,EAAE,MAAM,4BAA4B,QAAQ,OAAO,UAAU,gBAAgB;AAAA,EAC7E,EAAE,MAAM,yBAAyB,QAAQ,OAAO,UAAU,eAAe;AAAA,EACzE,EAAE,MAAM,qBAAqB,QAAQ,OAAO,UAAU,cAAc;AACtE;AAOA,eAAe,sBAAuC;AACpD,MAAI,kBAAmB,QAAO;AAE9B,QAAM,UAAU,MAAM,kBAAkB;AACxC,MAAI,CAAC,SAAS,OAAO;AACnB,wBAAoB;AACpB,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAsB,CAAC,2CAA2C;AAExE,aAAW,EAAE,MAAM,QAAQ,SAAS,KAAK,kBAAkB;AACzD,UAAM,UAAU,MAAM,IAAI;AAC1B,QAAI,CAAC,QAAS;AAEd,UAAM,WAAW,QAAQ,MAAM;AAC/B,QAAI,CAAC,SAAU;AAEf,UAAM,aAAa,yBAAyB,QAAQ;AACpD,QAAI,CAAC,YAAY,WAAY;AAE7B,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI;AAAA,IACjC;AACA,QAAI,QAAS,WAAU,KAAK,OAAO;AAAA,EACrC;AAEA,MAAI,UAAU,UAAU,GAAG;AACzB,wBAAoB;AACpB,WAAO;AAAA,EACT;AAEA,sBAAoB,UAAU,KAAK,IAAI;AACvC,UAAQ,MAAM,yBAAyB,UAAU,SAAS,CAAC,oBAAoB;AAC/E,SAAO;AACT;AAMA,SAAS,mBACP,UACA,QACA,SACe;AACf,QAAM,QAAQ,OAAO;AACrB,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,WAAW,IAAI,IAAK,OAAO,YAAyB,CAAC,CAAC;AAG5D,QAAM,aAAa,oBAAI,IAAI,CAAC,YAAY,gBAAgB,CAAC;AAEzD,QAAM,SAAmB,CAAC;AAC1B,QAAM,cAAwB,CAAC;AAE/B,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,WAAW,IAAI,IAAI,EAAG;AAC1B,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AAEvC,UAAM,aAAa,SAAS,IAAI,IAAI;AACpC,UAAM,UAAU,aAAa,KAAK;AAGlC,QACE,KAAK,SAAS,WACd,KAAK,SACJ,KAAK,MAAkC,SAAS,UACjD;AACA,YAAM,eAAe,GAAG,QAAQ,GAAG,WAAW,YAAY,IAAI,CAAC,CAAC;AAChE,YAAM,aAAa,KAAK;AACxB,YAAM,aAAa,mBAAmB,cAAc,YAAY,EAAE;AAClE,UAAI,WAAY,aAAY,KAAK,UAAU;AAC3C,aAAO,KAAK,GAAG,IAAI,GAAG,OAAO,KAAK,YAAY,IAAI;AAClD;AAAA,IACF;AAEA,UAAM,WAAW,oBAAoB,IAAI;AACzC,WAAO,KAAK,GAAG,IAAI,GAAG,OAAO,KAAK,QAAQ,EAAE;AAAA,EAC9C;AAEA,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,cAAc,UAAU,MAAM,OAAO;AAAA,IAAO;AAClD,QAAM,SAAS,YAAY,SAAS,IAAI,YAAY,KAAK,IAAI,IAAI,OAAO;AACxE,SAAO,GAAG,MAAM,GAAG,WAAW,QAAQ,QAAQ,QAAQ,OAAO,KAAK,IAAI,CAAC;AACzE;AAKA,SAAS,oBAAoB,MAAuC;AAElE,MAAI,KAAK,SAAS,MAAM,QAAQ,KAAK,KAAK,GAAG;AAC3C,UAAM,WAAY,KAAK,MAAgD;AAAA,MACrE,CAAC,MAAoC,KAAK;AAAA,IAC5C;AACA,UAAM,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM;AACxD,QAAI,QAAQ,WAAW,GAAG;AACxB,aAAO,oBAAoB,QAAQ,CAAC,CAAC,IAAI;AAAA,IAC3C;AACA,QAAI,QAAQ,SAAS,GAAG;AACtB,aAAO,QAAQ,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC,EAAE,KAAK,KAAK;AAAA,IAC9D;AAAA,EACF;AAGA,MAAI,KAAK,QAAQ,MAAM,QAAQ,KAAK,IAAI,GAAG;AACzC,WAAQ,KAAK,KAAkB,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,KAAK;AAAA,EAChE;AAEA,QAAM,OAAO,KAAK;AAClB,QAAM,SAAS,KAAK;AAEpB,MAAI,SAAS,SAAS;AACpB,UAAM,QAAQ,KAAK;AACnB,QAAI,MAAO,QAAO,GAAG,oBAAoB,KAAK,CAAC;AAC/C,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,SAAU,QAAO;AAE9B,MAAI,WAAW,OAAQ,QAAO;AAC9B,MAAI,WAAW,YAAa,QAAO;AACnC,MAAI,WAAW,OAAQ,QAAO;AAC9B,MAAI,WAAW,QAAS,QAAO;AAE/B,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAA,IACL,KAAK;AAAW,aAAO;AAAA,IACvB,KAAK;AAAW,aAAO;AAAA,IACvB;AAAS,aAAO;AAAA,EAClB;AACF;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC;AAC9C;AAEA,SAAS,YAAY,GAAmB;AACtC,MAAI,EAAE,SAAS,KAAK,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE,IAAI;AAC/C,MAAI,EAAE,SAAS,KAAK,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE;AAC3C,MAAI,EAAE,SAAS,GAAG,KAAK,CAAC,EAAE,SAAS,IAAI,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE;AAC9D,SAAO;AACT;AAMA,SAAS,sBAAsB,MAAuB;AACpD,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,WAAO,qBAAqB,KAAK,UAAU,IAAI,CAAC;AAAA,EAClD;AAGA,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAM,SAAS;AACf,UAAM,QAAQ,OAAO,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,UAAU;AAC9C,YAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,KAAK,KAAK,GAAG,IAAI;AAChE,YAAM,MAAM,MAAM,WAAqB,YAAY,MAAM,QAAQ,MAAM,MAAM,QAAkB;AAC/F,aAAO,OAAO,GAAG,IAAI,KAAK,GAAG,KAAK;AAAA,IACpC,CAAC;AACD,QAAI,MAAM,SAAS,GAAG;AACpB,aAAO,4BAAuB,MAAM,KAAK,IAAI,CAAC;AAAA,IAChD;AAAA,EACF;AAEA,QAAM,MAAM;AAGZ,MAAI,IAAI,eAAe,OAAO,IAAI,gBAAgB,UAAU;AAC1D,UAAM,cAAc,IAAI;AACxB,UAAM,QAAkB,CAAC;AACzB,eAAW,CAAC,OAAO,QAAQ,KAAK,OAAO,QAAQ,WAAW,GAAG;AAC3D,UAAI,MAAM,QAAQ,QAAQ,KAAK,SAAS,SAAS,GAAG;AAClD,cAAM,KAAK,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,EAAE;AAAA,MACvC;AAAA,IACF;AACA,UAAM,aAAa,IAAI;AACvB,QAAI,MAAM,QAAQ,UAAU,KAAK,WAAW,SAAS,GAAG;AACtD,YAAM,KAAK,WAAW,CAAC,CAAC;AAAA,IAC1B;AACA,QAAI,MAAM,SAAS,GAAG;AACpB,aAAO,4BAAuB,MAAM,KAAK,IAAI,CAAC;AAAA,IAChD;AAAA,EACF;AAGA,MAAI,IAAI,UAAU,MAAM,QAAQ,IAAI,MAAM,GAAG;AAC3C,UAAM,SAAS,IAAI;AACnB,UAAM,QAAQ,OAAO,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,UAAU;AAC9C,YAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,KAAK,KAAK,GAAG,IAAI;AAChE,YAAM,MAAM,MAAM,WAAqB,MAAM,QAAkB;AAC/D,aAAO,OAAO,GAAG,IAAI,KAAK,GAAG,KAAK;AAAA,IACpC,CAAC;AACD,WAAO,4BAAuB,MAAM,KAAK,IAAI,CAAC;AAAA,EAChD;AAGA,MAAI,IAAI,SAAS,OAAO,IAAI,UAAU,UAAU;AAC9C,UAAM,UAAU,IAAI;AACpB,QAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,aAAO,sBAAsB,OAAO;AAAA,IACtC;AACA,WAAO,IAAI;AAAA,EACb;AAGA,MAAI,IAAI,WAAW,OAAO,IAAI,YAAY,UAAU;AAClD,WAAO,IAAI;AAAA,EACb;AAGA,QAAM,OAAO,KAAK,UAAU,IAAI;AAChC,MAAI,KAAK,SAAS,KAAK;AACrB,WAAO,iCAAiC,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,EAC5D;AACA,SAAO,qBAAqB,IAAI;AAClC;AAKA,SAAS,mBAAmB,OAAoB;AAC9C,SAAO,MAAM,MAAM,IAAI,CAAC,SAAS;AAC/B,UAAM,gBAAgB,MAAM,MACzB,OAAO,CAAC,SAAS,KAAK,WAAW,KAAK,SAAS,EAC/C,IAAI,CAAC,UAAU;AAAA,MACd,cAAc,KAAK;AAAA,MACnB,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,UAAU,KAAK;AAAA,IACjB,EAAE;AAEJ,WAAO;AAAA,MACL,WAAW,KAAK;AAAA,MAChB,WAAW,KAAK;AAAA,MAChB,QAAQ,sBAAsB,KAAK,WAAW,KAAK,SAAS;AAAA,MAC5D,QAAQ,KAAK;AAAA,MACb;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAGO,MAAM,0BAA0B;AAEhC,MAAM,+BAA+B;AAO5C,eAAsB,oBAAqC;AACzD,QAAM,cAAc,MAAM,oBAAoB;AAC9C,qBAAmB;AACnB,sBAAoB,WAAW;AAC/B,SAAO;AACT;AAKA,SAAS,qBAA2B;AAClC;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA;AAAA;AAAA,MAGb,aAAa,EAAE,OAAO;AAAA,QACpB,MAAM,EACH,OAAO,EACP;AAAA,UACC;AAAA,QACF;AAAA,MACJ,CAAC;AAAA,MACD,kBAAkB,CAAC,GAAG,2BAA2B;AAAA,MACjD,SAAS,OAAO,OAAyB,QAAwB;AAC/D,cAAM,cAAc,MAAM,KAAK,MAAM,GAAG,GAAG,EAAE,QAAQ,OAAO,GAAG;AAC/D,gBAAQ,MAAM,4BAA4B,WAAW,GAAG,MAAM,KAAK,SAAS,MAAM,QAAQ,EAAE,GAAG;AAG/F,YAAI,IAAI,WAAW;AACjB,gBAAM,SAAS,kBAAkB,IAAI,WAAW,MAAM,IAAI;AAC1D,cAAI,QAAQ;AACV,oBAAQ,MAAM,wCAAwC,OAAO,KAAK,IAAI;AACtE,kBAAMA,iBAAgB,mBAAmB,IAAI,SAAS;AACtD,mBAAO;AAAA,cACL,SAAS;AAAA,cACT,QAAQ,OAAO;AAAA,cACf,WAAW;AAAA,cACX,gBAAgBA;AAAA,YAClB;AAAA,UACF;AAGA,gBAAM,EAAE,OAAO,SAAS,IAAI,uBAAuB,IAAI,SAAS;AAChE,cAAI,UAAU;AACZ,oBAAQ,MAAM,sDAAsD,KAAK,GAAG;AAC5E,mBAAO;AAAA,cACL,SAAS;AAAA,cACT,OAAO;AAAA,YACT;AAAA,UACF;AAAA,QACF;AAEA,cAAM,OAAO,MAAM,gBAAgB;AACnC,cAAM,UAAU,cAAc,EAAE,KAAK,CAAC;AACtC,cAAM,SAAS,MAAM,QAAQ,QAAQ,MAAM,IAAI;AAE/C,YAAI,OAAO,OAAO;AAChB,kBAAQ,MAAM,+BAA+B,OAAO,UAAU,aAAQ,OAAO,KAAK,EAAE;AACpF,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,OAAO,OAAO;AAAA,YACd,MAAM,OAAO;AAAA,YACb,YAAY,OAAO;AAAA,UACrB;AAAA,QACF;AAEA,cAAM,YAAY,eAAe,OAAO,MAAM;AAC9C,gBAAQ,MAAM,4BAA4B,OAAO,UAAU,aAAQ,UAAU,MAAM,QAAQ;AAG3F,YAAI,IAAI,WAAW;AACjB,gBAAM,QAAQ,iBAAiB,MAAM,IAAI;AACzC,4BAAkB,IAAI,WAAW,MAAM,MAAM,WAAW,KAAK;AAAA,QAC/D;AAEA,cAAM,gBAAgB,IAAI,YAAY,mBAAmB,IAAI,SAAS,IAAI;AAC1E,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ;AAAA,UACR,MAAM,OAAO;AAAA,UACb,YAAY,OAAO;AAAA,UACnB,gBAAgB;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,IACA,EAAE,UAAU,WAAW;AAAA,EACzB;AACF;AAKA,SAAS,oBAAoB,aAA2B;AACtD,QAAM,aAAa,cACf;AAAA;AAAA,EAAO,WAAW,KAClB;AAEJ;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA;AAAA,6XAE2V,UAAU;AAAA,MAClX,aAAa,EAAE,OAAO;AAAA,QACpB,MAAM,EACH,OAAO,EACP;AAAA,UACC;AAAA,QACF;AAAA,MACJ,CAAC;AAAA,MACD,kBAAkB,CAAC,GAAG,2BAA2B;AAAA,MACjD,SAAS,OAAO,OAAyB,QAAwB;AAC/D,cAAM,cAAc,MAAM,KAAK,MAAM,GAAG,GAAG,EAAE,QAAQ,OAAO,GAAG;AAC/D,gBAAQ,MAAM,6BAA6B,WAAW,GAAG,MAAM,KAAK,SAAS,MAAM,QAAQ,EAAE,UAAU,IAAI,UAAU,SAAS,EAAE;AAGhI,YAAI,IAAI,WAAW;AACjB,gBAAM,EAAE,OAAO,SAAS,IAAI,uBAAuB,IAAI,SAAS;AAChE,cAAI,UAAU;AACZ,oBAAQ,MAAM,uDAAuD,KAAK,GAAG;AAC7E,mBAAO;AAAA,cACL,SAAS;AAAA,cACT,OAAO;AAAA,YACT;AAAA,UACF;AAAA,QACF;AAKA,cAAM,cAAc;AACpB,YAAI,eAAe;AACnB,YAAI,oBAAoB;AAExB,cAAM,eAAe,mBAAmB,KAAK,CAAC,qBAAqB;AACjE;AACA,cAAI,eAAe,aAAa;AAC9B,kBAAM,IAAI,MAAM,gCAAgC,WAAW,GAAG;AAAA,UAChE;AACA,cAAI,mBAAmB,gBAAgB,GAAG;AACxC;AACA,gBAAI,oBAAoB,8BAA8B;AACpD,oBAAM,IAAI,MAAM,yCAAyC,4BAA4B,GAAG;AAAA,YAC1F;AAAA,UACF;AAAA,QACF,CAAC;AAED,cAAM,UAAU;AAAA,UACd,UAAU,IAAI;AAAA,UACd,gBAAgB,IAAI;AAAA,UACpB,QAAQ,IAAI;AAAA,QACd;AAEA,cAAM,UAAU;AAAA,UACd,EAAE,KAAK,EAAE,SAAS,aAAa,GAAG,QAAQ;AAAA,UAC1C,EAAE,YAAY;AAAA,QAChB;AAEA,cAAM,SAAS,MAAM,QAAQ,QAAQ,MAAM,IAAI;AAE/C,YAAI,OAAO,OAAO;AAChB,kBAAQ,MAAM,gCAAgC,OAAO,UAAU,sBAAiB,YAAY,WAAM,OAAO,KAAK,EAAE;AAChH,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,OAAO,OAAO;AAAA,YACd,MAAM,OAAO;AAAA,YACb,YAAY,OAAO;AAAA,YACnB;AAAA,UACF;AAAA,QACF;AAEA,cAAM,YAAY,eAAe,OAAO,MAAM;AAC9C,gBAAQ,MAAM,6BAA6B,OAAO,UAAU,sBAAiB,YAAY,WAAM,UAAU,MAAM,QAAQ;AAEvH,cAAM,gBAAgB,IAAI,YAAY,mBAAmB,IAAI,SAAS,IAAI;AAC1E,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ;AAAA,UACR,MAAM,OAAO;AAAA,UACb,YAAY,OAAO;AAAA,UACnB;AAAA,UACA,gBAAgB;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,IACA,EAAE,UAAU,WAAW;AAAA,EACzB;AACF;AAKO,SAAS,mBACd,KACA,QAMqB;AACrB,QAAM,UACJ,QAAQ,IAAI,4BACZ,QAAQ,IAAI,uBACZ,QAAQ,IAAI,WACZ;AAEF,SAAO,OAAO,WAAW;AACvB,UAAM,EAAE,QAAQ,MAAM,OAAO,KAAK,IAAI;AACtC,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,mBAAmB,OAAO,UAAU,EAAE,EAAE,YAAY;AAC1D,WAAO,gBAAgB;AACvB,UAAM,UAAU,wBAAwB,IAAI;AAC5C,UAAM,gBAAgB,MAAM,4BAA4B,KAAK,kBAAkB,OAAO;AAEtF,QAAI,CAAC,cAAc,SAAS;AAC1B,YAAMC,gBAAe,KAAK,IAAI,IAAI;AAClC,cAAQ;AAAA,QACN,2BAA2B,gBAAgB,IAAI,OAAO,WAAM,cAAc,UAAU,OAAOA,aAAY;AAAA,MACzG;AACA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY,cAAc;AAAA,QAC1B,OAAO,cAAc;AAAA,QACrB,SAAS,cAAc;AAAA,MACzB;AAAA,IACF;AAEA,QAAI,MAAM,GAAG,OAAO,GAAG,OAAO;AAG9B,UAAM,cAAsC,EAAE,GAAG,MAAM;AAEvD,QAAI,qBAAqB,OAAO;AAC9B,UAAI,IAAI,SAAU,aAAY,WAAW,IAAI;AAC7C,UAAI,IAAI,eAAgB,aAAY,iBAAiB,IAAI;AAAA,IAC3D;AAEA,QAAI,OAAO,KAAK,WAAW,EAAE,SAAS,GAAG;AACvC,YAAM,YAAY,IAAI,SAAS,GAAG,IAAI,MAAM;AAC5C,aAAO,YAAY,IAAI,gBAAgB,WAAW,EAAE,SAAS;AAAA,IAC/D;AAGA,QAAI;AACJ,QAAI,CAAC,QAAQ,OAAO,OAAO,EAAE,SAAS,gBAAgB,GAAG;AACvD,oBAAc,EAAE,GAAG,KAAK;AACxB,UAAI,IAAI,SAAU,aAAY,WAAW,IAAI;AAC7C,UAAI,IAAI,eAAgB,aAAY,iBAAiB,IAAI;AAAA,IAC3D;AAGA,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,IAClB;AACA,QAAI,IAAI,aAAc,SAAQ,WAAW,IAAI,IAAI;AACjD,QAAI,IAAI,SAAU,SAAQ,aAAa,IAAI,IAAI;AAC/C,QAAI,IAAI,eAAgB,SAAQ,mBAAmB,IAAI,IAAI;AAG3D,UAAM,WAAW,MAAM,iBAAiB,KAAK;AAAA,MAC3C,QAAQ;AAAA,MACR;AAAA,MACA,MAAM,cAAc,KAAK,UAAU,WAAW,IAAI;AAAA,MAClD,WAAW,6BAA6B;AAAA,IAC1C,CAAC;AAED,UAAM,eAAe,MAAM,SAAS,KAAK;AACzC,UAAM,OAAO,aAAa,YAAY;AACtC,UAAM,eAAe,KAAK,IAAI,IAAI;AAElC,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,2BAA2B,gBAAgB,IAAI,OAAO,WAAM,SAAS,MAAM,OAAO,YAAY,IAAI;AAGhH,UAAI,SAAS,WAAW,KAAK;AAC3B,eAAO;AAAA,UACL,SAAS;AAAA,UACT,YAAY;AAAA,UACZ,OAAO,sBAAsB,IAAI;AAAA,QACnC;AAAA,MACF;AAEA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY,SAAS;AAAA,QACrB,OAAO,aAAa,SAAS,MAAM;AAAA,QACnC,SAAS;AAAA,MACX;AAAA,IACF;AAEA,YAAQ,MAAM,2BAA2B,gBAAgB,IAAI,OAAO,WAAM,SAAS,MAAM,OAAO,YAAY,OAAO,aAAa,MAAM,SAAS;AAG/I,QAAI,CAAC,CAAC,OAAO,QAAQ,SAAS,EAAE,SAAS,gBAAgB,GAAG;AAC1D,aAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY,SAAS;AAAA,QACrB;AAAA,QACA,OAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY,SAAS;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;AAMA,eAAsB,4BACpB,KACA,QACA,MACmC;AACnC,QAAM,mBAAmB,OAAO,YAAY;AAE5C,MAAI,uBAAuB,IAAI,GAAG;AAChC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,OAAO,uCAAuC,gBAAgB,IAAI,IAAI;AAAA,IACxE;AAAA,EACF;AAEA,QAAM,iBAAiB,wBAAwB,IAAI;AACnD,QAAM,WAAW,MAAM,wBAAwB,kBAAkB,cAAc;AAE/E,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,OAAO,mDAAmD,gBAAgB,IAAI,cAAc;AAAA,IAC9F;AAAA,EACF;AAEA,QAAM,cAAc,mBAAmB,GAAG;AAC1C,QAAM,mBAAmB,SAAS,oBAAoB,CAAC;AAEvD,MAAI,iBAAiB,SAAS,GAAG;AAC/B,QAAI,oBAAoB,kBAAkB,IAAI,cAAc,IAAI,cAAc,WAAW,GAAG;AAC1F,aAAO,EAAE,SAAS,MAAM,SAAS;AAAA,IACnC;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,OAAO,gCAAgC,gBAAgB,IAAI,cAAc;AAAA,MACzE,SAAS,EAAE,kBAAkB,aAAa,SAAS,YAAY;AAAA,IACjE;AAAA,EACF;AAEA,MAAI,mBAAmB,gBAAgB,GAAG;AACxC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,OAAO,+EAA+E,gBAAgB,IAAI,cAAc;AAAA,MACxH,SAAS,EAAE,aAAa,SAAS,YAAY;AAAA,IAC/C;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,MAAM,SAAS;AACnC;AAEA,SAAS,mBAAmB,KAA8C;AACxE,MAAI;AACF,WAAO,IAAI,UAAU,QAAQ,aAAa;AAAA,EAC5C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,wBACb,QACA,MAC6B;AAC7B,QAAM,YAAY,MAAM,gBAAgB;AACxC,QAAM,aAAa,UAAU,KAAK,CAAC,aAAa,SAAS,WAAW,UAAU,SAAS,SAAS,IAAI;AACpG,MAAI,YAAY;AACd,WAAO;AAAA,EACT;AAEA,SAAO,UAAU,KAAK,CAAC,aAAa,SAAS,WAAW,UAAU,qBAAqB,SAAS,MAAM,IAAI,CAAC,KAAK;AAClH;AAEO,SAAS,qBAAqB,cAAsB,aAA8B;AACvF,QAAM,yBAAyB,wBAAwB,YAAY;AACnE,QAAM,wBAAwB,wBAAwB,WAAW;AAEjE,MAAI,2BAA2B,uBAAuB;AACpD,WAAO;AAAA,EACT;AAEA,QAAM,mBAAmB,uBAAuB,MAAM,GAAG,EAAE,OAAO,OAAO;AACzE,QAAM,kBAAkB,sBAAsB,MAAM,GAAG,EAAE,OAAO,OAAO;AAEvE,MAAI,iBAAiB,WAAW,gBAAgB,QAAQ;AACtD,WAAO;AAAA,EACT;AAEA,SAAO,iBAAiB,MAAM,CAAC,SAAS,UAAU;AAChD,QAAI,uBAAuB,OAAO,GAAG;AACnC,aAAO,gBAAgB,KAAK,EAAE,SAAS;AAAA,IACzC;AACA,WAAO,YAAY,gBAAgB,KAAK;AAAA,EAC1C,CAAC;AACH;AAEA,SAAS,wBAAwB,MAAsB;AACrD,QAAM,CAAC,OAAO,IAAI,KAAK,MAAM,GAAG;AAChC,QAAM,iBAAiB,QAAQ,WAAW,MAAM,IAC5C,UACA,OAAO,QAAQ,WAAW,GAAG,IAAI,UAAU,IAAI,OAAO,EAAE;AAE5D,MAAI,eAAe,SAAS,KAAK,eAAe,SAAS,GAAG,GAAG;AAC7D,WAAO,eAAe,MAAM,GAAG,EAAE;AAAA,EACnC;AAEA,SAAO;AACT;AAEA,MAAM,sBAAsB,oBAAI,IAAI,CAAC,KAAK,KAAK,CAAC;AAChD,MAAM,sBAAsB,oBAAI,IAAI,CAAC,MAAM,QAAQ,QAAQ,QAAQ,CAAC;AAU7D,SAAS,uBAAuB,MAAuB;AAC5D,QAAM,CAAC,OAAO,IAAI,OAAO,QAAQ,EAAE,EAAE,MAAM,GAAG;AAM9C,MAAI,kBAAkB,KAAK,OAAO,GAAG;AACnC,WAAO;AAAA,EACT;AAIA,MAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,WAAO;AAAA,EACT;AAIA,MAAI,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,GAAG;AAChD,WAAO;AAAA,EACT;AAEA,SAAO,QAAQ,MAAM,GAAG,EAAE,KAAK,CAAC,YAAY;AAC1C,UAAM,UAAU,QAAQ,YAAY;AACpC,WAAO,oBAAoB,IAAI,OAAO,KAAK,oBAAoB,IAAI,OAAO;AAAA,EAC5E,CAAC;AACH;AAEA,SAAS,uBAAuB,SAA0B;AACxD,SACG,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,KAC/C,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,KAChD,QAAQ,WAAW,GAAG;AAE1B;AAEO,SAAS,mBAAmB,QAAyB;AAC1D,SAAO,CAAC,CAAC,OAAO,QAAQ,SAAS,EAAE,SAAS,OAAO,YAAY,CAAC;AAClE;AAEA,SAAS,aAAa,MAAuB;AAC3C,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
6
  "names": ["memoryContext", "callDuration"]
7
7
  }
@@ -48,16 +48,35 @@ async function compileAndImportGenerated(tsPath) {
48
48
  }
49
49
  return await import(pathToFileURL(jsPath).href);
50
50
  }
51
+ const UNSAFE_JS_STRING_CHAR_ESCAPES = {
52
+ 60: "\\u003C",
53
+ // < — HTML/script-tag breakout
54
+ 62: "\\u003E",
55
+ // > — HTML/script-tag breakout
56
+ 8232: "\\u2028",
57
+ // line separator — string content but a statement terminator pre-ES2019
58
+ 8233: "\\u2029"
59
+ // paragraph separator — same
60
+ };
61
+ function escapeUnsafeJsStringChars(value) {
62
+ return value.replace(
63
+ /[<>\u2028\u2029]/g,
64
+ (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)]
65
+ );
66
+ }
67
+ function toSafeJsStringLiteral(value) {
68
+ return escapeUnsafeJsStringChars(JSON.stringify(value));
69
+ }
51
70
  function rewriteGeneratedAliasImports(source, appRoot) {
52
71
  const resolveAlias = (relativePath) => {
53
72
  const target = path.join(appRoot, relativePath);
54
73
  const candidate = fs.existsSync(target) ? target : fs.existsSync(target + ".ts") ? target + ".ts" : target;
55
- return pathToFileURL(candidate).href;
74
+ return toSafeJsStringLiteral(pathToFileURL(candidate).href);
56
75
  };
57
76
  return source.replace(/from\s+["']@\/([^"']+)["']/g, (_match, relativePath) => {
58
- return `from ${JSON.stringify(resolveAlias(relativePath))}`;
77
+ return `from ${resolveAlias(relativePath)}`;
59
78
  }).replace(/import\s*\(\s*["']@\/([^"']+)["']\s*\)/g, (_match, relativePath) => {
60
- return `import(${JSON.stringify(resolveAlias(relativePath))})`;
79
+ return `import(${resolveAlias(relativePath)})`;
61
80
  });
62
81
  }
63
82
  async function ensureApiRouteManifestsRegistered() {
@@ -85,6 +104,7 @@ async function ensureApiRouteManifestsRegistered() {
85
104
  export {
86
105
  compileAndImportGenerated,
87
106
  ensureApiRouteManifestsRegistered,
107
+ escapeUnsafeJsStringChars,
88
108
  findGeneratedFile,
89
109
  rewriteGeneratedAliasImports
90
110
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/generated-registry-loader.ts"],
4
- "sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Resolves the `@/`\n * alias to the app root, transpiles TS \u2192 ESM, and emits a sibling `.mjs` we can\n * `import()` from Node. Cached on mtime so repeat calls in the same process\n * don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n */\nexport async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n const aliasRewritten = rewriteGeneratedAliasImports(tsSource, appRoot)\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\n/**\n * Rewrite `@/...` path-alias imports (both `from \"@/x\"` and dynamic\n * `import(\"@/x\")`) in generated-registry source to absolute `file://` URLs\n * rooted at `appRoot`. The `@/` alias is a Next.js bundler convention; outside\n * the bundler Node treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(source: string, appRoot: string): string {\n const resolveAlias = (relativePath: string): string => {\n const target = path.join(appRoot, relativePath)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return pathToFileURL(candidate).href\n }\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${JSON.stringify(resolveAlias(relativePath))}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${JSON.stringify(resolveAlias(relativePath))})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n console.warn(\n '[MCP Tools] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n return 0\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Resolves the `@/`\n * alias to the app root, transpiles TS \u2192 ESM, and emits a sibling `.mjs` we can\n * `import()` from Node. Cached on mtime so repeat calls in the same process\n * don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n */\nexport async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n const aliasRewritten = rewriteGeneratedAliasImports(tsSource, appRoot)\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\nconst UNSAFE_JS_STRING_CHAR_ESCAPES: Record<number, string> = {\n 0x3c: '\\\\u003C', // < \u2014 HTML/script-tag breakout\n 0x3e: '\\\\u003E', // > \u2014 HTML/script-tag breakout\n 0x2028: '\\\\u2028', // line separator \u2014 string content but a statement terminator pre-ES2019\n 0x2029: '\\\\u2029', // paragraph separator \u2014 same\n}\n\n/**\n * Escape characters that `JSON.stringify` leaves intact but which can still\n * break out of (or alter the meaning of) the JavaScript string literal that the\n * stringified value is embedded into \u2014 notably `<`/`>` (HTML/script-tag\n * breakout) and the U+2028 / U+2029 line separators (valid string content but\n * statement terminators in pre-ES2019 parsers). Apply this on top of\n * `JSON.stringify` so the emitted import source stays well-formed regardless of\n * the resolved path. Exported for unit testing.\n */\nexport function escapeUnsafeJsStringChars(value: string): string {\n return value.replace(\n /[<>\\u2028\\u2029]/g,\n (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)],\n )\n}\n\n/**\n * Stringify a resolved path into a JavaScript string literal that is safe to\n * embed in generated source: `JSON.stringify` handles quoting/standard escapes,\n * and `escapeUnsafeJsStringChars` neutralizes the characters it leaves intact.\n */\nfunction toSafeJsStringLiteral(value: string): string {\n return escapeUnsafeJsStringChars(JSON.stringify(value))\n}\n\n/**\n * Rewrite `@/...` path-alias imports (both `from \"@/x\"` and dynamic\n * `import(\"@/x\")`) in generated-registry source to absolute `file://` URLs\n * rooted at `appRoot`. The `@/` alias is a Next.js bundler convention; outside\n * the bundler Node treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(source: string, appRoot: string): string {\n const resolveAlias = (relativePath: string): string => {\n const target = path.join(appRoot, relativePath)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return toSafeJsStringLiteral(pathToFileURL(candidate).href)\n }\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${resolveAlias(relativePath)}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${resolveAlias(relativePath)})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n console.warn(\n '[MCP Tools] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n return 0\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.5-develop.5382.1.f542de69af",
3
+ "version": "0.6.6-develop.5412.1.e2a52b14f0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -98,16 +98,16 @@
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.5-develop.5382.1.f542de69af",
102
- "@open-mercato/ui": "0.6.5-develop.5382.1.f542de69af",
101
+ "@open-mercato/shared": "0.6.6-develop.5412.1.e2a52b14f0",
102
+ "@open-mercato/ui": "0.6.6-develop.5412.1.e2a52b14f0",
103
103
  "react": "^19.0.0",
104
104
  "react-dom": "^19.0.0",
105
105
  "zod": ">=3.23.0"
106
106
  },
107
107
  "devDependencies": {
108
- "@open-mercato/cli": "0.6.5-develop.5382.1.f542de69af",
109
- "@open-mercato/shared": "0.6.5-develop.5382.1.f542de69af",
110
- "@open-mercato/ui": "0.6.5-develop.5382.1.f542de69af",
108
+ "@open-mercato/cli": "0.6.6-develop.5412.1.e2a52b14f0",
109
+ "@open-mercato/shared": "0.6.6-develop.5412.1.e2a52b14f0",
110
+ "@open-mercato/ui": "0.6.6-develop.5412.1.e2a52b14f0",
111
111
  "@types/react": "^19.2.17",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.7",
@@ -123,5 +123,5 @@
123
123
  "url": "https://github.com/open-mercato/open-mercato",
124
124
  "directory": "packages/ai-assistant"
125
125
  },
126
- "stableVersion": "0.6.4"
126
+ "stableVersion": "0.6.5"
127
127
  }
@@ -349,6 +349,118 @@ describe('resolveAttachmentParts — tenant / org scope enforcement', () => {
349
349
  expect(parts).toEqual([])
350
350
  expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('not found'))
351
351
  })
352
+
353
+ // Regression for #2663 — null-scoped rows used to bypass the tenant check via
354
+ // a truthiness short-circuit and leak into the caller's LLM context.
355
+ it('drops a null-tenant (global) record for a non super-admin caller', async () => {
356
+ findOneWithDecryptionMock.mockResolvedValueOnce(
357
+ makeRow({
358
+ id: 'null-tenant-1',
359
+ tenantId: null,
360
+ organizationId: null,
361
+ mimeType: 'image/png',
362
+ fileSize: 64,
363
+ content: 'secret OCR text',
364
+ }),
365
+ )
366
+
367
+ const parts = await resolveAttachmentParts({
368
+ attachmentIds: ['null-tenant-1'],
369
+ authContext: makeAuth({ tenantId: 'tenant-1', organizationId: 'org-1' }),
370
+ container: makeContainer(),
371
+ })
372
+
373
+ expect(parts).toEqual([])
374
+ expect(fsReadFileMock).not.toHaveBeenCalled()
375
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('out of scope'))
376
+ })
377
+
378
+ // Regression for #2663 — `tenantId='X', organizationId=null` used to be
379
+ // readable by every org within tenant X.
380
+ it('drops a null-org record in the caller tenant for an org-scoped caller', async () => {
381
+ findOneWithDecryptionMock.mockResolvedValueOnce(
382
+ makeRow({
383
+ id: 'null-org-1',
384
+ tenantId: 'tenant-1',
385
+ organizationId: null,
386
+ mimeType: 'image/png',
387
+ fileSize: 64,
388
+ }),
389
+ )
390
+
391
+ const parts = await resolveAttachmentParts({
392
+ attachmentIds: ['null-org-1'],
393
+ authContext: makeAuth({ tenantId: 'tenant-1', organizationId: 'org-1' }),
394
+ container: makeContainer(),
395
+ })
396
+
397
+ expect(parts).toEqual([])
398
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('out of scope'))
399
+ })
400
+
401
+ it('lets a tenant-wide caller (null org) read any org within its tenant', async () => {
402
+ findOneWithDecryptionMock.mockResolvedValueOnce(
403
+ makeRow({
404
+ id: 'same-tenant-other-org',
405
+ tenantId: 'tenant-1',
406
+ organizationId: 'org-OTHER',
407
+ mimeType: 'application/zip',
408
+ fileSize: 64,
409
+ content: null,
410
+ }),
411
+ )
412
+
413
+ const parts = await resolveAttachmentParts({
414
+ attachmentIds: ['same-tenant-other-org'],
415
+ authContext: makeAuth({ tenantId: 'tenant-1', organizationId: null }),
416
+ container: makeContainer(),
417
+ })
418
+
419
+ expect(parts).toHaveLength(1)
420
+ expect(parts[0].attachmentId).toBe('same-tenant-other-org')
421
+ })
422
+
423
+ it('scopes the ORM query by tenant + org for a non super-admin caller', async () => {
424
+ findOneWithDecryptionMock.mockResolvedValueOnce(makeRow({ id: 'att-scope-1', fileSize: 0 }))
425
+
426
+ await resolveAttachmentParts({
427
+ attachmentIds: ['att-scope-1'],
428
+ authContext: makeAuth({ tenantId: 'tenant-1', organizationId: 'org-1' }),
429
+ container: makeContainer(),
430
+ })
431
+
432
+ const whereArg = findOneWithDecryptionMock.mock.calls[0][2]
433
+ expect(whereArg).toEqual({ id: 'att-scope-1', tenantId: 'tenant-1', organizationId: 'org-1' })
434
+ })
435
+
436
+ it('omits org from the ORM query for a tenant-wide caller (null org)', async () => {
437
+ findOneWithDecryptionMock.mockResolvedValueOnce(makeRow({ id: 'att-scope-2', fileSize: 0 }))
438
+
439
+ await resolveAttachmentParts({
440
+ attachmentIds: ['att-scope-2'],
441
+ authContext: makeAuth({ tenantId: 'tenant-1', organizationId: null }),
442
+ container: makeContainer(),
443
+ })
444
+
445
+ const whereArg = findOneWithDecryptionMock.mock.calls[0][2]
446
+ expect(whereArg).toEqual({ id: 'att-scope-2', tenantId: 'tenant-1' })
447
+ expect(whereArg).not.toHaveProperty('organizationId')
448
+ })
449
+
450
+ it('does not scope the ORM query for super-admin callers', async () => {
451
+ findOneWithDecryptionMock.mockResolvedValueOnce(
452
+ makeRow({ id: 'att-scope-3', tenantId: 'tenant-OTHER', organizationId: 'org-OTHER', fileSize: 0 }),
453
+ )
454
+
455
+ await resolveAttachmentParts({
456
+ attachmentIds: ['att-scope-3'],
457
+ authContext: makeAuth({ isSuperAdmin: true }),
458
+ container: makeContainer(),
459
+ })
460
+
461
+ const whereArg = findOneWithDecryptionMock.mock.calls[0][2]
462
+ expect(whereArg).toEqual({ id: 'att-scope-3' })
463
+ })
352
464
  })
353
465
 
354
466
  describe('resolveAttachmentParts — unavailable service graceful skip', () => {
@@ -7,6 +7,7 @@ import {
7
7
  CODE_MODE_MAX_MUTATION_CALLS,
8
8
  CODE_MODE_REQUIRED_FEATURES,
9
9
  createApiRequestFn,
10
+ isUnsafeApiRequestPath,
10
11
  isUnsafeHttpMethod,
11
12
  matchApiEndpointPath,
12
13
  } from '../codemode-tools'
@@ -287,6 +288,112 @@ describe('authorizeCodeModeApiRequest', () => {
287
288
  })
288
289
  })
289
290
 
291
+ describe('path traversal hardening (issue #2667)', () => {
292
+ describe('isUnsafeApiRequestPath', () => {
293
+ it.each([
294
+ '/api/foo/../admin',
295
+ '/api/foo/..',
296
+ '/api/foo/./admin',
297
+ '/api/.',
298
+ '/api/foo/%2e%2e/admin',
299
+ '/api/foo/%2E%2E/admin',
300
+ '/api/foo/%2e./admin',
301
+ '/api/foo/.%2e/admin',
302
+ '/api/foo/%2e/admin',
303
+ '/api/foo\\admin',
304
+ '/api/foo%2fadmin',
305
+ '/api/foo%2Fadmin',
306
+ '/api/foo%5cadmin',
307
+ 'foo/../admin',
308
+ '/api/foo/../admin?expand=1',
309
+ // Raw ASCII tab/newline/CR are stripped by the WHATWG URL parser before
310
+ // the fetch, so `.<ctrl>.` collapses to `..` on the wire.
311
+ '/api/foo/.\t./admin',
312
+ '/api/foo/.\n./admin',
313
+ '/api/foo/.\r./admin',
314
+ '/api/foo/\t../admin',
315
+ ])('flags %s as unsafe', (path) => {
316
+ expect(isUnsafeApiRequestPath(path)).toBe(true)
317
+ })
318
+
319
+ it.each([
320
+ '/api/customers/companies',
321
+ '/api/customers/companies/company-1',
322
+ 'customers/companies/company-1?expand=1',
323
+ '/api/catalog/products/1.2.3',
324
+ '/api/customers/companies/00000000-0000-0000-0000-000000000000',
325
+ ])('treats %s as safe', (path) => {
326
+ expect(isUnsafeApiRequestPath(path)).toBe(false)
327
+ })
328
+ })
329
+
330
+ it('denies a traversal path even when a parameterized endpoint would match', async () => {
331
+ mockedGetApiEndpoints.mockReset()
332
+ // A documented read the user is authorized for. The un-normalized segment
333
+ // matcher would have accepted '/api/foo/..' against '/api/foo/{id}', then
334
+ // the wire fetch would collapse it to '/api/admin'. The guard blocks it.
335
+ mockedGetApiEndpoints.mockResolvedValue([
336
+ {
337
+ id: 'get_foo',
338
+ operationId: 'get_foo',
339
+ method: 'GET',
340
+ path: '/api/foo/{id}',
341
+ summary: '',
342
+ description: '',
343
+ tags: [],
344
+ requiredFeatures: [],
345
+ parameters: [],
346
+ requestBodySchema: null,
347
+ deprecated: false,
348
+ },
349
+ ])
350
+
351
+ const result = await authorizeCodeModeApiRequest(createContext(), 'GET', '/api/foo/..')
352
+
353
+ expect(result).toEqual({
354
+ allowed: false,
355
+ statusCode: 403,
356
+ error: 'Code Mode rejected unsafe API path: GET /api/foo/..',
357
+ })
358
+ })
359
+
360
+ it('never issues the wire request for a traversal path', async () => {
361
+ mockedGetApiEndpoints.mockReset()
362
+ mockedFetchWithTimeout.mockReset()
363
+ mockedFetchWithTimeout.mockResolvedValue(okResponse())
364
+ mockedGetApiEndpoints.mockResolvedValue([
365
+ {
366
+ id: 'create_company',
367
+ operationId: 'create_company',
368
+ method: 'POST',
369
+ path: '/api/customers/companies',
370
+ summary: '',
371
+ description: '',
372
+ tags: [],
373
+ requiredFeatures: ['customers.companies.create'],
374
+ parameters: [],
375
+ requestBodySchema: null,
376
+ deprecated: false,
377
+ },
378
+ ])
379
+
380
+ const apiRequest = createApiRequestFn(
381
+ createContext({ userFeatures: ['ai_assistant.view', 'customers.companies.create'] }),
382
+ () => {}
383
+ )
384
+
385
+ const result = (await apiRequest({
386
+ method: 'POST',
387
+ path: '/api/customers/companies/../admin',
388
+ body: {},
389
+ })) as { success: boolean; statusCode: number }
390
+
391
+ expect(result.success).toBe(false)
392
+ expect(result.statusCode).toBe(403)
393
+ expect(mockedFetchWithTimeout).not.toHaveBeenCalled()
394
+ })
395
+ })
396
+
290
397
  describe('mutation call cap (issue #2724)', () => {
291
398
  beforeEach(() => {
292
399
  mockedGetApiEndpoints.mockReset()
@@ -5,6 +5,7 @@ import { pathToFileURL } from 'node:url'
5
5
  import * as registry from '@open-mercato/shared/modules/registry'
6
6
  import {
7
7
  rewriteGeneratedAliasImports,
8
+ escapeUnsafeJsStringChars,
8
9
  findGeneratedFile,
9
10
  ensureApiRouteManifestsRegistered,
10
11
  } from '../generated-registry-loader'
@@ -13,6 +14,14 @@ function makeTempDir(): string {
13
14
  return fs.mkdtempSync(path.join(os.tmpdir(), 'om-gen-loader-'))
14
15
  }
15
16
 
17
+ // Mirror the production sanitizer so expectations match how the rewriter
18
+ // embeds a resolved path into the generated source. For ordinary file URLs the
19
+ // escape is a no-op, but routing the expected literal through the same helper
20
+ // keeps the test honest if the rewriter's escaping ever changes.
21
+ function safeJsLiteral(value: string): string {
22
+ return escapeUnsafeJsStringChars(JSON.stringify(value))
23
+ }
24
+
16
25
  describe('rewriteGeneratedAliasImports', () => {
17
26
  // Regression for the MCP dev-server crash:
18
27
  // "Cannot find package '@/.mercato' imported from .../tool-loader.js"
@@ -28,7 +37,7 @@ describe('rewriteGeneratedAliasImports', () => {
28
37
  const expectedUrl = pathToFileURL(
29
38
  path.join(appRoot, '.mercato/generated/entities'),
30
39
  ).href
31
- expect(out).toBe(`import { x } from ${JSON.stringify(expectedUrl)}`)
40
+ expect(out).toBe(`import { x } from ${safeJsLiteral(expectedUrl)}`)
32
41
  expect(out).not.toContain('@/')
33
42
  })
34
43
 
@@ -40,7 +49,7 @@ describe('rewriteGeneratedAliasImports', () => {
40
49
  const expectedUrl = pathToFileURL(
41
50
  path.join(appRoot, '.mercato/generated/ai-tools.generated'),
42
51
  ).href
43
- const expected = source.replace(aliasSpecifier, JSON.stringify(expectedUrl))
52
+ const expected = source.replace(aliasSpecifier, safeJsLiteral(expectedUrl))
44
53
  expect(out).toBe(expected)
45
54
  expect(out).not.toContain('@/')
46
55
  })
@@ -53,7 +62,7 @@ describe('rewriteGeneratedAliasImports', () => {
53
62
  const out = rewriteGeneratedAliasImports(`import x from '@/src/thing'`, appRoot)
54
63
 
55
64
  const expectedUrl = pathToFileURL(path.join(appRoot, 'src', 'thing.ts')).href
56
- expect(out).toBe(`import x from ${JSON.stringify(expectedUrl)}`)
65
+ expect(out).toBe(`import x from ${safeJsLiteral(expectedUrl)}`)
57
66
  })
58
67
 
59
68
  it('leaves non-alias imports untouched', () => {
@@ -66,6 +75,24 @@ describe('rewriteGeneratedAliasImports', () => {
66
75
  })
67
76
  })
68
77
 
78
+ describe('escapeUnsafeJsStringChars', () => {
79
+ it('escapes characters JSON.stringify leaves intact in a JS string literal', () => {
80
+ const lineSep = String.fromCharCode(0x2028)
81
+ const paraSep = String.fromCharCode(0x2029)
82
+ const out = escapeUnsafeJsStringChars(`a<b>c${lineSep}d${paraSep}e`)
83
+ expect(out).toBe('a\\u003Cb\\u003Ec\\u2028d\\u2029e')
84
+ expect(out).not.toContain('<')
85
+ expect(out).not.toContain('>')
86
+ expect(out).not.toContain(lineSep)
87
+ expect(out).not.toContain(paraSep)
88
+ })
89
+
90
+ it('leaves ordinary file URLs unchanged', () => {
91
+ const url = pathToFileURL(path.join('/tmp', 'app', '.mercato', 'x.generated')).href
92
+ expect(escapeUnsafeJsStringChars(JSON.stringify(url))).toBe(JSON.stringify(url))
93
+ })
94
+ })
95
+
69
96
  describe('findGeneratedFile', () => {
70
97
  let cwdSpy: jest.SpyInstance
71
98
 
@@ -128,10 +128,23 @@ async function loadAttachmentRow(
128
128
  // core package owns the MikroORM metadata and is the only place tests would
129
129
  // need to bootstrap for real DB access.
130
130
  const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')
131
+ // Tenant isolation is enforced in the SQL WHERE clause, not just the JS
132
+ // post-filter below: the decryption scope (5th arg) only drives field
133
+ // decryption, so omitting tenant/org from `where` would let `em.findOne`
134
+ // return a row from another tenant. Super-admins bypass the scope. Org-scoped
135
+ // callers also constrain by org; tenant-wide callers (organizationId === null)
136
+ // may read any org within their tenant.
137
+ const where: Record<string, unknown> = { id: attachmentId }
138
+ if (!authContext.isSuperAdmin) {
139
+ where.tenantId = authContext.tenantId
140
+ if (authContext.organizationId != null) {
141
+ where.organizationId = authContext.organizationId
142
+ }
143
+ }
131
144
  const record = await findOneWithDecryption(
132
145
  em,
133
146
  Attachment as never,
134
- { id: attachmentId } as never,
147
+ where as never,
135
148
  undefined,
136
149
  {
137
150
  tenantId: authContext.tenantId,
@@ -157,10 +170,18 @@ async function loadAttachmentRow(
157
170
 
158
171
  function rowBelongsToCaller(row: AttachmentRow, authContext: AiChatRequestContext): boolean {
159
172
  if (authContext.isSuperAdmin) return true
160
- // Tenant scope: if the record is tenant-scoped, it MUST match the caller tenant.
161
- if (row.tenantId && row.tenantId !== authContext.tenantId) return false
162
- // Organization scope: if the record is org-scoped, it MUST match the caller org.
163
- if (row.organizationId && row.organizationId !== authContext.organizationId) return false
173
+ // Tenant scope: fail closed. The record MUST carry the caller's tenant.
174
+ // A null `tenant_id` (a supported "global"/unscoped attachment state) is NOT
175
+ // accessible through the AI path: it has no `partition.isPublic` gate, so a
176
+ // truthiness short-circuit here would leak the bytes / extracted text into a
177
+ // different tenant's LLM context (cross-tenant IDOR, issue #2663). Requiring
178
+ // strict equality also rejects a non-super-admin caller with a null tenant.
179
+ if (authContext.tenantId == null || row.tenantId !== authContext.tenantId) return false
180
+ // Organization scope: when the caller is org-scoped, the record MUST match
181
+ // that organization (this also rejects null-org rows for an org-scoped
182
+ // caller). Tenant-wide callers (organizationId === null) may read any org
183
+ // within their tenant.
184
+ if (authContext.organizationId != null && row.organizationId !== authContext.organizationId) return false
164
185
  return true
165
186
  }
166
187
 
@@ -883,6 +883,15 @@ export async function authorizeCodeModeApiRequest(
883
883
  path: string
884
884
  ): Promise<CodeModeApiAuthorization> {
885
885
  const normalizedMethod = method.toUpperCase()
886
+
887
+ if (isUnsafeApiRequestPath(path)) {
888
+ return {
889
+ allowed: false,
890
+ statusCode: 403,
891
+ error: `Code Mode rejected unsafe API path: ${normalizedMethod} ${path}`,
892
+ }
893
+ }
894
+
886
895
  const normalizedPath = normalizeApiRequestPath(path)
887
896
  const endpoint = await findCodeModeApiEndpoint(normalizedMethod, normalizedPath)
888
897
 
@@ -979,6 +988,46 @@ function normalizeApiRequestPath(path: string): string {
979
988
  return normalizedPath
980
989
  }
981
990
 
991
+ const SINGLE_DOT_SEGMENTS = new Set(['.', '%2e'])
992
+ const DOUBLE_DOT_SEGMENTS = new Set(['..', '.%2e', '%2e.', '%2e%2e'])
993
+
994
+ /**
995
+ * Rejects request paths that the WHATWG URL parser would rewrite before the
996
+ * actual fetch (`..`/`.` path segments — including their percent-encoded forms
997
+ * — backslashes, and percent-encoded separators). Code Mode authorizes the
998
+ * literal path it was given, but `new URL()` collapses dot segments and
999
+ * normalizes backslashes for http(s) URLs, so without this guard the wire
1000
+ * request can resolve to a different endpoint than the one that was authorized.
1001
+ */
1002
+ export function isUnsafeApiRequestPath(path: string): boolean {
1003
+ const [rawPath] = String(path ?? '').split('?')
1004
+
1005
+ // The WHATWG URL parser strips ASCII tab/newline/carriage-return from the URL
1006
+ // before parsing, so a smuggled `.<TAB>.` segment collapses to `..` on the
1007
+ // wire even though the literal segment never equals a dot segment here. Raw
1008
+ // control characters never appear in legitimate REST paths, so reject them.
1009
+ if (/[\u0000-\u001f]/.test(rawPath)) {
1010
+ return true
1011
+ }
1012
+
1013
+ // http(s) URLs treat backslashes as path separators, so they can smuggle
1014
+ // separators past the segment-based authorizer.
1015
+ if (rawPath.includes('\\')) {
1016
+ return true
1017
+ }
1018
+
1019
+ // Percent-encoded separators never appear in legitimate REST paths and let
1020
+ // the literal-'/' segment split desync from the parsed request URL.
1021
+ if (/%2f/i.test(rawPath) || /%5c/i.test(rawPath)) {
1022
+ return true
1023
+ }
1024
+
1025
+ return rawPath.split('/').some((segment) => {
1026
+ const lowered = segment.toLowerCase()
1027
+ return SINGLE_DOT_SEGMENTS.has(lowered) || DOUBLE_DOT_SEGMENTS.has(lowered)
1028
+ })
1029
+ }
1030
+
982
1031
  function isPathParameterSegment(segment: string): boolean {
983
1032
  return (
984
1033
  (segment.startsWith('{') && segment.endsWith('}')) ||
@@ -95,6 +95,38 @@ export async function compileAndImportGenerated(tsPath: string): Promise<Record<
95
95
  return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>
96
96
  }
97
97
 
98
+ const UNSAFE_JS_STRING_CHAR_ESCAPES: Record<number, string> = {
99
+ 0x3c: '\\u003C', // < — HTML/script-tag breakout
100
+ 0x3e: '\\u003E', // > — HTML/script-tag breakout
101
+ 0x2028: '\\u2028', // line separator — string content but a statement terminator pre-ES2019
102
+ 0x2029: '\\u2029', // paragraph separator — same
103
+ }
104
+
105
+ /**
106
+ * Escape characters that `JSON.stringify` leaves intact but which can still
107
+ * break out of (or alter the meaning of) the JavaScript string literal that the
108
+ * stringified value is embedded into — notably `<`/`>` (HTML/script-tag
109
+ * breakout) and the U+2028 / U+2029 line separators (valid string content but
110
+ * statement terminators in pre-ES2019 parsers). Apply this on top of
111
+ * `JSON.stringify` so the emitted import source stays well-formed regardless of
112
+ * the resolved path. Exported for unit testing.
113
+ */
114
+ export function escapeUnsafeJsStringChars(value: string): string {
115
+ return value.replace(
116
+ /[<>\u2028\u2029]/g,
117
+ (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)],
118
+ )
119
+ }
120
+
121
+ /**
122
+ * Stringify a resolved path into a JavaScript string literal that is safe to
123
+ * embed in generated source: `JSON.stringify` handles quoting/standard escapes,
124
+ * and `escapeUnsafeJsStringChars` neutralizes the characters it leaves intact.
125
+ */
126
+ function toSafeJsStringLiteral(value: string): string {
127
+ return escapeUnsafeJsStringChars(JSON.stringify(value))
128
+ }
129
+
98
130
  /**
99
131
  * Rewrite `@/...` path-alias imports (both `from "@/x"` and dynamic
100
132
  * `import("@/x")`) in generated-registry source to absolute `file://` URLs
@@ -110,14 +142,14 @@ export function rewriteGeneratedAliasImports(source: string, appRoot: string): s
110
142
  : fs.existsSync(target + '.ts')
111
143
  ? target + '.ts'
112
144
  : target
113
- return pathToFileURL(candidate).href
145
+ return toSafeJsStringLiteral(pathToFileURL(candidate).href)
114
146
  }
115
147
  return source
116
148
  .replace(/from\s+["']@\/([^"']+)["']/g, (_match, relativePath: string) => {
117
- return `from ${JSON.stringify(resolveAlias(relativePath))}`
149
+ return `from ${resolveAlias(relativePath)}`
118
150
  })
119
151
  .replace(/import\s*\(\s*["']@\/([^"']+)["']\s*\)/g, (_match, relativePath: string) => {
120
- return `import(${JSON.stringify(resolveAlias(relativePath))})`
152
+ return `import(${resolveAlias(relativePath)})`
121
153
  })
122
154
  }
123
155