@open-mercato/core 0.6.1-develop.3227.1.2b7b8ab258 → 0.6.1-develop.3256.1.fe3dec2464

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/catalog/ai-agents.js +4 -0
  3. package/dist/modules/catalog/ai-agents.js.map +2 -2
  4. package/dist/modules/customers/ai-agents.js.map +2 -2
  5. package/dist/modules/customers/api/companies/route.js +11 -1
  6. package/dist/modules/customers/api/companies/route.js.map +2 -2
  7. package/dist/modules/customers/api/people/route.js +11 -1
  8. package/dist/modules/customers/api/people/route.js.map +2 -2
  9. package/dist/modules/customers/backend/customers/companies/page.js +78 -35
  10. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  11. package/dist/modules/customers/backend/customers/deals/page.js +74 -30
  12. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  13. package/dist/modules/customers/backend/customers/people/page.js +78 -35
  14. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  15. package/dist/modules/customers/commands/people.js +20 -0
  16. package/dist/modules/customers/commands/people.js.map +2 -2
  17. package/dist/modules/entities/components/EncryptionManager.js +40 -17
  18. package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
  19. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  20. package/package.json +5 -5
  21. package/src/modules/catalog/ai-agents.ts +8 -49
  22. package/src/modules/customers/ai-agents.ts +4 -51
  23. package/src/modules/customers/api/companies/route.ts +10 -0
  24. package/src/modules/customers/api/people/route.ts +10 -0
  25. package/src/modules/customers/backend/customers/companies/page.tsx +95 -35
  26. package/src/modules/customers/backend/customers/deals/page.tsx +91 -30
  27. package/src/modules/customers/backend/customers/people/page.tsx +96 -35
  28. package/src/modules/customers/commands/people.ts +30 -0
  29. package/src/modules/customers/i18n/de.json +8 -0
  30. package/src/modules/customers/i18n/en.json +8 -0
  31. package/src/modules/customers/i18n/es.json +8 -0
  32. package/src/modules/customers/i18n/pl.json +8 -0
  33. package/src/modules/entities/components/EncryptionManager.tsx +50 -21
  34. package/src/modules/inbox_ops/lib/llmProvider.ts +10 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/components/EncryptionManager.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Plus, Save, Trash2 } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getEntityFields } from '#generated/entity-fields-registry'\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\n\ntype EncryptionFieldRow = {\n id: string\n field: string\n hashField?: string | null\n}\n\ntype EncryptionMapResponse = {\n entityId: string\n fields?: Array<{ field: string; hashField?: string | null }>\n isActive?: boolean\n}\n\ntype CanonicalOption = { value: string; label?: string }\n\nfunction normalizeToken(value: string): string {\n return value\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^a-z0-9]/gi, '')\n .toLowerCase()\n}\n\nfunction canonicalizeFieldName(raw: string, options: CanonicalOption[]): string {\n const trimmed = typeof raw === 'string' ? raw.trim() : ''\n if (!trimmed) return ''\n const normalized = normalizeToken(trimmed)\n for (const option of options) {\n const optionValue = typeof option.value === 'string' ? option.value.trim() : ''\n if (optionValue && normalizeToken(optionValue) === normalized) return optionValue\n if (option.label && normalizeToken(option.label) === normalized) return optionValue\n }\n return trimmed\n}\n\nfunction normalizeFieldRows(raw: EncryptionMapResponse | undefined, options: CanonicalOption[]): EncryptionFieldRow[] {\n const rows = Array.isArray(raw?.fields) ? raw!.fields : []\n return rows.map((entry, idx) => ({\n id: `${entry.field}-${idx}`,\n field: canonicalizeFieldName(entry.field, options),\n hashField: entry.hashField ? canonicalizeFieldName(entry.hashField, options) : null,\n }))\n}\n\nfunction buildRowId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()\n return `row-${Math.random().toString(36).slice(2)}`\n}\n\nexport function EncryptionManager() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const [selectedEntityId, setSelectedEntityId] = React.useState('')\n const [fields, setFields] = React.useState<EncryptionFieldRow[]>([])\n const [isActive, setIsActive] = React.useState(true)\n const [baseFieldOptions, setBaseFieldOptions] = React.useState<Array<{ value: string; label: string }>>([])\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n const { data: fieldDefs = [], isLoading: loadingFieldDefs } = useCustomFieldDefs(selectedEntityId ? [selectedEntityId] : [], {\n enabled: !!selectedEntityId,\n })\n const canonicalOptions = React.useMemo<CanonicalOption[]>(() => {\n const options: CanonicalOption[] = []\n for (const option of baseFieldOptions) {\n if (!option.value) continue\n if (options.some((opt) => opt.value === option.value)) continue\n options.push(option)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || options.some((opt) => opt.value === key)) continue\n const label = typeof def.label === 'string' ? def.label : undefined\n options.push({ value: key, label })\n }\n return options\n }, [baseFieldOptions, fieldDefs])\n const fieldOptions = React.useMemo(() => {\n const entries = new Map<string, string>()\n for (const option of baseFieldOptions) {\n if (!option.value || entries.has(option.value)) continue\n entries.set(option.value, option.label || option.value)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || entries.has(key)) continue\n const label = typeof def.label === 'string' && def.label.trim().length ? def.label : key\n entries.set(key, label)\n }\n for (const row of fields) {\n const main = row.field?.trim()\n if (main && !entries.has(main)) entries.set(main, main)\n const hash = row.hashField?.trim()\n if (hash && !entries.has(hash)) entries.set(hash, hash)\n }\n return Array.from(entries.entries()).map(([value, label]) => ({ value, label }))\n }, [baseFieldOptions, fieldDefs, fields])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('entities.encryption.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n React.useEffect(() => {\n if (!selectedEntityId && entities?.items?.length) {\n const first = entities.items[0]\n setSelectedEntityId(first.entityId)\n }\n }, [entities, selectedEntityId])\n\n React.useEffect(() => {\n if (!selectedEntityId) {\n setBaseFieldOptions([])\n return\n }\n const parts = selectedEntityId.split(':')\n const entitySlug = parts[1]\n if (!entitySlug) {\n setBaseFieldOptions([])\n return\n }\n\n // Use static registry instead of dynamic import for Turbopack compatibility\n const mod = getEntityFields(entitySlug)\n if (!mod) {\n console.warn('[encryption] No fields found for entity', entitySlug)\n setBaseFieldOptions([])\n return\n }\n\n const options: Array<{ value: string; label: string }> = []\n for (const raw of Object.values(mod)) {\n if (typeof raw !== 'string' || !raw.trim()) continue\n const value = raw.trim()\n if (options.some((opt) => opt.value === value)) continue\n const label = value\n .split('_')\n .map((segment) => (segment ? `${segment[0].toUpperCase()}${segment.slice(1)}` : ''))\n .join(' ')\n .trim() || value\n options.push({ value, label })\n }\n setBaseFieldOptions(options)\n }, [selectedEntityId])\n\n const {\n data: map,\n isLoading: loadingMap,\n isError: mapError,\n refetch: refetchMap,\n } = useQuery<EncryptionMapResponse>({\n queryKey: ['encryption-map', selectedEntityId, scopeVersion],\n enabled: !!selectedEntityId,\n queryFn: async () =>\n readApiResultOrThrow(`/api/entities/encryption?entityId=${encodeURIComponent(selectedEntityId)}`, undefined, {\n errorMessage: t('entities.encryption.errors.loadMap', 'Failed to load encryption map'),\n }),\n })\n\n const mapSignature = React.useMemo(() => JSON.stringify(map ?? null), [map])\n const lastMapSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const signature = mapSignature\n const isSameMap = signature === lastMapSignatureRef.current\n if (isSameMap && hasUserEdited) return\n if (!map) {\n setFields([])\n setIsActive(true)\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n return\n }\n setFields(normalizeFieldRows(map, canonicalOptions))\n setIsActive(map?.isActive !== false)\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n }, [mapSignature, map, canonicalOptions, hasUserEdited])\n\n const mutation = useMutation({\n mutationFn: async () => {\n const trimmed = fields\n .map((row) => ({\n field: canonicalizeFieldName(row.field, canonicalOptions).trim(),\n hashField: row.hashField ? canonicalizeFieldName(row.hashField, canonicalOptions).trim() : '',\n }))\n .filter((row) => row.field.length > 0)\n .map((row) => ({\n field: row.field,\n hashField: row.hashField ? row.hashField : null,\n }))\n if (!selectedEntityId) {\n throw new Error(t('entities.encryption.errors.selectEntity', 'Select an entity before saving'))\n }\n if (!trimmed.length) {\n throw new Error(t('entities.encryption.errors.noFields', 'Add at least one field to encrypt'))\n }\n const res = await apiCall('/api/entities/encryption', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ entityId: selectedEntityId, fields: trimmed, isActive }),\n })\n if (!res.ok) {\n await raiseCrudError(res.response, t('entities.encryption.errors.save', 'Failed to save encryption map'))\n }\n return true\n },\n onSuccess: () => {\n flash(t('entities.encryption.flash.saved', 'Encryption map saved'), 'success')\n void refetchMap()\n },\n onError: (err: any) => {\n const message = err?.message || t('entities.encryption.errors.save', 'Failed to save encryption map')\n flash(message, 'error')\n },\n })\n\n const addField = () => {\n setHasUserEdited(true)\n setFields((prev) => [...prev, { id: buildRowId(), field: '', hashField: null }])\n }\n\n const updateField = (id: string, patch: Partial<EncryptionFieldRow>) => {\n setHasUserEdited(true)\n setFields((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row)))\n }\n\n const removeField = (id: string) => {\n setHasUserEdited(true)\n setFields((prev) => prev.filter((row) => row.id !== id))\n }\n\n const renderFields = () => {\n if (loadingMap || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('entities.encryption.loading', 'Loading encryption map\u2026')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (mapError) {\n return (\n <ErrorMessage\n label={t('entities.encryption.errors.loadMap', 'Failed to load encryption map')}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => void refetchMap()}>\n {t('entities.encryption.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fields.length) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('entities.encryption.empty', 'No fields are encrypted yet. Add the first one below.')}\n </div>\n )\n }\n const withFallbackOption = (value?: string | null) => {\n if (value && !fieldOptions.some((opt) => opt.value === value)) {\n return [...fieldOptions, { value, label: value }]\n }\n return fieldOptions\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[720px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.field', 'Field name')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.hash', 'Hash field (optional)')}\n </th>\n <th className=\"px-3 py-2 text-right\">\n {t('entities.encryption.fields.actions', 'Actions')}\n </th>\n </tr>\n </thead>\n <tbody>\n {fields.map((row) => {\n const fieldOpts = withFallbackOption(row.field)\n const hashOpts = withFallbackOption(row.hashField)\n return (\n <tr key={row.id} className=\"border-t\">\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.field || undefined}\n onValueChange={(value) => updateField(row.id, { field: value ?? '' })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectField', 'Select field')} />\n </SelectTrigger>\n <SelectContent>\n {fieldOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.hashField || undefined}\n onValueChange={(value) => updateField(row.id, { hashField: value || null })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectHash', 'Select hash field (optional)')} />\n </SelectTrigger>\n <SelectContent>\n {hashOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"mt-1 text-overline text-muted-foreground\">\n {t('entities.encryption.fields.hashHint', 'Use when lookups must stay deterministic (e.g., login by email).')}\n </p>\n </td>\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-9 w-9 px-0\"\n aria-label={t('entities.encryption.actions.remove', 'Remove')}\n onClick={() => removeField(row.id)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('entities.encryption.title', 'Encryption')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.encryption.description', 'Manage which entity fields are encrypted with tenant keys and optional hash columns.')}\n </p>\n </div>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.selectEntity', 'Choose entity')}\n </label>\n <Select\n value={selectedEntityId || undefined}\n onValueChange={(value) => setSelectedEntityId(value ?? '')}\n disabled={loadingEntities || !!entitiesError}\n >\n <SelectTrigger className=\"mt-1\">\n <SelectValue placeholder={t('entities.encryption.placeholder', 'Select an entity')} />\n </SelectTrigger>\n <SelectContent>\n {(entities?.items || []).map((item) => (\n <SelectItem key={item.entityId} value={item.entityId}>\n {item.label || item.entityId} {item.source === 'custom' ? `(${t('entities.encryption.source.custom', 'custom')})` : ''}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {entitiesError ? (\n <p className=\"mt-1 text-xs text-red-600\">\n {t('entities.encryption.errors.loadEntities', 'Failed to load entities')}\n </p>\n ) : (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('entities.encryption.entityHint', 'Maps apply per tenant/organization. Use field names from your entities.')}\n </p>\n )}\n </div>\n <label className=\"inline-flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={isActive}\n onChange={(event) => setIsActive(event.target.checked)}\n />\n {t('entities.encryption.active', 'Encryption enabled for this entity')}\n </label>\n </div>\n </div>\n <div className=\"rounded-lg border bg-background/80 p-4\">\n <div className=\"mb-3 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('entities.encryption.fields.title', 'Encrypted fields')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.fields.subtitle', 'List the attributes that should be encrypted with the tenant key.')}\n </p>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={addField}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('entities.encryption.actions.add', 'Add field')}\n </Button>\n </div>\n {renderFields()}\n </div>\n <div className=\"flex justify-end\">\n <Button\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !selectedEntityId}\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('entities.encryption.actions.saving', 'Saving\u2026')\n : t('entities.encryption.actions.save', 'Save encryption map')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useCustomFieldDefs, type CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Plus, Save, Trash2 } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getEntityFields } from '#generated/entity-fields-registry'\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\ntype FieldOption = { value: string; label: string }\n\ntype EncryptionFieldRow = {\n id: string\n field: string\n hashField?: string | null\n}\n\ntype EncryptionMapResponse = {\n entityId: string\n fields?: Array<{ field: string; hashField?: string | null }>\n isActive?: boolean\n}\n\ntype CanonicalOption = { value: string; label?: string }\n\nconst EMPTY_FIELD_DEFS: CustomFieldDefDto[] = []\n\nfunction normalizeToken(value: string): string {\n return value\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^a-z0-9]/gi, '')\n .toLowerCase()\n}\n\nfunction canonicalizeFieldName(raw: string, options: CanonicalOption[]): string {\n const trimmed = typeof raw === 'string' ? raw.trim() : ''\n if (!trimmed) return ''\n const normalized = normalizeToken(trimmed)\n for (const option of options) {\n const optionValue = typeof option.value === 'string' ? option.value.trim() : ''\n if (optionValue && normalizeToken(optionValue) === normalized) return optionValue\n if (option.label && normalizeToken(option.label) === normalized) return optionValue\n }\n return trimmed\n}\n\nfunction normalizeFieldRows(raw: EncryptionMapResponse | undefined, options: CanonicalOption[]): EncryptionFieldRow[] {\n const rows = Array.isArray(raw?.fields) ? raw!.fields : []\n return rows.map((entry, idx) => ({\n id: `${entry.field}-${idx}`,\n field: canonicalizeFieldName(entry.field, options),\n hashField: entry.hashField ? canonicalizeFieldName(entry.hashField, options) : null,\n }))\n}\n\nfunction areFieldRowsEqual(a: EncryptionFieldRow[], b: EncryptionFieldRow[]): boolean {\n if (a === b) return true\n if (a.length !== b.length) return false\n return a.every((row, idx) => {\n const other = b[idx]\n return !!other && row.id === other.id && row.field === other.field && (row.hashField ?? null) === (other.hashField ?? null)\n })\n}\n\nfunction areFieldOptionsEqual(a: FieldOption[], b: FieldOption[]): boolean {\n if (a === b) return true\n if (a.length !== b.length) return false\n return a.every((option, idx) => {\n const other = b[idx]\n return !!other && option.value === other.value && option.label === other.label\n })\n}\n\nfunction buildRowId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()\n return `row-${Math.random().toString(36).slice(2)}`\n}\n\nexport function EncryptionManager() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const [selectedEntityId, setSelectedEntityId] = React.useState('')\n const [fields, setFields] = React.useState<EncryptionFieldRow[]>([])\n const [isActive, setIsActive] = React.useState(true)\n const [baseFieldOptions, setBaseFieldOptions] = React.useState<FieldOption[]>([])\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n const { data: rawFieldDefs, isLoading: loadingFieldDefs } = useCustomFieldDefs(selectedEntityId ? [selectedEntityId] : [], {\n enabled: !!selectedEntityId,\n })\n const fieldDefs = rawFieldDefs ?? EMPTY_FIELD_DEFS\n const canonicalOptions = React.useMemo<CanonicalOption[]>(() => {\n const options: CanonicalOption[] = []\n for (const option of baseFieldOptions) {\n if (!option.value) continue\n if (options.some((opt) => opt.value === option.value)) continue\n options.push(option)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || options.some((opt) => opt.value === key)) continue\n const label = typeof def.label === 'string' ? def.label : undefined\n options.push({ value: key, label })\n }\n return options\n }, [baseFieldOptions, fieldDefs])\n const fieldOptions = React.useMemo(() => {\n const entries = new Map<string, string>()\n for (const option of baseFieldOptions) {\n if (!option.value || entries.has(option.value)) continue\n entries.set(option.value, option.label || option.value)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || entries.has(key)) continue\n const label = typeof def.label === 'string' && def.label.trim().length ? def.label : key\n entries.set(key, label)\n }\n for (const row of fields) {\n const main = row.field?.trim()\n if (main && !entries.has(main)) entries.set(main, main)\n const hash = row.hashField?.trim()\n if (hash && !entries.has(hash)) entries.set(hash, hash)\n }\n return Array.from(entries.entries()).map(([value, label]) => ({ value, label }))\n }, [baseFieldOptions, fieldDefs, fields])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('entities.encryption.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n React.useEffect(() => {\n if (!selectedEntityId && entities?.items?.length) {\n const first = entities.items[0]\n setSelectedEntityId(first.entityId)\n }\n }, [entities, selectedEntityId])\n\n React.useEffect(() => {\n if (!selectedEntityId) {\n setBaseFieldOptions((prev) => (prev.length ? [] : prev))\n return\n }\n const parts = selectedEntityId.split(':')\n const entitySlug = parts[1]\n if (!entitySlug) {\n setBaseFieldOptions((prev) => (prev.length ? [] : prev))\n return\n }\n\n // Use static registry instead of dynamic import for Turbopack compatibility\n const mod = getEntityFields(entitySlug)\n if (!mod) {\n console.warn('[encryption] No fields found for entity', entitySlug)\n setBaseFieldOptions((prev) => (prev.length ? [] : prev))\n return\n }\n\n const options: FieldOption[] = []\n for (const raw of Object.values(mod)) {\n if (typeof raw !== 'string' || !raw.trim()) continue\n const value = raw.trim()\n if (options.some((opt) => opt.value === value)) continue\n const label = value\n .split('_')\n .map((segment) => (segment ? `${segment[0].toUpperCase()}${segment.slice(1)}` : ''))\n .join(' ')\n .trim() || value\n options.push({ value, label })\n }\n setBaseFieldOptions((prev) => (areFieldOptionsEqual(prev, options) ? prev : options))\n }, [selectedEntityId])\n\n const {\n data: map,\n isLoading: loadingMap,\n isError: mapError,\n refetch: refetchMap,\n } = useQuery<EncryptionMapResponse>({\n queryKey: ['encryption-map', selectedEntityId, scopeVersion],\n enabled: !!selectedEntityId,\n queryFn: async () =>\n readApiResultOrThrow(`/api/entities/encryption?entityId=${encodeURIComponent(selectedEntityId)}`, undefined, {\n errorMessage: t('entities.encryption.errors.loadMap', 'Failed to load encryption map'),\n }),\n })\n\n const mapSignature = React.useMemo(() => JSON.stringify(map ?? null), [map])\n const lastMapSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const signature = mapSignature\n const isSameMap = signature === lastMapSignatureRef.current\n if (isSameMap && hasUserEdited) return\n if (!map) {\n setFields((prev) => (prev.length ? [] : prev))\n setIsActive((prev) => (prev === true ? prev : true))\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n return\n }\n const nextFields = normalizeFieldRows(map, canonicalOptions)\n setFields((prev) => (areFieldRowsEqual(prev, nextFields) ? prev : nextFields))\n const nextIsActive = map?.isActive !== false\n setIsActive((prev) => (prev === nextIsActive ? prev : nextIsActive))\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n }, [mapSignature, map, canonicalOptions, hasUserEdited])\n\n const mutation = useMutation({\n mutationFn: async () => {\n const trimmed = fields\n .map((row) => ({\n field: canonicalizeFieldName(row.field, canonicalOptions).trim(),\n hashField: row.hashField ? canonicalizeFieldName(row.hashField, canonicalOptions).trim() : '',\n }))\n .filter((row) => row.field.length > 0)\n .map((row) => ({\n field: row.field,\n hashField: row.hashField ? row.hashField : null,\n }))\n if (!selectedEntityId) {\n throw new Error(t('entities.encryption.errors.selectEntity', 'Select an entity before saving'))\n }\n if (!trimmed.length) {\n throw new Error(t('entities.encryption.errors.noFields', 'Add at least one field to encrypt'))\n }\n const res = await apiCall('/api/entities/encryption', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ entityId: selectedEntityId, fields: trimmed, isActive }),\n })\n if (!res.ok) {\n await raiseCrudError(res.response, t('entities.encryption.errors.save', 'Failed to save encryption map'))\n }\n return true\n },\n onSuccess: () => {\n flash(t('entities.encryption.flash.saved', 'Encryption map saved'), 'success')\n void refetchMap()\n },\n onError: (err: any) => {\n const message = err?.message || t('entities.encryption.errors.save', 'Failed to save encryption map')\n flash(message, 'error')\n },\n })\n\n const addField = () => {\n setHasUserEdited(true)\n setFields((prev) => [...prev, { id: buildRowId(), field: '', hashField: null }])\n }\n\n const updateField = (id: string, patch: Partial<EncryptionFieldRow>) => {\n setHasUserEdited(true)\n setFields((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row)))\n }\n\n const removeField = (id: string) => {\n setHasUserEdited(true)\n setFields((prev) => prev.filter((row) => row.id !== id))\n }\n\n const renderFields = () => {\n if (loadingMap || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('entities.encryption.loading', 'Loading encryption map\u2026')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (mapError) {\n return (\n <ErrorMessage\n label={t('entities.encryption.errors.loadMap', 'Failed to load encryption map')}\n action={(\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void refetchMap()}>\n {t('entities.encryption.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fields.length) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('entities.encryption.empty', 'No fields are encrypted yet. Add the first one below.')}\n </div>\n )\n }\n const withFallbackOption = (value?: string | null) => {\n if (value && !fieldOptions.some((opt) => opt.value === value)) {\n return [...fieldOptions, { value, label: value }]\n }\n return fieldOptions\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[720px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.field', 'Field name')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.hash', 'Hash field (optional)')}\n </th>\n <th className=\"px-3 py-2 text-right\">\n {t('entities.encryption.fields.actions', 'Actions')}\n </th>\n </tr>\n </thead>\n <tbody>\n {fields.map((row) => {\n const fieldOpts = withFallbackOption(row.field)\n const hashOpts = withFallbackOption(row.hashField)\n return (\n <tr key={row.id} className=\"border-t\">\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.field || undefined}\n onValueChange={(value) => updateField(row.id, { field: value ?? '' })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectField', 'Select field')} />\n </SelectTrigger>\n <SelectContent>\n {fieldOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.hashField || undefined}\n onValueChange={(value) => updateField(row.id, { hashField: value || null })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectHash', 'Select hash field (optional)')} />\n </SelectTrigger>\n <SelectContent>\n {hashOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"mt-1 text-overline text-muted-foreground\">\n {t('entities.encryption.fields.hashHint', 'Use when lookups must stay deterministic (e.g., login by email).')}\n </p>\n </td>\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-9 w-9 px-0\"\n aria-label={t('entities.encryption.actions.remove', 'Remove')}\n onClick={() => removeField(row.id)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('entities.encryption.title', 'Encryption')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.encryption.description', 'Manage which entity fields are encrypted with tenant keys and optional hash columns.')}\n </p>\n </div>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.selectEntity', 'Choose entity')}\n </label>\n <Select\n value={selectedEntityId || undefined}\n onValueChange={(value) => setSelectedEntityId(value ?? '')}\n disabled={loadingEntities || !!entitiesError}\n >\n <SelectTrigger className=\"mt-1\">\n <SelectValue placeholder={t('entities.encryption.placeholder', 'Select an entity')} />\n </SelectTrigger>\n <SelectContent>\n {(entities?.items || []).map((item) => (\n <SelectItem key={item.entityId} value={item.entityId}>\n {item.label || item.entityId} {item.source === 'custom' ? `(${t('entities.encryption.source.custom', 'custom')})` : ''}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {entitiesError ? (\n <p className=\"mt-1 text-xs text-status-error-text\">\n {t('entities.encryption.errors.loadEntities', 'Failed to load entities')}\n </p>\n ) : (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('entities.encryption.entityHint', 'Maps apply per tenant/organization. Use field names from your entities.')}\n </p>\n )}\n </div>\n <div className=\"inline-flex items-center gap-2 text-sm\">\n <Checkbox\n id=\"entities-encryption-active\"\n checked={isActive}\n onCheckedChange={(checked) => setIsActive(checked === true)}\n />\n <label htmlFor=\"entities-encryption-active\">\n {t('entities.encryption.active', 'Encryption enabled for this entity')}\n </label>\n </div>\n </div>\n </div>\n <div className=\"rounded-lg border bg-background/80 p-4\">\n <div className=\"mb-3 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('entities.encryption.fields.title', 'Encrypted fields')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.fields.subtitle', 'List the attributes that should be encrypted with the tenant key.')}\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={addField}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('entities.encryption.actions.add', 'Add field')}\n </Button>\n </div>\n {renderFields()}\n </div>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !selectedEntityId}\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('entities.encryption.actions.saving', 'Saving\u2026')\n : t('entities.encryption.actions.save', 'Save encryption map')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/inbox_ops/lib/llmProvider.ts"],
