@open-mercato/core 0.6.2-develop.3381.1.8461b312a9 → 0.6.2-develop.3400.2.d948268f50

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 (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/file/[id]/route.js +8 -8
  3. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  4. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -8
  5. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  6. package/dist/modules/attachments/api/library/[id]/route.js +6 -2
  7. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  8. package/dist/modules/attachments/api/library/route.js +2 -0
  9. package/dist/modules/attachments/api/library/route.js.map +2 -2
  10. package/dist/modules/attachments/api/openapi.js +5 -1
  11. package/dist/modules/attachments/api/openapi.js.map +2 -2
  12. package/dist/modules/attachments/api/partitions/route.js +10 -1
  13. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  14. package/dist/modules/attachments/api/route.js +19 -7
  15. package/dist/modules/attachments/api/route.js.map +2 -2
  16. package/dist/modules/attachments/backend/config/attachments/page.js +3 -1
  17. package/dist/modules/attachments/backend/config/attachments/page.js.map +2 -2
  18. package/dist/modules/attachments/cli.js +6 -2
  19. package/dist/modules/attachments/cli.js.map +2 -2
  20. package/dist/modules/attachments/components/AttachmentPartitionSettings.js +216 -10
  21. package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
  22. package/dist/modules/attachments/di.js +13 -0
  23. package/dist/modules/attachments/di.js.map +7 -0
  24. package/dist/modules/attachments/lib/drivers/driverFactory.js +67 -0
  25. package/dist/modules/attachments/lib/drivers/driverFactory.js.map +7 -0
  26. package/dist/modules/attachments/lib/drivers/index.js +11 -0
  27. package/dist/modules/attachments/lib/drivers/index.js.map +7 -0
  28. package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js +42 -0
  29. package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js.map +7 -0
  30. package/dist/modules/attachments/lib/drivers/localDriver.js +70 -0
  31. package/dist/modules/attachments/lib/drivers/localDriver.js.map +7 -0
  32. package/dist/modules/attachments/lib/drivers/types.js +1 -0
  33. package/dist/modules/attachments/lib/drivers/types.js.map +7 -0
  34. package/dist/modules/attachments/lib/ocrQueue.js +10 -5
  35. package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
  36. package/dist/modules/attachments/lib/storage.js.map +2 -2
  37. package/dist/modules/attachments/lib/thumbnailCache.js +11 -13
  38. package/dist/modules/attachments/lib/thumbnailCache.js.map +2 -2
  39. package/dist/modules/integrations/backend/integrations/[id]/page.js +31 -5
  40. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  41. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +14 -2
  42. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  43. package/package.json +11 -7
  44. package/src/modules/attachments/api/file/[id]/route.ts +10 -8
  45. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +8 -8
  46. package/src/modules/attachments/api/library/[id]/route.ts +7 -3
  47. package/src/modules/attachments/api/library/route.ts +2 -0
  48. package/src/modules/attachments/api/openapi.ts +4 -0
  49. package/src/modules/attachments/api/partitions/route.ts +10 -1
  50. package/src/modules/attachments/api/route.ts +20 -7
  51. package/src/modules/attachments/backend/config/attachments/page.tsx +3 -1
  52. package/src/modules/attachments/cli.ts +7 -2
  53. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +275 -7
  54. package/src/modules/attachments/di.ts +13 -0
  55. package/src/modules/attachments/lib/drivers/driverFactory.ts +91 -0
  56. package/src/modules/attachments/lib/drivers/index.ts +4 -0
  57. package/src/modules/attachments/lib/drivers/legacyPublicDriver.ts +46 -0
  58. package/src/modules/attachments/lib/drivers/localDriver.ts +79 -0
  59. package/src/modules/attachments/lib/drivers/types.ts +28 -0
  60. package/src/modules/attachments/lib/ocrQueue.ts +14 -6
  61. package/src/modules/attachments/lib/storage.ts +13 -0
  62. package/src/modules/attachments/lib/thumbnailCache.ts +25 -19
  63. package/src/modules/integrations/backend/integrations/[id]/page.tsx +30 -4
  64. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +15 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/integrations/backend/integrations/bundle/%5Bid%5D/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n}\n\ntype BundleConfigPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{ credentials: Record<string, unknown> }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result?.credentials) {\n setCredValues(credCall.result.credentials)\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n const call = await apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: credValues }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n setIsSavingCreds(false)\n }, [resolveCurrentBundleId, credValues, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n const call = await apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null })\n if (call.ok) {\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId ? { ...item, isEnabled: enabled } : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }, [t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"text-red-500 ml-0.5\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : (\n <Input\n type={field.type === 'secret' ? 'password' : 'text'}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": ";AAiLwC,cAchC,YAdgC;AAhLxC,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,mBAAmB;AAC5B,SAAS,MAAM,gBAAgB;AAC/B,SAAS,MAAM,YAAY,WAAW,mBAAmB;AACzD,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,YAAY;AAErB,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAI7B,MAAM,qCAAqC,oBAAI,IAAyB,CAAC,SAAS,aAAa,CAAC;AAEhG,SAAS,0BAA0B,OAAiC;AAClE,SAAO,CAAC,mCAAmC,IAAI,MAAM,IAAI;AAC3D;AAkCA,SAAS,eAAe,OAA0D;AAChF,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,CAAC;AACxC,SAAO;AACT;AAEA,SAAS,kBAAkB,UAAsC;AAC/D,QAAM,QAAQ,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAChD,QAAM,WAAW,MAAM,GAAG,EAAE;AAC5B,MAAI,CAAC,YAAY,aAAa,YAAY,aAAa,eAAgB,QAAO;AAC9E,SAAO,mBAAmB,QAAQ;AACpC;AAEe,SAAR,iBAAkC,EAAE,OAAO,GAA0B;AAC1E,QAAM,WAAW,YAAY;AAC7B,QAAM,WAAW,eAAe,QAAQ,EAAE,KAAK,kBAAkB,QAAQ;AACzE,QAAM,IAAI,KAAK;AAEf,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA8B,IAAI;AACpE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAkC,CAAC,CAAC;AAC9E,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAsB,oBAAI,IAAI,CAAC;AAE3E,QAAM,yBAAyB,MAAM,YAAY,MAAM;AACrD,WAAO,aACL,OAAO,WAAW,cACd,kBAAkB,OAAO,SAAS,QAAQ,IAC1C;AAAA,EAER,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,OAAO,MAAM,YAAY,YAAY;AACzC,UAAM,kBAAkB,uBAAuB;AAC/C,QAAI,CAAC,iBAAiB;AACpB,eAAS,EAAE,+BAA+B,CAAC;AAC3C,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,iBAAa,IAAI;AACjB,aAAS,IAAI;AACb,UAAM,OAAO,MAAM;AAAA,MACjB,qBAAqB,mBAAmB,eAAe,CAAC;AAAA,MACxD;AAAA,MACA,EAAE,UAAU,KAAK;AAAA,IACnB;AACA,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,QAAQ;AAC5B,eAAS,EAAE,+BAA+B,CAAC;AAC3C,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,cAAU,KAAK,MAAM;AAErB,UAAM,WAAW,MAAM;AAAA,MACrB,qBAAqB,mBAAmB,eAAe,CAAC;AAAA,MACxD;AAAA,MACA,EAAE,UAAU,KAAK;AAAA,IACnB;AACA,QAAI,SAAS,MAAM,SAAS,QAAQ,aAAa;AAC/C,oBAAc,SAAS,OAAO,WAAW;AAAA,IAC3C;AACA,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,wBAAwB,CAAC,CAAC;AAE9B,QAAM,UAAU,MAAM;AAAE,SAAK,KAAK;AAAA,EAAE,GAAG,CAAC,IAAI,CAAC;AAE7C,QAAM,wBAAwB,MAAM,YAAY,YAAY;AAC1D,UAAM,kBAAkB,uBAAuB;AAC/C,QAAI,CAAC,gBAAiB;AACtB,qBAAiB,IAAI;AACrB,UAAM,OAAO,MAAM,QAAQ,qBAAqB,mBAAmB,eAAe,CAAC,gBAAgB;AAAA,MACjG,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,aAAa,WAAW,CAAC;AAAA,IAClD,GAAG,EAAE,UAAU,KAAK,CAAC;AACrB,QAAI,KAAK,IAAI;AACX,YAAM,EAAE,uCAAuC,GAAG,SAAS;AAAA,IAC7D,OAAO;AACL,YAAM,EAAE,2CAA2C,GAAG,OAAO;AAAA,IAC/D;AACA,qBAAiB,KAAK;AAAA,EACxB,GAAG,CAAC,wBAAwB,YAAY,CAAC,CAAC;AAE1C,QAAM,eAAe,MAAM,YAAY,OAAO,eAAuB,YAAqB;AACxF,mBAAe,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,IAAI,aAAa,CAAC;AACzD,UAAM,OAAO,MAAM,QAAQ,qBAAqB,mBAAmB,aAAa,CAAC,UAAU;AAAA,MACzF,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,WAAW,QAAQ,CAAC;AAAA,IAC7C,GAAG,EAAE,UAAU,KAAK,CAAC;AACrB,QAAI,KAAK,IAAI;AACX,gBAAU,CAAC,SAAS;AAClB,YAAI,CAAC,KAAM,QAAO;AAClB,eAAO;AAAA,UACL,GAAG;AAAA,UACH,oBAAoB,KAAK,mBAAmB;AAAA,YAAI,CAAC,SAC/C,KAAK,OAAO,gBAAgB,EAAE,GAAG,MAAM,WAAW,QAAQ,IAAI;AAAA,UAChE;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AACL,YAAM,EAAE,gCAAgC,GAAG,OAAO;AAAA,IACpD;AACA,mBAAe,CAAC,SAAS;AAAE,YAAM,OAAO,IAAI,IAAI,IAAI;AAAG,WAAK,OAAO,aAAa;AAAG,aAAO;AAAA,IAAK,CAAC;AAAA,EAClG,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,mBAAmB,MAAM,YAAY,OAAO,YAAqB;AACrE,QAAI,CAAC,OAAQ;AACb,UAAM,UAAU,OAAO,mBAAmB,OAAO,CAAC,SAAS,KAAK,cAAc,OAAO;AACrF,UAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,SAAS,aAAa,KAAK,IAAI,OAAO,CAAC,CAAC;AAAA,EACzE,GAAG,CAAC,QAAQ,YAAY,CAAC;AAEzB,MAAI,UAAW,QAAO,oBAAC,QAAK,8BAAC,YAAS,8BAAC,kBAAe,OAAO,EAAE,2BAA2B,GAAG,GAAE,GAAW;AAC1G,MAAI,SAAS,CAAC,QAAQ,OAAQ,QAAO,oBAAC,QAAK,8BAAC,YAAS,8BAAC,gBAAa,OAAO,SAAS,EAAE,+BAA+B,GAAG,GAAE,GAAW;AAEpI,QAAM,cAAc,OAAO,OAAO,aAAa,UAAU,CAAC,GAAG,OAAO,yBAAyB;AAE7F,SACE,oBAAC,QACC,+BAAC,YAAS,WAAU,aAClB;AAAA,wBAAC,SACC,8BAAC,QAAK,MAAK,yBAAwB,WAAU,iDAC1C,YAAE,0BAA0B,GAC/B,GACF;AAAA,IAEA,qBAAC,SACC;AAAA,0BAAC,QAAG,WAAU,0BAA0B,iBAAO,OAAO,OAAM;AAAA,MAC3D,OAAO,OAAO,eACb,oBAAC,OAAE,WAAU,8BAA8B,iBAAO,OAAO,aAAY;AAAA,OAEzE;AAAA,IAEC,WAAW,SAAS,KACnB,qBAAC,QACC;AAAA,0BAAC,cACC,8BAAC,aAAW,YAAE,uCAAuC,GAAE,GACzD;AAAA,MACA,qBAAC,eAAY,WAAU,aACpB;AAAA,mBAAW,IAAI,CAAC,UACf,qBAAC,SAAoB,WAAU,eAC7B;AAAA,+BAAC,WAAM,WAAU,uBACd;AAAA,kBAAM;AAAA,YAAO,MAAM,YAAY,oBAAC,UAAK,WAAU,uBAAsB,eAAC;AAAA,aACzE;AAAA,UACC,MAAM,SAAS,YAAY,MAAM,UAChC;AAAA,YAAC;AAAA;AAAA,cACC,OAAQ,WAAW,MAAM,GAAG,KAAgB;AAAA,cAC5C,eAAe,CAAC,UAAU,cAAc,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,SAAS,GAAG,EAAE;AAAA,cAEzF;AAAA,oCAAC,iBACC,8BAAC,eAAY,aAAY,UAAI,GAC/B;AAAA,gBACA,oBAAC,iBACE,gBAAM,QAAQ,IAAI,CAAC,QAClB,oBAAC,cAA2B,OAAO,IAAI,OAAQ,cAAI,SAAlC,IAAI,KAAoC,CAC1D,GACH;AAAA;AAAA;AAAA,UACF,IACE,MAAM,SAAS,YACjB;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,QAAQ,WAAW,MAAM,GAAG,CAAC;AAAA,cACtC,iBAAiB,CAAC,YAAY,cAAc,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,QAAQ,EAAE;AAAA;AAAA,UAC3F,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAM,MAAM,SAAS,WAAW,aAAa;AAAA,cAC7C,aAAa,MAAM;AAAA,cACnB,OAAQ,WAAW,MAAM,GAAG,KAAgB;AAAA,cAC5C,UAAU,CAAC,MAAM,cAAc,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,MAAM,EAAE;AAAA;AAAA,UACrF;AAAA,aA7BM,MAAM,GA+BhB,CACD;AAAA,QACD,qBAAC,UAAO,MAAK,UAAS,SAAS,MAAM,KAAK,sBAAsB,GAAG,UAAU,eAC1E;AAAA,0BAAgB,oBAAC,WAAQ,WAAU,gBAAe,IAAK;AAAA,UACvD,EAAE,sCAAsC;AAAA,WAC3C;AAAA,SACF;AAAA,OACF;AAAA,IAGF,qBAAC,QACC;AAAA,0BAAC,cACC,+BAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,aAAW,YAAE,wCAAwC,GAAE;AAAA,QACxD,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM,KAAK,iBAAiB,IAAI,GACxF,YAAE,oCAAoC,GACzC;AAAA,UACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM,KAAK,iBAAiB,KAAK,GACzF,YAAE,qCAAqC,GAC1C;AAAA,WACF;AAAA,SACF,GACF;AAAA,MACA,oBAAC,eACC,8BAAC,SAAI,WAAU,aACZ,iBAAO,mBAAmB,IAAI,CAAC,SAC9B,qBAAC,SAAkB,WAAU,2DAC3B;AAAA,6BAAC,SACC;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAM,yBAAyB,mBAAmB,KAAK,EAAE,CAAC;AAAA,cAC1D,WAAU;AAAA,cAET,eAAK;AAAA;AAAA,UACR;AAAA,UACC,KAAK,YACJ,oBAAC,SAAM,SAAQ,aAAY,WAAU,gBAAgB,eAAK,UAAS;AAAA,UAEpE,KAAK,eACJ,oBAAC,OAAE,WAAU,wCAAwC,eAAK,aAAY;AAAA,WAE1E;AAAA,QACA,qBAAC,SAAI,WAAU,2BACb;AAAA,8BAAC,UAAO,SAAO,MAAC,SAAQ,SAAQ,MAAK,MACnC,8BAAC,QAAK,MAAM,yBAAyB,mBAAmB,KAAK,EAAE,CAAC,IAC7D,YAAE,0CAA0C,GAC/C,GACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,KAAK;AAAA,cACd,UAAU,YAAY,IAAI,KAAK,EAAE;AAAA,cACjC,iBAAiB,CAAC,YAAY,KAAK,aAAa,KAAK,IAAI,OAAO;AAAA;AAAA,UAClE;AAAA,WACF;AAAA,WA1BQ,KAAK,EA2Bf,CACD,GACH,GACF;AAAA,OACF;AAAA,KACF,GACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n}\n\ntype BundleConfigPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{ credentials: Record<string, unknown> }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result?.credentials) {\n const next = { ...credCall.result.credentials }\n if (currentBundleId === 'storage_s3') {\n const authMode = next.authMode\n if (authMode !== 'access_keys' && authMode !== 'ambient') {\n const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)\n next.authMode = hasKeys ? 'access_keys' : 'ambient'\n }\n }\n setCredValues(next)\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n const call = await apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: credValues }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n setIsSavingCreds(false)\n }, [resolveCurrentBundleId, credValues, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n const call = await apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null })\n if (call.ok) {\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId ? { ...item, isEnabled: enabled } : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }, [t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n function isFieldVisible(field: CredentialField): boolean {\n if (!field.visibleWhen) return true\n return credValues[field.visibleWhen.field] === field.visibleWhen.equals\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.filter(isFieldVisible).map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"text-red-500 ml-0.5\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : (\n <Input\n type={field.type === 'secret' ? 'password' : 'text'}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": ";AAyLwC,cAmBhC,YAnBgC;AAxLxC,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,mBAAmB;AAC5B,SAAS,MAAM,gBAAgB;AAC/B,SAAS,MAAM,YAAY,WAAW,mBAAmB;AACzD,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,YAAY;AAErB,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAI7B,MAAM,qCAAqC,oBAAI,IAAyB,CAAC,SAAS,aAAa,CAAC;AAEhG,SAAS,0BAA0B,OAAiC;AAClE,SAAO,CAAC,mCAAmC,IAAI,MAAM,IAAI;AAC3D;AAkCA,SAAS,eAAe,OAA0D;AAChF,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,CAAC;AACxC,SAAO;AACT;AAEA,SAAS,kBAAkB,UAAsC;AAC/D,QAAM,QAAQ,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAChD,QAAM,WAAW,MAAM,GAAG,EAAE;AAC5B,MAAI,CAAC,YAAY,aAAa,YAAY,aAAa,eAAgB,QAAO;AAC9E,SAAO,mBAAmB,QAAQ;AACpC;AAEe,SAAR,iBAAkC,EAAE,OAAO,GAA0B;AAC1E,QAAM,WAAW,YAAY;AAC7B,QAAM,WAAW,eAAe,QAAQ,EAAE,KAAK,kBAAkB,QAAQ;AACzE,QAAM,IAAI,KAAK;AAEf,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA8B,IAAI;AACpE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAkC,CAAC,CAAC;AAC9E,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAsB,oBAAI,IAAI,CAAC;AAE3E,QAAM,yBAAyB,MAAM,YAAY,MAAM;AACrD,WAAO,aACL,OAAO,WAAW,cACd,kBAAkB,OAAO,SAAS,QAAQ,IAC1C;AAAA,EAER,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,OAAO,MAAM,YAAY,YAAY;AACzC,UAAM,kBAAkB,uBAAuB;AAC/C,QAAI,CAAC,iBAAiB;AACpB,eAAS,EAAE,+BAA+B,CAAC;AAC3C,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,iBAAa,IAAI;AACjB,aAAS,IAAI;AACb,UAAM,OAAO,MAAM;AAAA,MACjB,qBAAqB,mBAAmB,eAAe,CAAC;AAAA,MACxD;AAAA,MACA,EAAE,UAAU,KAAK;AAAA,IACnB;AACA,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,QAAQ;AAC5B,eAAS,EAAE,+BAA+B,CAAC;AAC3C,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,cAAU,KAAK,MAAM;AAErB,UAAM,WAAW,MAAM;AAAA,MACrB,qBAAqB,mBAAmB,eAAe,CAAC;AAAA,MACxD;AAAA,MACA,EAAE,UAAU,KAAK;AAAA,IACnB;AACA,QAAI,SAAS,MAAM,SAAS,QAAQ,aAAa;AAC/C,YAAM,OAAO,EAAE,GAAG,SAAS,OAAO,YAAY;AAC9C,UAAI,oBAAoB,cAAc;AACpC,cAAM,WAAW,KAAK;AACtB,YAAI,aAAa,iBAAiB,aAAa,WAAW;AACxD,gBAAM,UAAU,QAAQ,KAAK,eAAe,KAAK,eAAe;AAChE,eAAK,WAAW,UAAU,gBAAgB;AAAA,QAC5C;AAAA,MACF;AACA,oBAAc,IAAI;AAAA,IACpB;AACA,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,wBAAwB,CAAC,CAAC;AAE9B,QAAM,UAAU,MAAM;AAAE,SAAK,KAAK;AAAA,EAAE,GAAG,CAAC,IAAI,CAAC;AAE7C,QAAM,wBAAwB,MAAM,YAAY,YAAY;AAC1D,UAAM,kBAAkB,uBAAuB;AAC/C,QAAI,CAAC,gBAAiB;AACtB,qBAAiB,IAAI;AACrB,UAAM,OAAO,MAAM,QAAQ,qBAAqB,mBAAmB,eAAe,CAAC,gBAAgB;AAAA,MACjG,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,aAAa,WAAW,CAAC;AAAA,IAClD,GAAG,EAAE,UAAU,KAAK,CAAC;AACrB,QAAI,KAAK,IAAI;AACX,YAAM,EAAE,uCAAuC,GAAG,SAAS;AAAA,IAC7D,OAAO;AACL,YAAM,EAAE,2CAA2C,GAAG,OAAO;AAAA,IAC/D;AACA,qBAAiB,KAAK;AAAA,EACxB,GAAG,CAAC,wBAAwB,YAAY,CAAC,CAAC;AAE1C,QAAM,eAAe,MAAM,YAAY,OAAO,eAAuB,YAAqB;AACxF,mBAAe,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,IAAI,aAAa,CAAC;AACzD,UAAM,OAAO,MAAM,QAAQ,qBAAqB,mBAAmB,aAAa,CAAC,UAAU;AAAA,MACzF,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,WAAW,QAAQ,CAAC;AAAA,IAC7C,GAAG,EAAE,UAAU,KAAK,CAAC;AACrB,QAAI,KAAK,IAAI;AACX,gBAAU,CAAC,SAAS;AAClB,YAAI,CAAC,KAAM,QAAO;AAClB,eAAO;AAAA,UACL,GAAG;AAAA,UACH,oBAAoB,KAAK,mBAAmB;AAAA,YAAI,CAAC,SAC/C,KAAK,OAAO,gBAAgB,EAAE,GAAG,MAAM,WAAW,QAAQ,IAAI;AAAA,UAChE;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AACL,YAAM,EAAE,gCAAgC,GAAG,OAAO;AAAA,IACpD;AACA,mBAAe,CAAC,SAAS;AAAE,YAAM,OAAO,IAAI,IAAI,IAAI;AAAG,WAAK,OAAO,aAAa;AAAG,aAAO;AAAA,IAAK,CAAC;AAAA,EAClG,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,mBAAmB,MAAM,YAAY,OAAO,YAAqB;AACrE,QAAI,CAAC,OAAQ;AACb,UAAM,UAAU,OAAO,mBAAmB,OAAO,CAAC,SAAS,KAAK,cAAc,OAAO;AACrF,UAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,SAAS,aAAa,KAAK,IAAI,OAAO,CAAC,CAAC;AAAA,EACzE,GAAG,CAAC,QAAQ,YAAY,CAAC;AAEzB,MAAI,UAAW,QAAO,oBAAC,QAAK,8BAAC,YAAS,8BAAC,kBAAe,OAAO,EAAE,2BAA2B,GAAG,GAAE,GAAW;AAC1G,MAAI,SAAS,CAAC,QAAQ,OAAQ,QAAO,oBAAC,QAAK,8BAAC,YAAS,8BAAC,gBAAa,OAAO,SAAS,EAAE,+BAA+B,GAAG,GAAE,GAAW;AAEpI,QAAM,cAAc,OAAO,OAAO,aAAa,UAAU,CAAC,GAAG,OAAO,yBAAyB;AAE7F,WAAS,eAAe,OAAiC;AACvD,QAAI,CAAC,MAAM,YAAa,QAAO;AAC/B,WAAO,WAAW,MAAM,YAAY,KAAK,MAAM,MAAM,YAAY;AAAA,EACnE;AAEA,SACE,oBAAC,QACC,+BAAC,YAAS,WAAU,aAClB;AAAA,wBAAC,SACC,8BAAC,QAAK,MAAK,yBAAwB,WAAU,iDAC1C,YAAE,0BAA0B,GAC/B,GACF;AAAA,IAEA,qBAAC,SACC;AAAA,0BAAC,QAAG,WAAU,0BAA0B,iBAAO,OAAO,OAAM;AAAA,MAC3D,OAAO,OAAO,eACb,oBAAC,OAAE,WAAU,8BAA8B,iBAAO,OAAO,aAAY;AAAA,OAEzE;AAAA,IAEC,WAAW,SAAS,KACnB,qBAAC,QACC;AAAA,0BAAC,cACC,8BAAC,aAAW,YAAE,uCAAuC,GAAE,GACzD;AAAA,MACA,qBAAC,eAAY,WAAU,aACpB;AAAA,mBAAW,OAAO,cAAc,EAAE,IAAI,CAAC,UACtC,qBAAC,SAAoB,WAAU,eAC7B;AAAA,+BAAC,WAAM,WAAU,uBACd;AAAA,kBAAM;AAAA,YAAO,MAAM,YAAY,oBAAC,UAAK,WAAU,uBAAsB,eAAC;AAAA,aACzE;AAAA,UACC,MAAM,SAAS,YAAY,MAAM,UAChC;AAAA,YAAC;AAAA;AAAA,cACC,OAAQ,WAAW,MAAM,GAAG,KAAgB;AAAA,cAC5C,eAAe,CAAC,UAAU,cAAc,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,SAAS,GAAG,EAAE;AAAA,cAEzF;AAAA,oCAAC,iBACC,8BAAC,eAAY,aAAY,UAAI,GAC/B;AAAA,gBACA,oBAAC,iBACE,gBAAM,QAAQ,IAAI,CAAC,QAClB,oBAAC,cAA2B,OAAO,IAAI,OAAQ,cAAI,SAAlC,IAAI,KAAoC,CAC1D,GACH;AAAA;AAAA;AAAA,UACF,IACE,MAAM,SAAS,YACjB;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,QAAQ,WAAW,MAAM,GAAG,CAAC;AAAA,cACtC,iBAAiB,CAAC,YAAY,cAAc,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,QAAQ,EAAE;AAAA;AAAA,UAC3F,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAM,MAAM,SAAS,WAAW,aAAa;AAAA,cAC7C,aAAa,MAAM;AAAA,cACnB,OAAQ,WAAW,MAAM,GAAG,KAAgB;AAAA,cAC5C,UAAU,CAAC,MAAM,cAAc,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,MAAM,EAAE;AAAA;AAAA,UACrF;AAAA,aA7BM,MAAM,GA+BhB,CACD;AAAA,QACD,qBAAC,UAAO,MAAK,UAAS,SAAS,MAAM,KAAK,sBAAsB,GAAG,UAAU,eAC1E;AAAA,0BAAgB,oBAAC,WAAQ,WAAU,gBAAe,IAAK;AAAA,UACvD,EAAE,sCAAsC;AAAA,WAC3C;AAAA,SACF;AAAA,OACF;AAAA,IAGF,qBAAC,QACC;AAAA,0BAAC,cACC,+BAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,aAAW,YAAE,wCAAwC,GAAE;AAAA,QACxD,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM,KAAK,iBAAiB,IAAI,GACxF,YAAE,oCAAoC,GACzC;AAAA,UACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM,KAAK,iBAAiB,KAAK,GACzF,YAAE,qCAAqC,GAC1C;AAAA,WACF;AAAA,SACF,GACF;AAAA,MACA,oBAAC,eACC,8BAAC,SAAI,WAAU,aACZ,iBAAO,mBAAmB,IAAI,CAAC,SAC9B,qBAAC,SAAkB,WAAU,2DAC3B;AAAA,6BAAC,SACC;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAM,yBAAyB,mBAAmB,KAAK,EAAE,CAAC;AAAA,cAC1D,WAAU;AAAA,cAET,eAAK;AAAA;AAAA,UACR;AAAA,UACC,KAAK,YACJ,oBAAC,SAAM,SAAQ,aAAY,WAAU,gBAAgB,eAAK,UAAS;AAAA,UAEpE,KAAK,eACJ,oBAAC,OAAE,WAAU,wCAAwC,eAAK,aAAY;AAAA,WAE1E;AAAA,QACA,qBAAC,SAAI,WAAU,2BACb;AAAA,8BAAC,UAAO,SAAO,MAAC,SAAQ,SAAQ,MAAK,MACnC,8BAAC,QAAK,MAAM,yBAAyB,mBAAmB,KAAK,EAAE,CAAC,IAC7D,YAAE,0CAA0C,GAC/C,GACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,KAAK;AAAA,cACd,UAAU,YAAY,IAAI,KAAK,EAAE;AAAA,cACjC,iBAAiB,CAAC,YAAY,KAAK,aAAa,KAAK,IAAI,OAAO;AAAA;AAAA,UAClE;AAAA,WACF;AAAA,WA1BQ,KAAK,EA2Bf,CACD,GACH,GACF;AAAA,OACF;AAAA,KACF,GACF;AAEJ;",
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.2-develop.3381.1.8461b312a9",
3
+ "version": "0.6.2-develop.3400.2.d948268f50",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -109,6 +109,10 @@
109
109
  "types": "./src/modules/attachments/index.ts",
110
110
  "default": "./dist/modules/attachments/index.js"
111
111
  },
112
+ "./modules/attachments/lib/drivers": {
113
+ "types": "./src/modules/attachments/lib/drivers/index.ts",
114
+ "default": "./dist/modules/attachments/lib/drivers/index.js"
115
+ },
112
116
  "./modules/dashboards": {
113
117
  "types": "./src/modules/dashboards/index.ts",
114
118
  "default": "./dist/modules/dashboards/index.js"
@@ -238,16 +242,16 @@
238
242
  "zod": "^4.3.6"
239
243
  },
240
244
  "peerDependencies": {
241
- "@open-mercato/ai-assistant": "0.6.2-develop.3381.1.8461b312a9",
242
- "@open-mercato/shared": "0.6.2-develop.3381.1.8461b312a9",
243
- "@open-mercato/ui": "0.6.2-develop.3381.1.8461b312a9",
245
+ "@open-mercato/ai-assistant": "0.6.2-develop.3400.2.d948268f50",
246
+ "@open-mercato/shared": "0.6.2-develop.3400.2.d948268f50",
247
+ "@open-mercato/ui": "0.6.2-develop.3400.2.d948268f50",
244
248
  "react": "^19.0.0",
245
249
  "react-dom": "^19.0.0"
246
250
  },
247
251
  "devDependencies": {
248
- "@open-mercato/ai-assistant": "0.6.2-develop.3381.1.8461b312a9",
249
- "@open-mercato/shared": "0.6.2-develop.3381.1.8461b312a9",
250
- "@open-mercato/ui": "0.6.2-develop.3381.1.8461b312a9",
252
+ "@open-mercato/ai-assistant": "0.6.2-develop.3400.2.d948268f50",
253
+ "@open-mercato/shared": "0.6.2-develop.3400.2.d948268f50",
254
+ "@open-mercato/ui": "0.6.2-develop.3400.2.d948268f50",
251
255
  "@testing-library/dom": "^10.4.1",
252
256
  "@testing-library/jest-dom": "^6.9.1",
253
257
  "@testing-library/react": "^16.3.1",
@@ -1,5 +1,4 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { promises as fs } from "fs";
3
2
  import type { OpenApiRouteDoc } from "@open-mercato/shared/lib/openapi";
4
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
@@ -7,7 +6,6 @@ import {
7
6
  Attachment,
8
7
  AttachmentPartition,
9
8
  } from "@open-mercato/core/modules/attachments/data/entities";
10
- import { resolveAttachmentAbsolutePath } from "@open-mercato/core/modules/attachments/lib/storage";
11
9
  import type { EntityManager } from "@mikro-orm/postgresql";
12
10
  import { checkAttachmentAccess } from "@open-mercato/core/modules/attachments/lib/access";
13
11
  import { z } from "zod";
@@ -16,6 +14,7 @@ import {
16
14
  buildAttachmentContentDisposition,
17
15
  canRenderInlineAttachment,
18
16
  } from "@open-mercato/core/modules/attachments/lib/security";
17
+ import { StorageDriverFactory } from '../../../lib/drivers';
19
18
 
20
19
  export const metadata = {
21
20
  GET: { requireAuth: false },
@@ -35,6 +34,9 @@ export async function GET(
35
34
  const auth = await getAuthFromRequest(req);
36
35
  const { resolve } = await createRequestContainer();
37
36
  const em = resolve("em") as EntityManager;
37
+ const storageDriverFactory =
38
+ (resolve("storageDriverFactory") as StorageDriverFactory | null) ??
39
+ new StorageDriverFactory(em);
38
40
 
39
41
  const attachment = await em.findOne(Attachment, { id });
40
42
  if (!attachment) {
@@ -59,14 +61,14 @@ export async function GET(
59
61
  return NextResponse.json({ error: message }, { status: access.status });
60
62
  }
61
63
 
62
- const filePath = resolveAttachmentAbsolutePath(
63
- attachment.partitionCode,
64
- attachment.storagePath,
65
- attachment.storageDriver,
66
- );
64
+ const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {
65
+ tenantId: attachment.tenantId ?? '',
66
+ organizationId: attachment.organizationId ?? '',
67
+ });
67
68
  let buffer: Buffer;
68
69
  try {
69
- buffer = await fs.readFile(filePath);
70
+ const result = await driver.read(attachment.partitionCode, attachment.storagePath);
71
+ buffer = result.buffer;
70
72
  } catch {
71
73
  return NextResponse.json({ error: "File not available" }, { status: 404 });
72
74
  }
@@ -5,7 +5,6 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
5
5
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
6
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
7
7
  import { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'
8
- import { resolveAttachmentAbsolutePath } from '@open-mercato/core/modules/attachments/lib/storage'
9
8
  import {
10
9
  buildThumbnailCacheKey,
11
10
  readThumbnailCache,
@@ -14,13 +13,13 @@ import {
14
13
  import { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'
15
14
  import { checkAttachmentAccess } from '@open-mercato/core/modules/attachments/lib/access'
16
15
  import type { EntityManager } from '@mikro-orm/postgresql'
17
- import { promises as fs } from 'fs'
18
16
  import { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'
19
17
  import {
20
18
  MAX_IMAGE_SOURCE_PIXELS,
21
19
  validateImageDimensions,
22
20
  validateImageMagicBytes,
23
21
  } from '@open-mercato/core/modules/attachments/lib/imageSafety'
22
+ import { StorageDriverFactory } from '../../../../lib/drivers'
24
23
 
25
24
  const querySchema = z.object({
26
25
  width: z.coerce.number().int().min(1).max(4000).optional(),
@@ -51,6 +50,8 @@ export async function GET(
51
50
 
52
51
  const { resolve } = await createRequestContainer()
53
52
  const em = resolve('em') as EntityManager
53
+ const storageDriverFactory =
54
+ (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
54
55
 
55
56
  const attachment = await em.findOne(Attachment, {
56
57
  id,
@@ -71,11 +72,10 @@ export async function GET(
71
72
  return NextResponse.json({ error: message }, { status: access.status })
72
73
  }
73
74
 
74
- const filePath = resolveAttachmentAbsolutePath(
75
- attachment.partitionCode,
76
- attachment.storagePath,
77
- attachment.storageDriver
78
- )
75
+ const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {
76
+ tenantId: attachment.tenantId ?? '',
77
+ organizationId: attachment.organizationId ?? '',
78
+ })
79
79
  const cacheKey = buildThumbnailCacheKey(width, height, cropType)
80
80
  try {
81
81
  let buffer: Buffer | null = null
@@ -83,7 +83,7 @@ export async function GET(
83
83
  buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)
84
84
  }
85
85
  if (!buffer) {
86
- const input = await fs.readFile(filePath)
86
+ const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)
87
87
  const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)
88
88
  if (!magicBytesValidation.ok) {
89
89
  return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })
@@ -11,7 +11,7 @@ import {
11
11
  normalizeAttachmentTags,
12
12
  readAttachmentMetadata,
13
13
  } from '../../../lib/metadata'
14
- import { deletePartitionFile } from '../../../lib/storage'
14
+ import type { StorageDriverFactory } from '../../../lib/drivers'
15
15
  import { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'
16
16
  import { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'
17
17
  import { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'
@@ -228,6 +228,7 @@ export async function DELETE(req: NextRequest, ctx: RouteContext) {
228
228
  const { resolve } = await createRequestContainer()
229
229
  const em = resolve('em') as EntityManager
230
230
  const dataEngine = resolve('dataEngine') as DataEngine
231
+ const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory
231
232
  const deleteFilter: Record<string, unknown> = {
232
233
  id: attachmentId,
233
234
  tenantId: auth.tenantId,
@@ -237,8 +238,11 @@ export async function DELETE(req: NextRequest, ctx: RouteContext) {
237
238
  if (!record) {
238
239
  return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })
239
240
  }
240
-
241
- await deletePartitionFile(record.partitionCode, record.storagePath, record.storageDriver)
241
+ const deleteDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
242
+ tenantId: record.tenantId ?? auth.tenantId,
243
+ organizationId: record.organizationId ?? auth.orgId,
244
+ })
245
+ await deleteDriver.delete(record.partitionCode, record.storagePath)
242
246
  await em.remove(record).flush()
243
247
 
244
248
  if (dataEngine) {
@@ -11,6 +11,7 @@ import { readAttachmentMetadata } from '../../lib/metadata'
11
11
  import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
12
12
  import { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../lib/assignmentDetails'
13
13
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
14
+ import { ensureDefaultPartitions } from '../../lib/partitions'
14
15
  import {
15
16
  attachmentsTag,
16
17
  attachmentListQuerySchema as openApiListQuerySchema,
@@ -70,6 +71,7 @@ export async function GET(req: Request) {
70
71
  const offset = (page - 1) * pageSize
71
72
  const { resolve } = await createRequestContainer()
72
73
  const em = resolve('em') as EntityManager
74
+ await ensureDefaultPartitions(em)
73
75
  let queryEngine: QueryEngine | null = null
74
76
  try {
75
77
  queryEngine = resolve('queryEngine') as QueryEngine
@@ -78,6 +78,8 @@ export const partitionSchema = z.object({
78
78
  isPublic: z.boolean(),
79
79
  requiresOcr: z.boolean(),
80
80
  ocrModel: z.string().nullable(),
81
+ storageDriver: z.string().optional().describe('Storage driver: local (default) or s3'),
82
+ configJson: z.record(z.string(), z.unknown()).nullable().optional().describe('Driver-specific configuration'),
81
83
  createdAt: z.string().nullable(),
82
84
  updatedAt: z.string().nullable(),
83
85
  envKey: z.string(),
@@ -99,6 +101,8 @@ export const partitionCreateSchema = z.object({
99
101
  isPublic: z.boolean().optional().describe('Whether partition is publicly accessible'),
100
102
  requiresOcr: z.boolean().optional().describe('Whether OCR should be performed on uploads'),
101
103
  ocrModel: z.string().max(50).nullable().optional().describe('OCR model to use'),
104
+ storageDriver: z.enum(['local', 's3']).optional().default('local').describe('Storage driver to use'),
105
+ configJson: z.record(z.string(), z.unknown()).nullable().optional().describe('Driver-specific configuration (e.g. S3 bucket, region, endpoint)'),
102
106
  })
103
107
 
104
108
  export const partitionUpdateSchema = partitionCreateSchema.extend({
@@ -32,6 +32,8 @@ function serializePartition(entry: AttachmentPartition) {
32
32
  isPublic: entry.isPublic ?? false,
33
33
  requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),
34
34
  ocrModel: entry.ocrModel ?? null,
35
+ storageDriver: entry.storageDriver ?? 'local',
36
+ configJson: entry.configJson ?? null,
35
37
  createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,
36
38
  updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,
37
39
  envKey: resolvePartitionEnvKey(entry.code),
@@ -96,7 +98,8 @@ export async function POST(req: Request) {
96
98
  code,
97
99
  title: parsed.data.title.trim(),
98
100
  description: parsed.data.description?.trim() ?? null,
99
- storageDriver: 'local',
101
+ storageDriver: parsed.data.storageDriver ?? 'local',
102
+ configJson: parsed.data.configJson ?? null,
100
103
  isPublic: parsed.data.isPublic ?? false,
101
104
  requiresOcr:
102
105
  typeof parsed.data.requiresOcr === 'boolean'
@@ -146,6 +149,12 @@ export async function PUT(req: Request) {
146
149
  if (parsed.data.ocrModel !== undefined) {
147
150
  entry.ocrModel = parsed.data.ocrModel?.trim() || null
148
151
  }
152
+ if (parsed.data.storageDriver !== undefined) {
153
+ entry.storageDriver = parsed.data.storageDriver
154
+ }
155
+ if (parsed.data.configJson !== undefined) {
156
+ entry.configJson = parsed.data.configJson ?? null
157
+ }
149
158
  await em.persist(entry).flush()
150
159
  return NextResponse.json({ item: serializePartition(entry) })
151
160
  }
@@ -7,9 +7,9 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
7
7
  import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '../lib/imageUrls'
8
8
  import { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from '../lib/partitions'
9
9
  import { Attachment, AttachmentPartition } from '../data/entities'
10
- import { storePartitionFile, deletePartitionFile } from '../lib/storage'
11
10
  import { extractAttachmentContent } from '../lib/textExtraction'
12
11
  import { requestOcrProcessing } from '../lib/ocrQueue'
12
+ import { StorageDriverFactory } from '../lib/drivers'
13
13
  import { OcrService, shouldUseLlmOcr } from '../lib/ocrService'
14
14
  import { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'
15
15
  import {
@@ -289,6 +289,8 @@ export async function POST(req: Request) {
289
289
  const { resolve } = await createRequestContainer()
290
290
  const em = resolve('em') as EntityManager
291
291
  const dataEngine = resolve('dataEngine')
292
+ const storageDriverFactory =
293
+ (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
292
294
  await ensureDefaultPartitions(em)
293
295
  // Optional per-field validations
294
296
  let partitionFromField: string | null = null
@@ -374,15 +376,17 @@ export async function POST(req: Request) {
374
376
  if (requestedPublicOverride) {
375
377
  return NextResponse.json({ error: t('attachments.errors.publicPartitionBlocked', 'Public storage partitions cannot be selected explicitly for this upload.') }, { status: 403 })
376
378
  }
377
- let stored
379
+ const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId })
380
+ let storedPath: string
378
381
  try {
379
- stored = await storePartitionFile({
382
+ const stored = await uploadDriver.store({
380
383
  partitionCode: partition.code,
381
384
  orgId,
382
385
  tenantId,
383
386
  fileName: safeName,
384
387
  buffer: buf,
385
388
  })
389
+ storedPath = stored.storagePath
386
390
  } catch (error) {
387
391
  console.error('[attachments] failed to persist file', error)
388
392
  return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })
@@ -398,13 +402,16 @@ export async function POST(req: Request) {
398
402
  const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)
399
403
 
400
404
  if (requiresOcr && !useLlmOcr) {
405
+ const { filePath: localPath, cleanup } = await uploadDriver.toLocalPath(partition.code, storedPath)
401
406
  try {
402
407
  extractedContent = await extractAttachmentContent({
403
- filePath: stored.absolutePath,
408
+ filePath: localPath,
404
409
  mimeType: fileMimeType,
405
410
  })
406
411
  } catch (error) {
407
412
  console.error('[attachments] failed to extract attachment content', error)
413
+ } finally {
414
+ await cleanup().catch(() => {})
408
415
  }
409
416
  }
410
417
 
@@ -425,7 +432,7 @@ export async function POST(req: Request) {
425
432
  fileSize: buf.length,
426
433
  partitionCode: partition.code,
427
434
  storageDriver: partition.storageDriver || 'local',
428
- storagePath: stored.storagePath,
435
+ storagePath: storedPath,
429
436
  url: buildAttachmentFileUrl(attachmentId),
430
437
  content: extractedContent,
431
438
  storageMetadata: metadata,
@@ -433,7 +440,7 @@ export async function POST(req: Request) {
433
440
  await em.persist(att).flush()
434
441
 
435
442
  if (useLlmOcr) {
436
- requestOcrProcessing(em, att, stored.absolutePath).catch((error) => {
443
+ requestOcrProcessing(em, att, uploadDriver, storedPath).catch((error) => {
437
444
  console.error('[attachments] failed to queue OCR processing', error)
438
445
  })
439
446
  } else if (wantsLlmOcr) {
@@ -519,6 +526,8 @@ export async function DELETE(req: Request) {
519
526
  const { resolve } = await createRequestContainer()
520
527
  const em = resolve('em') as EntityManager
521
528
  const dataEngine = resolve('dataEngine')
529
+ const storageDriverFactory =
530
+ (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
522
531
  const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId!, organizationId: auth.orgId }
523
532
  const record = await em.findOne(Attachment, deleteFilter)
524
533
  if (!record) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })
@@ -527,7 +536,11 @@ export async function DELETE(req: Request) {
527
536
  console.error('[attachments] failed to cleanup cached thumbnails', error)
528
537
  })
529
538
  if (record.storagePath) {
530
- await deletePartitionFile(record.partitionCode, record.storagePath, record.storageDriver)
539
+ const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
540
+ tenantId: record.tenantId ?? auth.tenantId!,
541
+ organizationId: record.organizationId ?? auth.orgId,
542
+ })
543
+ await delDriver.delete(record.partitionCode, record.storagePath)
531
544
  }
532
545
  if (dataEngine) {
533
546
  await emitCrudSideEffects({
@@ -1,11 +1,13 @@
1
1
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
2
2
  import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
3
3
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
+ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
4
5
  import { AttachmentPartitionSettings } from '../../../components/AttachmentPartitionSettings'
5
6
  import { isPartitionSettingsLocked } from '../../../lib/partitions'
6
7
 
7
8
  export default async function AttachmentsConfigurationPage() {
8
9
  const partitionsLocked = isPartitionSettingsLocked()
10
+ const s3Enabled = parseBooleanWithDefault(process.env.OM_ENABLE_STORAGE_S3, false)
9
11
  const { t } = await resolveTranslations()
10
12
  const lockedTitle = t('attachments.partitions.locked.title', 'Partition settings locked')
11
13
  const lockedDescription = t(
@@ -30,7 +32,7 @@ export default async function AttachmentsConfigurationPage() {
30
32
  </AlertDescription>
31
33
  </Alert>
32
34
  ) : null}
33
- {!partitionsLocked ? <AttachmentPartitionSettings /> : null}
35
+ {!partitionsLocked ? <AttachmentPartitionSettings s3Enabled={s3Enabled} /> : null}
34
36
  </PageBody>
35
37
  </Page>
36
38
  )
@@ -2,7 +2,7 @@ import type { ModuleCli } from '@open-mercato/shared/modules/registry'
2
2
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
4
  import { Attachment } from './data/entities'
5
- import { deletePartitionFile } from './lib/storage'
5
+ import type { StorageDriverFactory } from './lib/drivers'
6
6
 
7
7
  type ParsedArgs = Record<string, string | boolean>
8
8
 
@@ -65,6 +65,7 @@ const deleteAttachments: ModuleCli = {
65
65
  try {
66
66
  const container = await createRequestContainer()
67
67
  const em = container.resolve<EntityManager>('em')
68
+ const storageDriverFactory = container.resolve<StorageDriverFactory>('storageDriverFactory')
68
69
  const idList = Array.from(ids)
69
70
  const where: Record<string, unknown> = { id: { $in: idList } }
70
71
  if (organizationId) where.organizationId = organizationId
@@ -76,7 +77,11 @@ const deleteAttachments: ModuleCli = {
76
77
  }
77
78
  const removedIds = new Set<string>()
78
79
  for (const entry of attachments) {
79
- await deletePartitionFile(entry.partitionCode, entry.storagePath, entry.storageDriver)
80
+ const driver = await storageDriverFactory.resolveForPartition(entry.partitionCode, {
81
+ tenantId: entry.tenantId ?? '',
82
+ organizationId: entry.organizationId ?? '',
83
+ })
84
+ await driver.delete(entry.partitionCode, entry.storagePath)
80
85
  em.remove(entry)
81
86
  removedIds.add(entry.id)
82
87
  console.log(`Deleted attachment ${entry.id}${entry.fileName ? ` (${entry.fileName})` : ''}`)