@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
@@ -0,0 +1,42 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ class LegacyPublicStorageDriver {
4
+ constructor() {
5
+ this.key = "legacyPublic";
6
+ }
7
+ store(_payload) {
8
+ throw new Error("legacy-public driver is read-only");
9
+ }
10
+ async read(_partitionCode, storagePath) {
11
+ const absolutePath = this.resolveAbsolutePath(storagePath);
12
+ const buffer = await fs.readFile(absolutePath);
13
+ return { buffer };
14
+ }
15
+ async delete(_partitionCode, storagePath) {
16
+ const absolutePath = this.resolveAbsolutePath(storagePath);
17
+ try {
18
+ await fs.unlink(absolutePath);
19
+ } catch {
20
+ }
21
+ }
22
+ async toLocalPath(_partitionCode, storagePath) {
23
+ return {
24
+ filePath: this.resolveAbsolutePath(storagePath),
25
+ cleanup: async () => {
26
+ }
27
+ };
28
+ }
29
+ resolveAbsolutePath(storagePath) {
30
+ let safeRelative = storagePath.replace(/^\/*/, "");
31
+ let prev;
32
+ do {
33
+ prev = safeRelative;
34
+ safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, "");
35
+ } while (safeRelative !== prev);
36
+ return path.join(process.cwd(), safeRelative);
37
+ }
38
+ }
39
+ export {
40
+ LegacyPublicStorageDriver
41
+ };
42
+ //# sourceMappingURL=legacyPublicDriver.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/attachments/lib/drivers/legacyPublicDriver.ts"],
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nexport class LegacyPublicStorageDriver implements StorageDriver {\n readonly key = 'legacyPublic'\n\n store(_payload: StoreFilePayload): Promise<StoredFile> {\n throw new Error('legacy-public driver is read-only')\n }\n\n async read(_partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(_partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n _partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n return {\n filePath: this.resolveAbsolutePath(storagePath),\n cleanup: async () => {},\n }\n }\n\n private resolveAbsolutePath(storagePath: string): string {\n let safeRelative = storagePath.replace(/^\\/*/, '')\n let prev: string\n do {\n prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n return path.join(process.cwd(), safeRelative)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AAGV,MAAM,0BAAmD;AAAA,EAAzD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,UAAiD;AACrD,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAAA,EAEA,MAAM,KAAK,gBAAwB,aAA8C;AAC/E,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,gBAAwB,aAAoC;AACvE,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,gBACA,aAC6D;AAC7D,WAAO;AAAA,MACL,UAAU,KAAK,oBAAoB,WAAW;AAAA,MAC9C,SAAS,YAAY;AAAA,MAAC;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,oBAAoB,aAA6B;AACvD,QAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AACjD,QAAI;AACJ,OAAG;AACD,aAAO;AACP,qBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,IACxD,SAAS,iBAAiB;AAC1B,WAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AAAA,EAC9C;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,70 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import { randomUUID } from "crypto";
4
+ import { resolvePartitionRoot } from "../storage.js";
5
+ function sanitizeFileName(fileName) {
6
+ if (!fileName) return "file";
7
+ return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
8
+ }
9
+ function resolveOrgSegment(orgId) {
10
+ if (typeof orgId === "string" && orgId.trim().length > 0) return `org_${orgId}`;
11
+ return "org_shared";
12
+ }
13
+ function resolveTenantSegment(tenantId) {
14
+ if (typeof tenantId === "string" && tenantId.trim().length > 0) return `tenant_${tenantId}`;
15
+ return "tenant_shared";
16
+ }
17
+ class LocalStorageDriver {
18
+ constructor() {
19
+ this.key = "local";
20
+ }
21
+ async store(payload) {
22
+ const root = resolvePartitionRoot(payload.partitionCode);
23
+ const orgSegment = resolveOrgSegment(payload.orgId ?? null);
24
+ const tenantSegment = resolveTenantSegment(payload.tenantId ?? null);
25
+ const safeName = sanitizeFileName(payload.fileName || "file");
26
+ const uniqueSuffix = randomUUID().replace(/-/g, "").slice(0, 12);
27
+ const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`;
28
+ const relativePath = path.join(orgSegment, tenantSegment, storedName);
29
+ const absolutePath = path.join(root, relativePath);
30
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
31
+ await fs.writeFile(absolutePath, payload.buffer);
32
+ return {
33
+ storagePath: relativePath.replace(/\\/g, "/")
34
+ };
35
+ }
36
+ async read(partitionCode, storagePath) {
37
+ const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath);
38
+ const buffer = await fs.readFile(absolutePath);
39
+ return { buffer };
40
+ }
41
+ async delete(partitionCode, storagePath) {
42
+ const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath);
43
+ try {
44
+ await fs.unlink(absolutePath);
45
+ } catch {
46
+ }
47
+ }
48
+ async toLocalPath(partitionCode, storagePath) {
49
+ const filePath = this.resolveAbsolutePath(partitionCode, storagePath);
50
+ return {
51
+ filePath,
52
+ cleanup: async () => {
53
+ }
54
+ };
55
+ }
56
+ resolveAbsolutePath(partitionCode, storagePath) {
57
+ let safeRelative = storagePath.replace(/^\/*/, "");
58
+ let prev;
59
+ do {
60
+ prev = safeRelative;
61
+ safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, "");
62
+ } while (safeRelative !== prev);
63
+ const root = resolvePartitionRoot(partitionCode);
64
+ return path.join(root, safeRelative);
65
+ }
66
+ }
67
+ export {
68
+ LocalStorageDriver
69
+ };
70
+ //# sourceMappingURL=localDriver.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/attachments/lib/drivers/localDriver.ts"],
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionRoot } from '../storage'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\nexport class LocalStorageDriver implements StorageDriver {\n readonly key = 'local'\n\n async store(payload: StoreFilePayload): Promise<StoredFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n }\n }\n\n async read(partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n const filePath = this.resolveAbsolutePath(partitionCode, storagePath)\n return {\n filePath,\n cleanup: async () => {\n // no-op: local path is the real file, do not delete\n },\n }\n }\n\n private resolveAbsolutePath(partitionCode: string, storagePath: string): string {\n let safeRelative = storagePath.replace(/^\\/*/, '')\n let prev: string\n do {\n prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,4BAA4B;AAGrC,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAEO,MAAM,mBAA4C;AAAA,EAAlD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,MAAM,SAAgD;AAC1D,UAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,UAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,UAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,UAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,UAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,UAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,UAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,UAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,UAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,WAAO;AAAA,MACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,eAAuB,aAA8C;AAC9E,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,eAAuB,aAAoC;AACtE,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,eACA,aAC6D;AAC7D,UAAM,WAAW,KAAK,oBAAoB,eAAe,WAAW;AACpE,WAAO;AAAA,MACL;AAAA,MACA,SAAS,YAAY;AAAA,MAErB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,oBAAoB,eAAuB,aAA6B;AAC9E,QAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AACjD,QAAI;AACJ,OAAG;AACD,aAAO;AACP,qBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,IACxD,SAAS,iBAAiB;AAC1B,UAAM,OAAO,qBAAqB,aAAa;AAC/C,WAAO,KAAK,KAAK,MAAM,YAAY;AAAA,EACrC;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -1,9 +1,10 @@
1
1
  import { Attachment, AttachmentPartition } from "../data/entities.js";
2
2
  import { OcrService } from "./ocrService.js";
3
- async function processAttachmentOcr(em, payload) {
4
- const { attachmentId, filePath, mimeType, partitionCode } = payload;
3
+ async function processAttachmentOcr(em, payload, driver) {
4
+ const { attachmentId, storagePath, mimeType, partitionCode } = payload;
5
5
  console.log(`[attachments.ocr] Processing started for attachment: ${attachmentId}`);
6
6
  const startTime = Date.now();
7
+ const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath);
7
8
  try {
8
9
  const partition = await em.findOne(AttachmentPartition, { code: partitionCode });
9
10
  const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? "gpt-4o";
@@ -40,12 +41,16 @@ async function processAttachmentOcr(em, payload) {
40
41
  attachmentId,
41
42
  error: error instanceof Error ? error.message : String(error)
42
43
  });
44
+ } finally {
45
+ await cleanup().catch((cleanupError) => {
46
+ console.warn(`[attachments.ocr] Temp file cleanup failed:`, cleanupError);
47
+ });
43
48
  }
44
49
  }
45
- async function requestOcrProcessing(em, attachment, filePath) {
50
+ async function requestOcrProcessing(em, attachment, driver, storagePath) {
46
51
  const payload = {
47
52
  attachmentId: attachment.id,
48
- filePath,
53
+ storagePath,
49
54
  mimeType: attachment.mimeType,
50
55
  partitionCode: attachment.partitionCode,
51
56
  organizationId: attachment.organizationId ?? null,
@@ -53,7 +58,7 @@ async function requestOcrProcessing(em, attachment, filePath) {
53
58
  };
54
59
  setImmediate(() => {
55
60
  const workerEm = typeof em?.fork === "function" ? em.fork() : em;
56
- processAttachmentOcr(workerEm, payload).catch((error) => {
61
+ processAttachmentOcr(workerEm, payload, driver).catch((error) => {
57
62
  console.error(`[attachments.ocr] Background processing error:`, error);
58
63
  });
59
64
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/ocrQueue.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { OcrService } from './ocrService'\n\nexport type OcrRequestedEvent = {\n attachmentId: string\n filePath: string\n mimeType: string\n partitionCode: string\n organizationId: string | null\n tenantId: string | null\n}\n\nexport async function processAttachmentOcr(\n em: EntityManager,\n payload: OcrRequestedEvent\n): Promise<void> {\n const { attachmentId, filePath, mimeType, partitionCode } = payload\n\n console.log(`[attachments.ocr] Processing started for attachment: ${attachmentId}`)\n const startTime = Date.now()\n\n try {\n const partition = await em.findOne(AttachmentPartition, { code: partitionCode })\n const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? 'gpt-4o'\n\n const ocrService = new OcrService()\n\n if (!ocrService.available) {\n console.warn(`[attachments.ocr] OPENAI_API_KEY not configured, skipping OCR for: ${attachmentId}`)\n return\n }\n\n const result = await ocrService.processFile({\n filePath,\n mimeType,\n model: resolvedModel,\n })\n\n if (!result) {\n console.log(`[attachments.ocr] No content extracted for attachment: ${attachmentId}`)\n return\n }\n\n const attachment = await em.findOne(Attachment, { id: attachmentId })\n if (!attachment) {\n console.error(`[attachments.ocr] Attachment not found: ${attachmentId}`)\n return\n }\n\n attachment.content = result.content\n await em.persist(attachment).flush()\n\n console.log(`[attachments.ocr] Processing completed:`, {\n attachmentId,\n pageCount: result.pageCount,\n contentLength: result.content.length,\n timeMs: result.processingTimeMs,\n totalTimeMs: Date.now() - startTime,\n })\n } catch (error) {\n console.error(`[attachments.ocr] Processing failed:`, {\n attachmentId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n}\n\nexport async function requestOcrProcessing(\n em: EntityManager,\n attachment: Attachment,\n filePath: string\n): Promise<void> {\n const payload: OcrRequestedEvent = {\n attachmentId: attachment.id,\n filePath,\n mimeType: attachment.mimeType,\n partitionCode: attachment.partitionCode,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n }\n\n setImmediate(() => {\n const workerEm = typeof (em as any)?.fork === 'function' ? (em as any).fork() : em\n processAttachmentOcr(workerEm, payload).catch((error) => {\n console.error(`[attachments.ocr] Background processing error:`, error)\n })\n })\n}\n"],
5
- "mappings": "AACA,SAAS,YAAY,2BAA2B;AAChD,SAAS,kBAAkB;AAW3B,eAAsB,qBACpB,IACA,SACe;AACf,QAAM,EAAE,cAAc,UAAU,UAAU,cAAc,IAAI;AAE5D,UAAQ,IAAI,wDAAwD,YAAY,EAAE;AAClF,QAAM,YAAY,KAAK,IAAI;AAE3B,MAAI;AACF,UAAM,YAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC/E,UAAM,gBAAgB,WAAW,YAAY,QAAQ,IAAI,aAAa;AAEtE,UAAM,aAAa,IAAI,WAAW;AAElC,QAAI,CAAC,WAAW,WAAW;AACzB,cAAQ,KAAK,sEAAsE,YAAY,EAAE;AACjG;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,WAAW,YAAY;AAAA,MAC1C;AAAA,MACA;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAED,QAAI,CAAC,QAAQ;AACX,cAAQ,IAAI,0DAA0D,YAAY,EAAE;AACpF;AAAA,IACF;AAEA,UAAM,aAAa,MAAM,GAAG,QAAQ,YAAY,EAAE,IAAI,aAAa,CAAC;AACpE,QAAI,CAAC,YAAY;AACf,cAAQ,MAAM,2CAA2C,YAAY,EAAE;AACvE;AAAA,IACF;AAEA,eAAW,UAAU,OAAO;AAC5B,UAAM,GAAG,QAAQ,UAAU,EAAE,MAAM;AAEnC,YAAQ,IAAI,2CAA2C;AAAA,MACrD;AAAA,MACA,WAAW,OAAO;AAAA,MAClB,eAAe,OAAO,QAAQ;AAAA,MAC9B,QAAQ,OAAO;AAAA,MACf,aAAa,KAAK,IAAI,IAAI;AAAA,IAC5B,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,wCAAwC;AAAA,MACpD;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH;AACF;AAEA,eAAsB,qBACpB,IACA,YACA,UACe;AACf,QAAM,UAA6B;AAAA,IACjC,cAAc,WAAW;AAAA,IACzB;AAAA,IACA,UAAU,WAAW;AAAA,IACrB,eAAe,WAAW;AAAA,IAC1B,gBAAgB,WAAW,kBAAkB;AAAA,IAC7C,UAAU,WAAW,YAAY;AAAA,EACnC;AAEA,eAAa,MAAM;AACjB,UAAM,WAAW,OAAQ,IAAY,SAAS,aAAc,GAAW,KAAK,IAAI;AAChF,yBAAqB,UAAU,OAAO,EAAE,MAAM,CAAC,UAAU;AACvD,cAAQ,MAAM,kDAAkD,KAAK;AAAA,IACvE,CAAC;AAAA,EACH,CAAC;AACH;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { OcrService } from './ocrService'\nimport type { StorageDriver } from './drivers/types'\n\nexport type OcrRequestedEvent = {\n attachmentId: string\n storagePath: string\n mimeType: string\n partitionCode: string\n organizationId: string | null\n tenantId: string | null\n}\n\nexport async function processAttachmentOcr(\n em: EntityManager,\n payload: OcrRequestedEvent,\n driver: StorageDriver,\n): Promise<void> {\n const { attachmentId, storagePath, mimeType, partitionCode } = payload\n\n console.log(`[attachments.ocr] Processing started for attachment: ${attachmentId}`)\n const startTime = Date.now()\n\n const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)\n try {\n const partition = await em.findOne(AttachmentPartition, { code: partitionCode })\n const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? 'gpt-4o'\n\n const ocrService = new OcrService()\n\n if (!ocrService.available) {\n console.warn(`[attachments.ocr] OPENAI_API_KEY not configured, skipping OCR for: ${attachmentId}`)\n return\n }\n\n const result = await ocrService.processFile({\n filePath,\n mimeType,\n model: resolvedModel,\n })\n\n if (!result) {\n console.log(`[attachments.ocr] No content extracted for attachment: ${attachmentId}`)\n return\n }\n\n const attachment = await em.findOne(Attachment, { id: attachmentId })\n if (!attachment) {\n console.error(`[attachments.ocr] Attachment not found: ${attachmentId}`)\n return\n }\n\n attachment.content = result.content\n await em.persist(attachment).flush()\n\n console.log(`[attachments.ocr] Processing completed:`, {\n attachmentId,\n pageCount: result.pageCount,\n contentLength: result.content.length,\n timeMs: result.processingTimeMs,\n totalTimeMs: Date.now() - startTime,\n })\n } catch (error) {\n console.error(`[attachments.ocr] Processing failed:`, {\n attachmentId,\n error: error instanceof Error ? error.message : String(error),\n })\n } finally {\n await cleanup().catch((cleanupError) => {\n console.warn(`[attachments.ocr] Temp file cleanup failed:`, cleanupError)\n })\n }\n}\n\nexport async function requestOcrProcessing(\n em: EntityManager,\n attachment: Attachment,\n driver: StorageDriver,\n storagePath: string,\n): Promise<void> {\n const payload: OcrRequestedEvent = {\n attachmentId: attachment.id,\n storagePath,\n mimeType: attachment.mimeType,\n partitionCode: attachment.partitionCode,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n }\n\n setImmediate(() => {\n const workerEm = typeof (em as any)?.fork === 'function' ? (em as any).fork() : em\n processAttachmentOcr(workerEm, payload, driver).catch((error) => {\n console.error(`[attachments.ocr] Background processing error:`, error)\n })\n })\n}\n"],
5
+ "mappings": "AACA,SAAS,YAAY,2BAA2B;AAChD,SAAS,kBAAkB;AAY3B,eAAsB,qBACpB,IACA,SACA,QACe;AACf,QAAM,EAAE,cAAc,aAAa,UAAU,cAAc,IAAI;AAE/D,UAAQ,IAAI,wDAAwD,YAAY,EAAE;AAClF,QAAM,YAAY,KAAK,IAAI;AAE3B,QAAM,EAAE,UAAU,QAAQ,IAAI,MAAM,OAAO,YAAY,eAAe,WAAW;AACjF,MAAI;AACF,UAAM,YAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC/E,UAAM,gBAAgB,WAAW,YAAY,QAAQ,IAAI,aAAa;AAEtE,UAAM,aAAa,IAAI,WAAW;AAElC,QAAI,CAAC,WAAW,WAAW;AACzB,cAAQ,KAAK,sEAAsE,YAAY,EAAE;AACjG;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,WAAW,YAAY;AAAA,MAC1C;AAAA,MACA;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAED,QAAI,CAAC,QAAQ;AACX,cAAQ,IAAI,0DAA0D,YAAY,EAAE;AACpF;AAAA,IACF;AAEA,UAAM,aAAa,MAAM,GAAG,QAAQ,YAAY,EAAE,IAAI,aAAa,CAAC;AACpE,QAAI,CAAC,YAAY;AACf,cAAQ,MAAM,2CAA2C,YAAY,EAAE;AACvE;AAAA,IACF;AAEA,eAAW,UAAU,OAAO;AAC5B,UAAM,GAAG,QAAQ,UAAU,EAAE,MAAM;AAEnC,YAAQ,IAAI,2CAA2C;AAAA,MACrD;AAAA,MACA,WAAW,OAAO;AAAA,MAClB,eAAe,OAAO,QAAQ;AAAA,MAC9B,QAAQ,OAAO;AAAA,MACf,aAAa,KAAK,IAAI,IAAI;AAAA,IAC5B,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,wCAAwC;AAAA,MACpD;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH,UAAE;AACA,UAAM,QAAQ,EAAE,MAAM,CAAC,iBAAiB;AACtC,cAAQ,KAAK,+CAA+C,YAAY;AAAA,IAC1E,CAAC;AAAA,EACH;AACF;AAEA,eAAsB,qBACpB,IACA,YACA,QACA,aACe;AACf,QAAM,UAA6B;AAAA,IACjC,cAAc,WAAW;AAAA,IACzB;AAAA,IACA,UAAU,WAAW;AAAA,IACrB,eAAe,WAAW;AAAA,IAC1B,gBAAgB,WAAW,kBAAkB;AAAA,IAC7C,UAAU,WAAW,YAAY;AAAA,EACnC;AAEA,eAAa,MAAM;AACjB,UAAM,WAAW,OAAQ,IAAY,SAAS,aAAc,GAAW,KAAK,IAAI;AAChF,yBAAqB,UAAU,SAAS,MAAM,EAAE,MAAM,CAAC,UAAU;AAC/D,cAAQ,MAAM,kDAAkD,KAAK;AAAA,IACvE,CAAC;AAAA,EACH,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/storage.ts"],
4
- "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(process.cwd(), 'storage', 'attachments', code)\n}\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\nexport type StorePartitionFilePayload = {\n partitionCode: string\n orgId: string | null | undefined\n tenantId: string | null | undefined\n fileName: string\n buffer: Buffer\n}\n\nexport type StoredPartitionFile = {\n storagePath: string\n absolutePath: string\n fileName: string\n}\n\nexport async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n absolutePath,\n fileName: storedName,\n }\n}\n\nexport function resolveAttachmentAbsolutePath(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): string {\n // Remove leading slashes first\n let safeRelative = storagePath.replace(/^\\/*/, '')\n // Remove all ../ (and ..\\) path traversal segments, repeatedly until gone\n do {\n var prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n if (storageDriver === 'legacyPublic') {\n return path.join(process.cwd(), safeRelative)\n }\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n}\n\nexport async function deletePartitionFile(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): Promise<void> {\n const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AAEhC,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe,IAAI;AAChE;AAEA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAgBA,eAAsB,mBAAmB,SAAkE;AACzG,QAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,QAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,QAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,QAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,QAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,QAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,QAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,QAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,QAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,QAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,SAAO;AAAA,IACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC5C;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAEO,SAAS,8BACd,eACA,aACA,eACQ;AAER,MAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AAEjD,KAAG;AACD,QAAI,OAAO;AACX,mBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,EACxD,SAAS,iBAAiB;AAC1B,MAAI,kBAAkB,gBAAgB;AACpC,WAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AAAA,EAC9C;AACA,QAAM,OAAO,qBAAqB,aAAa;AAC/C,SAAO,KAAK,KAAK,MAAM,YAAY;AACrC;AAEA,eAAsB,oBACpB,eACA,aACA,eACe;AACf,QAAM,eAAe,8BAA8B,eAAe,aAAa,aAAa;AAC5F,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;",
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(process.cwd(), 'storage', 'attachments', code)\n}\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n * Kept for backward compatibility with external callers.\n */\nexport type StorePartitionFilePayload = {\n partitionCode: string\n orgId: string | null | undefined\n tenantId: string | null | undefined\n fileName: string\n buffer: Buffer\n}\n\nexport type StoredPartitionFile = {\n storagePath: string\n absolutePath: string\n fileName: string\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n */\nexport async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n absolutePath,\n fileName: storedName,\n }\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.read()` / `driver.toLocalPath()` instead.\n */\nexport function resolveAttachmentAbsolutePath(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): string {\n // Remove leading slashes first\n let safeRelative = storagePath.replace(/^\\/*/, '')\n // Remove all ../ (and ..\\) path traversal segments, repeatedly until gone\n do {\n var prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n if (storageDriver === 'legacyPublic') {\n return path.join(process.cwd(), safeRelative)\n }\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.delete()` instead.\n */\nexport async function deletePartitionFile(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): Promise<void> {\n const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AAEhC,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe,IAAI;AAChE;AAEA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAuBA,eAAsB,mBAAmB,SAAkE;AACzG,QAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,QAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,QAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,QAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,QAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,QAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,QAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,QAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,QAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,QAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,SAAO;AAAA,IACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC5C;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAKO,SAAS,8BACd,eACA,aACA,eACQ;AAER,MAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AAEjD,KAAG;AACD,QAAI,OAAO;AACX,mBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,EACxD,SAAS,iBAAiB;AAC1B,MAAI,kBAAkB,gBAAgB;AACpC,WAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AAAA,EAC9C;AACA,QAAM,OAAO,qBAAqB,aAAa;AAC/C,SAAO,KAAK,KAAK,MAAM,YAAY;AACrC;AAKA,eAAsB,oBACpB,eACA,aACA,eACe;AACf,QAAM,eAAe,8BAA8B,eAAe,aAAa,aAAa;AAC5F,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,26 +1,25 @@
1
1
  import { promises as fs } from "fs";
2
2
  import path from "path";
3
- import { resolvePartitionRoot } from "./storage.js";
4
- const CACHE_ROOT_SEGMENTS = [".cache", "thumbnails"];
5
3
  function sanitizeSegment(value) {
6
4
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
7
5
  }
8
- function buildThumbnailCacheKey(width, height, cropType) {
9
- if (!width && !height) return null;
10
- const safeWidth = typeof width === "number" && Number.isFinite(width) ? width : "auto";
11
- const safeHeight = typeof height === "number" && Number.isFinite(height) ? height : "auto";
12
- const safeCrop = cropType === "contain" ? "contain" : "cover";
13
- return `w${safeWidth}-h${safeHeight}-c${safeCrop}`;
6
+ function resolveCacheRoot(partitionCode) {
7
+ return path.join(process.cwd(), "storage", ".cache", "thumbnails", sanitizeSegment(partitionCode));
14
8
  }
15
9
  function resolveCachePath(partitionCode, attachmentId, cacheKey) {
16
- const root = resolvePartitionRoot(partitionCode);
17
10
  return path.join(
18
- root,
19
- ...CACHE_ROOT_SEGMENTS,
11
+ resolveCacheRoot(partitionCode),
20
12
  sanitizeSegment(attachmentId),
21
13
  sanitizeSegment(cacheKey)
22
14
  );
23
15
  }
16
+ function buildThumbnailCacheKey(width, height, cropType) {
17
+ if (!width && !height) return null;
18
+ const safeWidth = typeof width === "number" && Number.isFinite(width) ? width : "auto";
19
+ const safeHeight = typeof height === "number" && Number.isFinite(height) ? height : "auto";
20
+ const safeCrop = cropType === "contain" ? "contain" : "cover";
21
+ return `w${safeWidth}-h${safeHeight}-c${safeCrop}`;
22
+ }
24
23
  async function readThumbnailCache(partitionCode, attachmentId, cacheKey) {
25
24
  const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey);
26
25
  try {
@@ -35,8 +34,7 @@ async function writeThumbnailCache(partitionCode, attachmentId, cacheKey, data)
35
34
  await fs.writeFile(cachePath, data);
36
35
  }
37
36
  async function clearAttachmentThumbnailCache(partitionCode, attachmentId) {
38
- const root = resolvePartitionRoot(partitionCode);
39
- const dir = path.join(root, ...CACHE_ROOT_SEGMENTS, sanitizeSegment(attachmentId));
37
+ const dir = path.join(resolveCacheRoot(partitionCode), sanitizeSegment(attachmentId));
40
38
  try {
41
39
  await fs.rm(dir, { recursive: true, force: true });
42
40
  } catch {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/thumbnailCache.ts"],
4
- "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { resolvePartitionRoot } from './storage'\n\nconst CACHE_ROOT_SEGMENTS = ['.cache', 'thumbnails']\n\nfunction sanitizeSegment(value: string): string {\n return value.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nexport function buildThumbnailCacheKey(\n width?: number,\n height?: number,\n cropType?: 'cover' | 'contain'\n): string | null {\n if (!width && !height) return null\n const safeWidth = typeof width === 'number' && Number.isFinite(width) ? width : 'auto'\n const safeHeight = typeof height === 'number' && Number.isFinite(height) ? height : 'auto'\n const safeCrop = cropType === 'contain' ? 'contain' : 'cover'\n return `w${safeWidth}-h${safeHeight}-c${safeCrop}`\n}\n\nfunction resolveCachePath(partitionCode: string, attachmentId: string, cacheKey: string): string {\n const root = resolvePartitionRoot(partitionCode)\n return path.join(\n root,\n ...CACHE_ROOT_SEGMENTS,\n sanitizeSegment(attachmentId),\n sanitizeSegment(cacheKey)\n )\n}\n\nexport async function readThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n cacheKey: string\n): Promise<Buffer | null> {\n const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)\n try {\n return await fs.readFile(cachePath)\n } catch {\n return null\n }\n}\n\nexport async function writeThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n cacheKey: string,\n data: Buffer\n): Promise<void> {\n const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)\n await fs.mkdir(path.dirname(cachePath), { recursive: true })\n await fs.writeFile(cachePath, data)\n}\n\nexport async function clearAttachmentThumbnailCache(partitionCode: string, attachmentId: string): Promise<void> {\n const root = resolvePartitionRoot(partitionCode)\n const dir = path.join(root, ...CACHE_ROOT_SEGMENTS, sanitizeSegment(attachmentId))\n try {\n await fs.rm(dir, { recursive: true, force: true })\n } catch {\n // ignore cache cleanup failure\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,4BAA4B;AAErC,MAAM,sBAAsB,CAAC,UAAU,YAAY;AAEnD,SAAS,gBAAgB,OAAuB;AAC9C,SAAO,MAAM,QAAQ,oBAAoB,GAAG;AAC9C;AAEO,SAAS,uBACd,OACA,QACA,UACe;AACf,MAAI,CAAC,SAAS,CAAC,OAAQ,QAAO;AAC9B,QAAM,YAAY,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AAChF,QAAM,aAAa,OAAO,WAAW,YAAY,OAAO,SAAS,MAAM,IAAI,SAAS;AACpF,QAAM,WAAW,aAAa,YAAY,YAAY;AACtD,SAAO,IAAI,SAAS,KAAK,UAAU,KAAK,QAAQ;AAClD;AAEA,SAAS,iBAAiB,eAAuB,cAAsB,UAA0B;AAC/F,QAAM,OAAO,qBAAqB,aAAa;AAC/C,SAAO,KAAK;AAAA,IACV;AAAA,IACA,GAAG;AAAA,IACH,gBAAgB,YAAY;AAAA,IAC5B,gBAAgB,QAAQ;AAAA,EAC1B;AACF;AAEA,eAAsB,mBACpB,eACA,cACA,UACwB;AACxB,QAAM,YAAY,iBAAiB,eAAe,cAAc,QAAQ;AACxE,MAAI;AACF,WAAO,MAAM,GAAG,SAAS,SAAS;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,oBACpB,eACA,cACA,UACA,MACe;AACf,QAAM,YAAY,iBAAiB,eAAe,cAAc,QAAQ;AACxE,QAAM,GAAG,MAAM,KAAK,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3D,QAAM,GAAG,UAAU,WAAW,IAAI;AACpC;AAEA,eAAsB,8BAA8B,eAAuB,cAAqC;AAC9G,QAAM,OAAO,qBAAqB,aAAa;AAC/C,QAAM,MAAM,KAAK,KAAK,MAAM,GAAG,qBAAqB,gBAAgB,YAAY,CAAC;AACjF,MAAI;AACF,UAAM,GAAG,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACnD,QAAQ;AAAA,EAER;AACF;",
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\n\nfunction sanitizeSegment(value: string): string {\n return value.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\n/**\n * Resolves the thumbnail cache root. Uses a dedicated directory under\n * `storage/.cache/thumbnails/<partitionCode>/` that is independent of\n * the partition's storage backend (local or S3).\n */\nfunction resolveCacheRoot(partitionCode: string): string {\n return path.join(process.cwd(), 'storage', '.cache', 'thumbnails', sanitizeSegment(partitionCode))\n}\n\nfunction resolveCachePath(partitionCode: string, attachmentId: string, cacheKey: string): string {\n return path.join(\n resolveCacheRoot(partitionCode),\n sanitizeSegment(attachmentId),\n sanitizeSegment(cacheKey),\n )\n}\n\nexport function buildThumbnailCacheKey(\n width?: number,\n height?: number,\n cropType?: 'cover' | 'contain',\n): string | null {\n if (!width && !height) return null\n const safeWidth = typeof width === 'number' && Number.isFinite(width) ? width : 'auto'\n const safeHeight = typeof height === 'number' && Number.isFinite(height) ? height : 'auto'\n const safeCrop = cropType === 'contain' ? 'contain' : 'cover'\n return `w${safeWidth}-h${safeHeight}-c${safeCrop}`\n}\n\nexport async function readThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n cacheKey: string,\n): Promise<Buffer | null> {\n const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)\n try {\n return await fs.readFile(cachePath)\n } catch {\n return null\n }\n}\n\nexport async function writeThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n cacheKey: string,\n data: Buffer,\n): Promise<void> {\n const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)\n await fs.mkdir(path.dirname(cachePath), { recursive: true })\n await fs.writeFile(cachePath, data)\n}\n\nexport async function clearAttachmentThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n): Promise<void> {\n const dir = path.join(resolveCacheRoot(partitionCode), sanitizeSegment(attachmentId))\n try {\n await fs.rm(dir, { recursive: true, force: true })\n } catch {\n // ignore cache cleanup failure\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AAEjB,SAAS,gBAAgB,OAAuB;AAC9C,SAAO,MAAM,QAAQ,oBAAoB,GAAG;AAC9C;AAOA,SAAS,iBAAiB,eAA+B;AACvD,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,UAAU,cAAc,gBAAgB,aAAa,CAAC;AACnG;AAEA,SAAS,iBAAiB,eAAuB,cAAsB,UAA0B;AAC/F,SAAO,KAAK;AAAA,IACV,iBAAiB,aAAa;AAAA,IAC9B,gBAAgB,YAAY;AAAA,IAC5B,gBAAgB,QAAQ;AAAA,EAC1B;AACF;AAEO,SAAS,uBACd,OACA,QACA,UACe;AACf,MAAI,CAAC,SAAS,CAAC,OAAQ,QAAO;AAC9B,QAAM,YAAY,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AAChF,QAAM,aAAa,OAAO,WAAW,YAAY,OAAO,SAAS,MAAM,IAAI,SAAS;AACpF,QAAM,WAAW,aAAa,YAAY,YAAY;AACtD,SAAO,IAAI,SAAS,KAAK,UAAU,KAAK,QAAQ;AAClD;AAEA,eAAsB,mBACpB,eACA,cACA,UACwB;AACxB,QAAM,YAAY,iBAAiB,eAAe,cAAc,QAAQ;AACxE,MAAI;AACF,WAAO,MAAM,GAAG,SAAS,SAAS;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,oBACpB,eACA,cACA,UACA,MACe;AACf,QAAM,YAAY,iBAAiB,eAAe,cAAc,QAAQ;AACxE,QAAM,GAAG,MAAM,KAAK,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3D,QAAM,GAAG,UAAU,WAAW,IAAI;AACpC;AAEA,eAAsB,8BACpB,eACA,cACe;AACf,QAAM,MAAM,KAAK,KAAK,iBAAiB,aAAa,GAAG,gBAAgB,YAAY,CAAC;AACpF,MAAI;AACF,UAAM,GAAG,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACnD,QAAQ;AAAA,EAER;AACF;",
6
6
  "names": []
7
7
  }
@@ -176,7 +176,8 @@ function buildCredentialFields(credFields) {
176
176
  /* @__PURE__ */ jsx(WebhookSetupGuide, { guide: field.helpDetails, buttonLabel: "Show details" })
177
177
  ] }) : field.helpText,
178
178
  placeholder: field.placeholder,
179
- required: field.required
179
+ required: field.required,
180
+ visibleWhen: field.visibleWhen
180
181
  };
181
182
  if (field.type === "secret") {
182
183
  return {
@@ -326,7 +327,15 @@ function IntegrationDetailPage({ params }) {
326
327
  { fallback: null }
327
328
  );
328
329
  if (call.ok && call.result?.credentials) {
329
- setCredValues(call.result.credentials);
330
+ const next = { ...call.result.credentials };
331
+ if (currentIntegrationId === "storage_s3") {
332
+ const authMode = next.authMode;
333
+ if (authMode !== "access_keys" && authMode !== "ambient") {
334
+ const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey);
335
+ next.authMode = hasKeys ? "access_keys" : "ambient";
336
+ }
337
+ }
338
+ setCredValues(next);
330
339
  setCredentialsFormKey((current) => current + 1);
331
340
  }
332
341
  }, [resolveCurrentIntegrationId]);
@@ -517,18 +526,31 @@ function IntegrationDetailPage({ params }) {
517
526
  if (!currentIntegrationId) return;
518
527
  setIsSavingCredentials(true);
519
528
  try {
529
+ const sanitizedValues = { ...values };
530
+ if (currentIntegrationId === "storage_s3") {
531
+ const authMode = sanitizedValues.authMode;
532
+ if (authMode !== "access_keys" && authMode !== "ambient") {
533
+ const hasKeys = Boolean(sanitizedValues.accessKeyId || sanitizedValues.secretAccessKey);
534
+ sanitizedValues.authMode = hasKeys ? "access_keys" : "ambient";
535
+ }
536
+ if (sanitizedValues.authMode === "ambient") {
537
+ delete sanitizedValues.accessKeyId;
538
+ delete sanitizedValues.secretAccessKey;
539
+ delete sanitizedValues.sessionToken;
540
+ }
541
+ }
520
542
  const call = await runMutationWithContext({
521
543
  actionId: "save-credentials",
522
544
  tabId: "credentials",
523
- mutationPayload: { integrationId: currentIntegrationId, credentials: values },
545
+ mutationPayload: { integrationId: currentIntegrationId, credentials: sanitizedValues },
524
546
  operation: () => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
525
547
  method: "PUT",
526
548
  headers: { "Content-Type": "application/json" },
527
- body: JSON.stringify({ credentials: values })
549
+ body: JSON.stringify({ credentials: sanitizedValues })
528
550
  }, { fallback: null })
529
551
  });
530
552
  if (call.ok) {
531
- setCredValues(values);
553
+ setCredValues(sanitizedValues);
532
554
  setCredentialsFormKey((current) => current + 1);
533
555
  flash(t("integrations.detail.credentials.saved"), "success");
534
556
  return;
@@ -619,6 +641,10 @@ function IntegrationDetailPage({ params }) {
619
641
  const credentialSchema = React.useMemo(() => z.object({}).passthrough().superRefine((rawValues, ctx) => {
620
642
  const values = rawValues;
621
643
  editableCredentialFields.forEach((field) => {
644
+ if (field.visibleWhen) {
645
+ const targetValue = values[field.visibleWhen.field];
646
+ if (targetValue !== field.visibleWhen.equals) return;
647
+ }
622
648
  const value = values[field.key];
623
649
  if (field.type === "boolean") {
624
650
  if (value !== void 0 && value !== null && typeof value !== "boolean") {