4
- "sourcesContent": ["import { generateObject } from 'ai'\nimport type { AwilixContainer } from 'awilix'\nimport { createContainer } from 'awilix'\nimport {\n resolveAiProviderIdFromEnv,\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n resolveOpenCodeProviderId,\n type OpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n AiModelFactoryError,\n createModelFactory,\n type AiModelFactory,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory'\nimport { extractionOutputSchema } from '../data/validators'\n\n// Vercel AI SDK provider factories return LanguageModelV1 but generateObject()\n// expects a narrower LanguageModel union. The types are structurally compatible\n// at runtime; the cast is required until the AI SDK unifies its model types.\ntype AiModel = Parameters<typeof generateObject>[0]['model']\nfunction asAiModel(model: unknown): AiModel {\n return model as AiModel\n}\n\n/**\n * Step 5.1 \u2014 thin backward-compatibility shim. The public surface of this\n * module (`resolveExtractionProviderId`, `createStructuredModel`,\n * `withTimeout`, `runExtractionWithConfiguredProvider`) is unchanged so\n * `ai-tools.ts`, `translationProvider.ts`, and `extractionWorker.ts` continue\n * to compile and pass their existing tests.\n *\n * The model-instantiation path inside {@link runExtractionWithConfiguredProvider}\n * now delegates to the shared {@link createModelFactory} so every\n * AI-runtime caller shares one resolution order. The legacy\n * `OPENCODE_MODEL` / `OPENCODE_PROVIDER` envs remain honored via\n * {@link resolveExtractionProviderId} and {@link resolveOpenCodeModel} so\n * inbox_ops deployments do not see a behavior change \u2014 the factory is\n * consulted first (honoring `OM_AI_INBOX_OPS_MODEL` \u2014 legacy `INBOX_OPS_AI_MODEL` \u2014 + `input.modelOverride`),\n * with the legacy path as the fallback when no registry provider is\n * configured (preserving the historical error messages).\n */\n\nexport function resolveExtractionProviderId(): OpenCodeProviderId {\n // Honors OM_AI_PROVIDER first, then the legacy OPENCODE_PROVIDER, then the\n // first configured provider from `OPEN_CODE_PROVIDER_IDS`, then the unified\n // default (currently `openai`). Mirrors the precedence applied by the\n // shared model factory so the BC fallback path stays consistent.\n const explicit = (process.env.OM_AI_PROVIDER ?? process.env.OPENCODE_PROVIDER ?? '').trim()\n if (explicit.length > 0) {\n return resolveAiProviderIdFromEnv(process.env)\n }\n\n const firstConfiguredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (firstConfiguredProvider) {\n return firstConfiguredProvider\n }\n\n return resolveOpenCodeProviderId(undefined)\n}\n\nexport async function createStructuredModel(\n providerId: OpenCodeProviderId,\n apiKey: string,\n modelId: string,\n): Promise<AiModel> {\n switch (providerId) {\n case 'anthropic': {\n const { createAnthropic } = await import('@ai-sdk/anthropic')\n return asAiModel(createAnthropic({ apiKey })(modelId))\n }\n case 'openai': {\n const { createOpenAI } = await import('@ai-sdk/openai')\n return asAiModel(createOpenAI({ apiKey })(modelId))\n }\n case 'google': {\n const { createGoogleGenerativeAI } = await import('@ai-sdk/google')\n return asAiModel(createGoogleGenerativeAI({ apiKey })(modelId))\n }\n default:\n throw new Error(`Unsupported provider: ${providerId}`)\n }\n}\n\nexport async function withTimeout<T>(\n operation: Promise<T>,\n timeoutMs: number,\n timeoutMessage: string,\n): Promise<T> {\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)\n })\n\n try {\n return await Promise.race([operation, timeoutPromise])\n } finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n }\n}\n\n/**\n * Test-only seam for the factory-delegation regression suite. Production\n * callers MUST use {@link runExtractionWithConfiguredProvider} directly; the\n * suite overrides this binding via `jest.spyOn` to assert the shim actually\n * reaches `createModelFactory` without stubbing `@open-mercato/ai-assistant`.\n */\nexport const __inboxOpsLlmProviderInternal = {\n createModelFactory,\n createContainer,\n}\n\nfunction tryFactoryResolution(input: {\n modelOverride?: string | null\n}): { modelId: string; providerId: OpenCodeProviderId; model: AiModel } | null {\n let factory: AiModelFactory\n try {\n const container = __inboxOpsLlmProviderInternal.createContainer()\n factory = __inboxOpsLlmProviderInternal.createModelFactory(container as AwilixContainer)\n } catch {\n return null\n }\n try {\n const resolution = factory.resolveModel({\n moduleId: 'inbox_ops',\n callerOverride: input.modelOverride ?? undefined,\n })\n const providerId = resolveOpenCodeProviderId(resolution.providerId)\n return {\n modelId: resolution.modelId,\n providerId,\n model: asAiModel(resolution.model),\n }\n } catch (err) {\n if (err instanceof AiModelFactoryError) {\n // Fall back to the legacy path so the shim keeps throwing the original\n // OPENCODE_*-era error messages existing tests/consumers rely on.\n return null\n }\n throw err\n }\n}\n\nexport async function runExtractionWithConfiguredProvider(input: {\n systemPrompt: string\n userPrompt: string\n modelOverride?: string | null\n timeoutMs: number\n}): Promise<{\n object: ReturnType<typeof extractionOutputSchema.parse>\n totalTokens: number\n modelWithProvider: string\n}> {\n const factoryResolution = tryFactoryResolution({ modelOverride: input.modelOverride })\n\n let model: AiModel\n let modelWithProvider: string\n if (factoryResolution) {\n model = factoryResolution.model\n modelWithProvider = `${factoryResolution.providerId}/${factoryResolution.modelId}`\n } else {\n const providerId = resolveExtractionProviderId()\n const apiKey = requireOpenCodeProviderApiKey(providerId)\n const modelConfig = resolveOpenCodeModel(providerId, {\n overrideModel: input.modelOverride,\n })\n model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n modelWithProvider = modelConfig.modelWithProvider\n }\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: extractionOutputSchema,\n system: input.systemPrompt,\n prompt: input.userPrompt,\n temperature: 0,\n }),\n input.timeoutMs,\n `LLM extraction timed out after ${input.timeoutMs}ms`,\n )\n\n return {\n object: result.object,\n totalTokens: Number(result.usage?.totalTokens ?? 0) || 0,\n modelWithProvider,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { generateObject } from 'ai'\nimport type { AwilixContainer } from 'awilix'\nimport { createContainer } from 'awilix'\nimport {\n resolveAiProviderIdFromEnv,\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n resolveOpenCodeProviderId,\n type OpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n AiModelFactoryError,\n createModelFactory,\n type AiModelFactory,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory'\nimport { extractionOutputSchema } from '../data/validators'\n\n// Vercel AI SDK provider factories return LanguageModelV1 but generateObject()\n// expects a narrower LanguageModel union. The types are structurally compatible\n// at runtime; the cast is required until the AI SDK unifies its model types.\ntype AiModel = Parameters<typeof generateObject>[0]['model']\nfunction asAiModel(model: unknown): AiModel {\n return model as AiModel\n}\n\n/**\n * Step 5.1 \u2014 thin backward-compatibility shim. The public surface of this\n * module (`resolveExtractionProviderId`, `createStructuredModel`,\n * `withTimeout`, `runExtractionWithConfiguredProvider`) is unchanged so\n * `ai-tools.ts`, `translationProvider.ts`, and `extractionWorker.ts` continue\n * to compile and pass their existing tests.\n *\n * The model-instantiation path inside {@link runExtractionWithConfiguredProvider}\n * now delegates to the shared {@link createModelFactory} so every\n * AI-runtime caller shares one resolution order. The legacy\n * `OPENCODE_MODEL` / `OPENCODE_PROVIDER` envs remain honored via\n * {@link resolveExtractionProviderId} and {@link resolveOpenCodeModel} so\n * inbox_ops deployments do not see a behavior change \u2014 the factory is\n * consulted first (honoring `OM_AI_INBOX_OPS_MODEL` \u2014 legacy `INBOX_OPS_AI_MODEL` \u2014 + `input.modelOverride`),\n * with the legacy path as the fallback when no registry provider is\n * configured (preserving the historical error messages).\n */\n\nexport function resolveExtractionProviderId(): OpenCodeProviderId {\n // Honors OM_AI_PROVIDER first, then the legacy OPENCODE_PROVIDER, then the\n // first configured provider from `OPEN_CODE_PROVIDER_IDS`, then the unified\n // default (currently `openai`). Mirrors the precedence applied by the\n // shared model factory so the BC fallback path stays consistent.\n const explicit = (process.env.OM_AI_PROVIDER ?? process.env.OPENCODE_PROVIDER ?? '').trim()\n if (explicit.length > 0) {\n return resolveAiProviderIdFromEnv(process.env)\n }\n\n const firstConfiguredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (firstConfiguredProvider) {\n return firstConfiguredProvider\n }\n\n return resolveOpenCodeProviderId(undefined)\n}\n\nexport async function createStructuredModel(\n providerId: OpenCodeProviderId,\n apiKey: string,\n modelId: string,\n): Promise<AiModel> {\n switch (providerId) {\n case 'anthropic': {\n const { createAnthropic } = await import('@ai-sdk/anthropic')\n return asAiModel(createAnthropic({ apiKey })(modelId))\n }\n case 'openai': {\n const { createOpenAI } = await import('@ai-sdk/openai')\n return asAiModel(createOpenAI({ apiKey })(modelId))\n }\n case 'google': {\n const { createGoogleGenerativeAI } = await import('@ai-sdk/google')\n return asAiModel(createGoogleGenerativeAI({ apiKey })(modelId))\n }\n default:\n throw new Error(`Unsupported provider: ${providerId}`)\n }\n}\n\nexport async function withTimeout<T>(\n operation: Promise<T>,\n timeoutMs: number,\n timeoutMessage: string,\n): Promise<T> {\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)\n })\n\n try {\n return await Promise.race([operation, timeoutPromise])\n } finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n }\n}\n\n/**\n * Test-only seam for the factory-delegation regression suite. Production\n * callers MUST use {@link runExtractionWithConfiguredProvider} directly; the\n * suite overrides this binding via `jest.spyOn` to assert the shim actually\n * reaches `createModelFactory` without stubbing `@open-mercato/ai-assistant`.\n */\nexport const __inboxOpsLlmProviderInternal = {\n createModelFactory,\n createContainer,\n}\n\nfunction tryFactoryResolution(input: {\n modelOverride?: string | null\n}): { modelId: string; providerId: OpenCodeProviderId; model: AiModel } | null {\n let factory: AiModelFactory\n try {\n const container = __inboxOpsLlmProviderInternal.createContainer()\n factory = __inboxOpsLlmProviderInternal.createModelFactory(container as AwilixContainer)\n } catch {\n return null\n }\n try {\n const resolution = factory.resolveModel({\n moduleId: 'inbox_ops',\n callerOverride: input.modelOverride ?? undefined,\n })\n const providerId = resolveOpenCodeProviderId(resolution.providerId)\n return {\n modelId: resolution.modelId,\n providerId,\n model: asAiModel(resolution.model),\n }\n } catch (err) {\n if (err instanceof AiModelFactoryError) {\n // Fall back to the legacy path so the shim keeps throwing the original\n // OPENCODE_*-era error messages existing tests/consumers rely on.\n return null\n }\n throw err\n }\n}\n\nexport async function runExtractionWithConfiguredProvider(input: {\n systemPrompt: string\n userPrompt: string\n modelOverride?: string | null\n timeoutMs: number\n}): Promise<{\n object: ReturnType<typeof extractionOutputSchema.parse>\n totalTokens: number\n modelWithProvider: string\n}> {\n const factoryResolution = tryFactoryResolution({ modelOverride: input.modelOverride })\n\n let model: AiModel\n let modelWithProvider: string\n if (factoryResolution) {\n model = factoryResolution.model\n modelWithProvider = `${factoryResolution.providerId}/${factoryResolution.modelId}`\n } else {\n // BC: Legacy OPENCODE_PROVIDER / OPENCODE_MODEL path. This branch only\n // runs when createModelFactory throws AiModelFactoryError('no_provider_configured'),\n // meaning none of the new-style provider env vars (ANTHROPIC_API_KEY,\n // OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OM_AI_PROVIDER, ...) is\n // set. The OPENCODE_* envs are deliberately kept as BC fallbacks instead\n // of being treated as canonical OM_AI_* values because they are scoped to\n // the OpenCode Code Mode stack (spec\n // 2026-04-27-ai-agents-provider-model-baseurl-overrides, R1 mitigation).\n // Keeping this fallback alive preserves backward compatibility for existing\n // inbox_ops deployments that have not yet migrated to the new env vars.\n const providerId = resolveExtractionProviderId()\n const apiKey = requireOpenCodeProviderApiKey(providerId)\n const modelConfig = resolveOpenCodeModel(providerId, {\n overrideModel: input.modelOverride,\n })\n model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n modelWithProvider = modelConfig.modelWithProvider\n }\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: extractionOutputSchema,\n system: input.systemPrompt,\n prompt: input.userPrompt,\n temperature: 0,\n }),\n input.timeoutMs,\n `LLM extraction timed out after ${input.timeoutMs}ms`,\n )\n\n return {\n object: result.object,\n totalTokens: Number(result.usage?.totalTokens ?? 0) || 0,\n modelWithProvider,\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/core",
3
- "version": "0.6.1-develop.3227.1.2b7b8ab258",
3
+ "version": "0.6.1-develop.3256.1.fe3dec2464",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -237,12 +237,12 @@
237
237
  "ts-pattern": "^5.0.0"
238
238
  },
239
239
  "peerDependencies": {
240
- "@open-mercato/ai-assistant": "0.6.1-develop.3227.1.2b7b8ab258",
241
- "@open-mercato/shared": "0.6.1-develop.3227.1.2b7b8ab258"
240
+ "@open-mercato/ai-assistant": "0.6.1-develop.3256.1.fe3dec2464",
241
+ "@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464"
242
242
  },
243
243
  "devDependencies": {
244
- "@open-mercato/ai-assistant": "0.6.1-develop.3227.1.2b7b8ab258",
245
- "@open-mercato/shared": "0.6.1-develop.3227.1.2b7b8ab258",
244
+ "@open-mercato/ai-assistant": "0.6.1-develop.3256.1.fe3dec2464",
245
+ "@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464",
246
246
  "@testing-library/dom": "^10.4.1",
247
247
  "@testing-library/jest-dom": "^6.9.1",
248
248
  "@testing-library/react": "^16.3.1",
@@ -26,61 +26,16 @@
26
26
  * prompt-override merges can address sections by name. The composed
27
27
  * text is fed into `systemPrompt` so the current runtime continues to
28
28
  * work.
29
- *
30
- * Local type declarations mirror the public shapes from
31
- * `@open-mercato/ai-assistant`. `@open-mercato/core` does not depend on
32
- * `@open-mercato/ai-assistant` (see the companion comment in
33
- * `ai-tools/types.ts` and the Step 4.7 / 4.8 implementation notes), so
34
- * the generator imports this file via the app's bundler and the runtime
35
- * graph resolves through `apps/mercato/.mercato/generated/ai-agents.generated.ts`.
36
29
  */
37
- import type { AwilixContainer } from 'awilix'
30
+ import type {
31
+ AiAgentDefinition,
32
+ AiAgentPageContextInput,
33
+ } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-agent-definition'
38
34
  import {
39
35
  hydrateCatalogAssistantContext,
40
36
  hydrateMerchandisingAssistantContext,
41
37
  } from './ai-agents-context'
42
38
 
43
- type AiAgentExecutionMode = 'chat' | 'object'
44
- type AiAgentMutationPolicy = 'read-only' | 'confirm-required' | 'destructive-confirm-required'
45
- type AiAgentAcceptedMediaType = 'image' | 'pdf' | 'file'
46
- type AiAgentDataOperation = 'read' | 'search' | 'aggregate'
47
-
48
- interface AiAgentPageContextInput {
49
- entityType: string
50
- recordId: string
51
- container: AwilixContainer
52
- tenantId: string | null
53
- organizationId: string | null
54
- }
55
-
56
- interface AiAgentDataCapabilities {
57
- entities?: string[]
58
- operations?: AiAgentDataOperation[]
59
- searchableFields?: string[]
60
- }
61
-
62
- interface AiAgentDefinition {
63
- id: string
64
- moduleId: string
65
- label: string
66
- description: string
67
- systemPrompt: string
68
- allowedTools: string[]
69
- executionMode?: AiAgentExecutionMode
70
- defaultModel?: string
71
- acceptedMediaTypes?: AiAgentAcceptedMediaType[]
72
- requiredFeatures?: string[]
73
- uiParts?: string[]
74
- readOnly?: boolean
75
- mutationPolicy?: AiAgentMutationPolicy
76
- maxSteps?: number
77
- output?: unknown
78
- resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>
79
- keywords?: string[]
80
- domain?: string
81
- dataCapabilities?: AiAgentDataCapabilities
82
- }
83
-
84
39
  type PromptSectionName =
85
40
  | 'role'
86
41
  | 'scope'
@@ -314,6 +269,8 @@ const agent: AiAgentDefinition = {
314
269
  requiredFeatures: [...REQUIRED_FEATURES],
315
270
  readOnly: true,
316
271
  mutationPolicy: 'read-only',
272
+ defaultProvider: 'openai',
273
+ defaultModel: 'gpt-5-mini',
317
274
  keywords: ['catalog', 'products', 'categories', 'variants', 'prices', 'offers', 'media'],
318
275
  domain: 'catalog',
319
276
  dataCapabilities: {
@@ -506,6 +463,8 @@ const merchandisingAgent: AiAgentDefinition = {
506
463
  // the operator via the pending-action approval card. Per-tenant override
507
464
  // can downgrade to `read-only` to lock writes without a redeploy.
508
465
  mutationPolicy: 'confirm-required',
466
+ defaultProvider: 'openai',
467
+ defaultModel: 'gpt-5-mini',
509
468
  keywords: ['catalog', 'merchandising', 'products', 'attributes', 'pricing', 'copy'],
510
469
  domain: 'catalog',
511
470
  dataCapabilities: {
@@ -20,60 +20,13 @@
20
20
  * the structured template is additionally exported so downstream Phases
21
21
  * (5.3 prompt-override merge, 5.2 resolvePageContext hydration) can
22
22
  * address sections by name.
23
- *
24
- * Local type declarations mirror the public shapes from
25
- * `@open-mercato/ai-assistant`. The customers module does not depend on
26
- * `@open-mercato/ai-assistant` (see the companion comment in
27
- * `ai-tools/types.ts`) — the generator imports this file via the app's
28
- * bundler, so the runtime graph resolves through
29
- * `apps/mercato/.mercato/generated/ai-agents.generated.ts`. Keeping the
30
- * type declarations local mirrors how the `customers/ai-tools/types.ts`
31
- * handles `AiToolDefinition`.
32
23
  */
33
- import type { AwilixContainer } from 'awilix'
24
+ import type {
25
+ AiAgentDefinition,
26
+ AiAgentPageContextInput,
27
+ } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-agent-definition'
34
28
  import { hydrateCustomersAccountContext } from './ai-agents-context'
35
29
 
36
- type AiAgentExecutionMode = 'chat' | 'object'
37
- type AiAgentMutationPolicy = 'read-only' | 'confirm-required' | 'destructive-confirm-required'
38
- type AiAgentAcceptedMediaType = 'image' | 'pdf' | 'file'
39
- type AiAgentDataOperation = 'read' | 'search' | 'aggregate'
40
-
41
- interface AiAgentPageContextInput {
42
- entityType: string
43
- recordId: string
44
- container: AwilixContainer
45
- tenantId: string | null
46
- organizationId: string | null
47
- }
48
-
49
- interface AiAgentDataCapabilities {
50
- entities?: string[]
51
- operations?: AiAgentDataOperation[]
52
- searchableFields?: string[]
53
- }
54
-
55
- interface AiAgentDefinition {
56
- id: string
57
- moduleId: string
58
- label: string
59
- description: string
60
- systemPrompt: string
61
- allowedTools: string[]
62
- executionMode?: AiAgentExecutionMode
63
- defaultModel?: string
64
- acceptedMediaTypes?: AiAgentAcceptedMediaType[]
65
- requiredFeatures?: string[]
66
- uiParts?: string[]
67
- readOnly?: boolean
68
- mutationPolicy?: AiAgentMutationPolicy
69
- maxSteps?: number
70
- output?: unknown
71
- resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>
72
- keywords?: string[]
73
- domain?: string
74
- dataCapabilities?: AiAgentDataCapabilities
75
- }
76
-
77
30
  type PromptSectionName =
78
31
  | 'role'
79
32
  | 'scope'
@@ -528,5 +528,15 @@ export const openApi = createCustomersCrudOpenApi({
528
528
  schema: z.object({ id: z.string().uuid() }),
529
529
  responseSchema: defaultOkResponseSchema,
530
530
  description: 'Deletes a company by id. The identifier can be provided via body or query.',
531
+ errors: [
532
+ {
533
+ status: 422,
534
+ description: 'Company has dependent records (people, deals, or direct staff); unlink or reassign before delete.',
535
+ schema: z.object({
536
+ error: z.string(),
537
+ code: z.literal('COMPANY_HAS_DEPENDENTS'),
538
+ }),
539
+ },
540
+ ],
531
541
  },
532
542
  })
@@ -559,5 +559,15 @@ export const openApi = createCustomersCrudOpenApi({
559
559
  schema: z.object({ id: z.string().uuid() }),
560
560
  responseSchema: defaultOkResponseSchema,
561
561
  description: 'Deletes a person by id. Request body or query may provide the identifier.',
562
+ errors: [
563
+ {
564
+ status: 422,
565
+ description: 'Person has dependent records (e.g. linked deals); unlink or reassign before delete.',
566
+ schema: z.object({
567
+ error: z.string(),
568
+ code: z.literal('PERSON_HAS_DEPENDENTS'),
569
+ }),
570
+ },
571
+ ],
562
572
  },
563
573
  })
@@ -10,6 +10,9 @@ import { Button } from '@open-mercato/ui/primitives/button'
10
10
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
11
11
  import { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
12
12
  import { buildCrudExportUrl } from '@open-mercato/ui/backend/utils/crud'
13
+ import { groupBulkDeleteFailures, runBulkDelete } from '@open-mercato/ui/backend/utils/bulkDelete'
14
+ import { coalesceLastOperations } from '@open-mercato/ui/backend/operations/store'
15
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
13
16
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
17
  import { E } from '#generated/entities.ids.generated'
15
18
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
@@ -161,6 +164,26 @@ export default function CustomersCompaniesPage() {
161
164
  setPageSize(newSize)
162
165
  setPage(1)
163
166
  }, [])
167
+
168
+ const bulkMutationContextId = 'customers-companies-list:bulk-delete'
169
+ const { runMutation: runBulkMutation, retryLastMutation: retryBulkMutation } = useGuardedMutation<{
170
+ formId: string
171
+ resourceKind: string
172
+ retryLastMutation: () => Promise<boolean>
173
+ }>({
174
+ contextId: bulkMutationContextId,
175
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
176
+ })
177
+ const singleMutationContextId = 'customers-companies-list:single-delete'
178
+ const { runMutation: runSingleMutation, retryLastMutation: retrySingleMutation } = useGuardedMutation<{
179
+ formId: string
180
+ resourceKind: string
181
+ resourceId: string
182
+ retryLastMutation: () => Promise<boolean>
183
+ }>({
184
+ contextId: singleMutationContextId,
185
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
186
+ })
164
187
  const fetchDictionaryEntries = React.useCallback(async (kind: DictionaryKindKey) => {
165
188
  try {
166
189
  const data = await ensureCustomerDictionary(queryClient, kind, scopeVersion)
@@ -460,14 +483,24 @@ export default function CustomersCompaniesPage() {
460
483
  })
461
484
  if (!confirmed) return
462
485
  try {
463
- await apiCallOrThrow(
464
- `/api/customers/companies?id=${encodeURIComponent(company.id)}`,
465
- {
466
- method: 'DELETE',
467
- headers: { 'content-type': 'application/json' },
486
+ await runSingleMutation({
487
+ operation: async () => {
488
+ await apiCallOrThrow(
489
+ `/api/customers/companies?id=${encodeURIComponent(company.id)}`,
490
+ {
491
+ method: 'DELETE',
492
+ headers: { 'content-type': 'application/json' },
493
+ },
494
+ { errorMessage: t('customers.companies.list.deleteError') },
495
+ )
468
496
  },
469
- { errorMessage: t('customers.companies.list.deleteError') },
470
- )
497
+ context: {
498
+ formId: singleMutationContextId,
499
+ resourceKind: 'customers.company',
500
+ resourceId: company.id,
501
+ retryLastMutation: retrySingleMutation,
502
+ },
503
+ })
471
504
  setRows((prev) => prev.filter((row) => row.id !== company.id))
472
505
  setTotal((prev) => Math.max(prev - 1, 0))
473
506
  handleRefresh()
@@ -476,7 +509,7 @@ export default function CustomersCompaniesPage() {
476
509
  const message = err instanceof Error ? err.message : t('customers.companies.list.deleteError')
477
510
  flash(message, 'error')
478
511
  }
479
- }, [confirm, handleRefresh, t])
512
+ }, [confirm, handleRefresh, retrySingleMutation, runSingleMutation, singleMutationContextId, t])
480
513
 
481
514
  const handleBulkDelete = React.useCallback(async (selectedRows: CompanyRow[]) => {
482
515
  const confirmed = await confirm({
@@ -485,44 +518,71 @@ export default function CustomersCompaniesPage() {
485
518
  variant: 'destructive',
486
519
  })
487
520
  if (!confirmed) return false
488
- let deletedCount = 0
489
- const failedIds: string[] = []
490
- for (const row of selectedRows) {
491
- try {
492
- await apiCallOrThrow(`/api/customers/companies?id=${encodeURIComponent(row.id)}`, {
493
- method: 'DELETE',
494
- headers: { 'content-type': 'application/json' },
521
+
522
+ const { succeeded, failures } = await runBulkMutation({
523
+ operation: async () =>
524
+ runBulkDelete(
525
+ selectedRows,
526
+ async (row) => {
527
+ await apiCallOrThrow(`/api/customers/companies?id=${encodeURIComponent(row.id)}`, {
528
+ method: 'DELETE',
529
+ headers: { 'content-type': 'application/json' },
530
+ })
531
+ },
532
+ {
533
+ fallbackErrorMessage: t('customers.companies.list.deleteError', 'Failed to delete company.'),
534
+ logTag: 'customers.companies.list',
535
+ },
536
+ ),
537
+ context: {
538
+ formId: bulkMutationContextId,
539
+ resourceKind: 'customers.company',
540
+ retryLastMutation: retryBulkMutation,
541
+ },
542
+ })
543
+
544
+ if (succeeded.length > 0) {
545
+ const succeededIds = new Set(succeeded.map((r) => r.id))
546
+ setRows((prev) => prev.filter((r) => !succeededIds.has(r.id)))
547
+ setTotal((prev) => Math.max(0, prev - succeeded.length))
548
+ setReloadToken((prev) => prev + 1)
549
+ if (succeeded.length > 1) {
550
+ coalesceLastOperations(succeeded.length, {
551
+ commandId: 'customers.companies.delete',
552
+ actionLabel: t('customers.companies.list.bulkDelete.operationLabel', 'Delete {count} companies', { count: succeeded.length }),
553
+ resourceKind: 'customers.company',
495
554
  })
496
- deletedCount++
497
- } catch (err) {
498
- failedIds.push(row.id)
499
- console.warn('[customers.companies.list] bulk delete failed', row.id, err)
500
555
  }
501
- }
502
- if (deletedCount > 0) {
503
- setRows((prev) => {
504
- const succeeded = new Set(selectedRows.map((r) => r.id).filter((id) => !failedIds.includes(id)))
505
- return prev.filter((r) => !succeeded.has(r.id))
506
- })
507
- setTotal((prev) => Math.max(0, prev - deletedCount))
508
- if (failedIds.length === 0) {
509
- flash(t('customers.companies.list.bulkDelete.success', '{count} companies deleted', { count: deletedCount }), 'success')
556
+ if (failures.length === 0) {
557
+ flash(
558
+ t('customers.companies.list.bulkDelete.success', '{count} companies deleted', { count: succeeded.length }),
559
+ 'success',
560
+ )
510
561
  } else {
511
562
  flash(
512
563
  t('customers.companies.list.bulkDelete.partial', '{deleted} of {total} companies deleted; {failed} failed', {
513
- deleted: deletedCount,
564
+ deleted: succeeded.length,
514
565
  total: selectedRows.length,
515
- failed: failedIds.length,
566
+ failed: failures.length,
516
567
  }),
517
568
  'warning',
518
569
  )
519
570
  }
520
- setReloadToken((prev) => prev + 1)
521
- } else if (failedIds.length > 0) {
522
- flash(t('customers.companies.list.bulkDelete.failed', 'Failed to delete {count} companies', { count: failedIds.length }), 'error')
523
571
  }
524
- return deletedCount > 0
525
- }, [confirm, t])
572
+
573
+ for (const group of groupBulkDeleteFailures(failures)) {
574
+ const message = group.count === 1
575
+ ? group.sampleMessage
576
+ : t(
577
+ 'customers.companies.list.bulkDelete.failedGroup',
578
+ '{count} companies could not be deleted: {message}',
579
+ { count: group.count, message: group.sampleMessage },
580
+ )
581
+ flash(message, 'error')
582
+ }
583
+
584
+ return succeeded.length > 0
585
+ }, [bulkMutationContextId, confirm, retryBulkMutation, runBulkMutation, t])
526
586
 
527
587
  const handleFiltersApply = React.useCallback((values: FilterValues) => {
528
588
  const next: FilterValues = {}