@open-mercato/core 0.10.1-develop.7841.1.af87ac614d → 0.10.1-develop.7857.1.259928eb85
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/api_keys/api/keys/route.js +15 -1
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +6 -1
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +6 -1
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +16 -1
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +11 -3
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/cli.js +2 -0
- package/dist/modules/attachments/cli.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +11 -0
- package/dist/modules/attachments/data/validators.js.map +2 -2
- package/dist/modules/attachments/index.js +10 -0
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-query.js +5 -1
- package/dist/modules/attachments/lib/access-query.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/driverFactory.js +54 -33
- package/dist/modules/attachments/lib/drivers/driverFactory.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/index.js +11 -1
- package/dist/modules/attachments/lib/drivers/index.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/storageValidation.js +85 -0
- package/dist/modules/attachments/lib/drivers/storageValidation.js.map +7 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js +3 -1
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/storageErrors.js +22 -0
- package/dist/modules/attachments/lib/storageErrors.js.map +7 -0
- package/dist/modules/auth/api/users/route.js +16 -1
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +13 -2
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +4 -2
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +4 -9
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +4 -9
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +2 -0
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +23 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +6 -9
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/backend/directory/branding/page.js +15 -3
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/wms/api/listEnrichers.js +3 -1
- package/dist/modules/wms/api/listEnrichers.js.map +2 -2
- package/dist/modules/wms/index.js +1 -1
- package/dist/modules/wms/index.js.map +2 -2
- package/jest.config.cjs +1 -1
- package/package.json +8 -8
- package/src/modules/api_keys/api/keys/route.ts +15 -1
- package/src/modules/api_keys/i18n/de.json +1 -0
- package/src/modules/api_keys/i18n/en.json +1 -0
- package/src/modules/api_keys/i18n/es.json +1 -0
- package/src/modules/api_keys/i18n/ko.json +1 -0
- package/src/modules/api_keys/i18n/pl.json +1 -0
- package/src/modules/attachments/api/file/[id]/route.ts +7 -1
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -1
- package/src/modules/attachments/api/library/[id]/route.ts +14 -1
- package/src/modules/attachments/api/route.ts +11 -3
- package/src/modules/attachments/cli.ts +2 -0
- package/src/modules/attachments/data/validators.ts +11 -0
- package/src/modules/attachments/i18n/de.json +1 -0
- package/src/modules/attachments/i18n/en.json +1 -0
- package/src/modules/attachments/i18n/es.json +1 -0
- package/src/modules/attachments/i18n/ko.json +1 -0
- package/src/modules/attachments/i18n/pl.json +1 -0
- package/src/modules/attachments/index.ts +12 -0
- package/src/modules/attachments/lib/access-query.ts +5 -1
- package/src/modules/attachments/lib/createFromBuffer.ts +2 -0
- package/src/modules/attachments/lib/drivers/driverFactory.ts +79 -55
- package/src/modules/attachments/lib/drivers/index.ts +13 -0
- package/src/modules/attachments/lib/drivers/storageValidation.ts +114 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +3 -1
- package/src/modules/attachments/lib/storageErrors.ts +21 -0
- package/src/modules/auth/api/users/route.ts +18 -1
- package/src/modules/auth/backend/users/page.tsx +14 -2
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/ko.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/catalog/commands/products.ts +4 -1
- package/src/modules/customer_accounts/AGENTS.md +1 -1
- package/src/modules/customers/api/companies/[id]/route.ts +6 -13
- package/src/modules/customers/api/people/[id]/route.ts +6 -13
- package/src/modules/customers/components/detail/TaskDialog.tsx +2 -0
- package/src/modules/customers/lib/todoCompatibility.ts +28 -0
- package/src/modules/directory/api/organization-branding/route.ts +5 -8
- package/src/modules/directory/backend/directory/branding/page.tsx +15 -3
- package/src/modules/sales/AGENTS.md +3 -3
- package/src/modules/wms/api/listEnrichers.ts +5 -2
- package/src/modules/wms/index.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/createFromBuffer.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { StorageDriverFactory } from './drivers'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { mergeAttachmentMetadata, upsertAssignment } from './metadata'\nimport { assertAttachmentScopeInvariant } from './access'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport { ensureAttachmentPartitionProtection } from './access-protection'\n\n/** DataEngine handle whose exact type is derived from `emitCrudSideEffects` (no `any`). */\ntype CrudDataEngine = Parameters<typeof emitCrudSideEffects>[0]['dataEngine']\n\nexport type CreateAttachmentFromBufferInput = {\n em: EntityManager\n /** When provided, fires CRUD side effects (events + indexing). Omit to skip. */\n dataEngine?: CrudDataEngine\n tenantId: string\n organizationId: string\n /** Domain entity this attachment is linked to (e.g. 'customers:deal'). */\n entityId: string\n recordId: string\n fileName: string\n mimeType: string\n buffer: Buffer\n /** Storage partition; defaults to the entity's default partition. */\n partitionCode?: string | null\n}\n\nexport type CreatedAttachment = {\n id: string\n fileName: string\n mimeType: string\n fileSize: number\n url: string\n}\n\n/**\n * Create a durable `Attachment` from an in-memory buffer, linked to a domain\n * record. Additive reusable seam extracted from the upload route's store block so\n * server-side producers (e.g. the agent file-plane artifact-promotion effector,\n * #12) can materialize an attachment without going through the multipart HTTP\n * route. Resolves the partition + driver, stores the bytes, persists the row\n * atomically, and (when a `dataEngine` is passed) fires CRUD side effects so the\n * attachment is indexed and its events emitted \u2014 exactly like an uploaded file.\n */\nexport async function createAttachmentFromBuffer(input: CreateAttachmentFromBufferInput): Promise<CreatedAttachment> {\n const { em } = input\n const code = (input.partitionCode && input.partitionCode.length > 0)\n ? input.partitionCode\n : resolveDefaultPartitionCode(input.entityId)\n\n let partition = await em.findOne(AttachmentPartition, { code })\n if (!partition) {\n await ensureDefaultPartitions(em)\n partition = await em.findOne(AttachmentPartition, { code })\n }\n if (!partition) throw new Error(`[internal] attachment partition \"${code}\" is not available`)\n\n await ensureAttachmentPartitionProtection(em, partition)\n\n const driver = await new StorageDriverFactory(em).resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: input.fileName,\n buffer: input.buffer,\n })\n\n const metadata = mergeAttachmentMetadata(null, {\n assignments: upsertAssignment([], { type: input.entityId, id: input.recordId }),\n })\n const attachmentId = randomUUID()\n assertAttachmentScopeInvariant({ tenantId: input.tenantId, organizationId: input.organizationId })\n const attachment = em.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: input.fileName,\n mimeType: input.mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: stored.storagePath,\n url: buildAttachmentFileUrl(attachmentId),\n content: null,\n storageMetadata: metadata,\n })\n await em.transactional(async (tx) => {\n await tx.persist(attachment).flush()\n })\n\n if (input.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: input.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n }\n\n return {\n id: attachmentId,\n fileName: input.fileName,\n mimeType: input.mimeType,\n fileSize: input.buffer.length,\n url: attachment.url,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC,SAAS,YAAY,2BAA2B;AAChD,SAAS,4BAA4B;AACrC,SAAS,yBAAyB,mCAAmC;AACrE,SAAS,8BAA8B;AACvC,SAAS,yBAAyB,wBAAwB;AAC1D,SAAS,sCAAsC;AAC/C,SAAS,sBAAsB,6BAA6B;AAC5D,SAAS,2CAA2C;AAsCpD,eAAsB,2BAA2B,OAAoE;AACnH,QAAM,EAAE,GAAG,IAAI;AACf,QAAM,OAAQ,MAAM,iBAAiB,MAAM,cAAc,SAAS,IAC9D,MAAM,gBACN,4BAA4B,MAAM,QAAQ;AAE9C,MAAI,YAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,KAAK,CAAC;AAC9D,MAAI,CAAC,WAAW;AACd,UAAM,wBAAwB,EAAE;AAChC,gBAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,KAAK,CAAC;AAAA,EAC5D;AACA,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,oCAAoC,IAAI,oBAAoB;AAE5F,QAAM,oCAAoC,IAAI,SAAS;AAEvD,QAAM,SAAS,MAAM,IAAI,qBAAqB,EAAE,EAAE,oBAAoB,UAAU,MAAM;AAAA,IACpF,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AACD,QAAM,SAAS,MAAM,OAAO,MAAM;AAAA,IAChC,eAAe,UAAU;AAAA,IACzB,OAAO,MAAM;AAAA,IACb,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,QAAQ,MAAM;AAAA,EAChB,CAAC;AAED,QAAM,WAAW,wBAAwB,MAAM;AAAA,IAC7C,aAAa,iBAAiB,CAAC,GAAG,EAAE,MAAM,MAAM,UAAU,IAAI,MAAM,SAAS,CAAC;AAAA,EAChF,CAAC;AACD,QAAM,eAAe,WAAW;AAChC,iCAA+B,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe,CAAC;AACjG,QAAM,aAAa,GAAG,OAAO,YAAY;AAAA,IACvC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM,OAAO;AAAA,IACvB,eAAe,UAAU;AAAA,IACzB,eAAe,UAAU,iBAAiB;AAAA,IAC1C,aAAa,OAAO;AAAA,IACpB,KAAK,uBAAuB,YAAY;AAAA,IACxC,SAAS;AAAA,IACT,iBAAiB;AAAA,EACnB,CAAC;AACD,QAAM,GAAG,cAAc,OAAO,OAAO;AACnC,UAAM,GAAG,QAAQ,UAAU,EAAE,MAAM;AAAA,EACrC,CAAC;AAED,MAAI,MAAM,YAAY;AACpB,UAAM,oBAAoB;AAAA,MACxB,YAAY,MAAM;AAAA,MAClB,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,WAAW;AAAA,QACf,gBAAgB,WAAW,kBAAkB;AAAA,QAC7C,UAAU,WAAW,YAAY;AAAA,MACnC;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM,OAAO;AAAA,IACvB,KAAK,WAAW;AAAA,EAClB;AACF;",
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { StorageDriverFactory } from './drivers'\nimport { assertAttachmentStoragePartitionExists } from './drivers/storageValidation'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { mergeAttachmentMetadata, upsertAssignment } from './metadata'\nimport { assertAttachmentScopeInvariant } from './access'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport { ensureAttachmentPartitionProtection } from './access-protection'\n\n/** DataEngine handle whose exact type is derived from `emitCrudSideEffects` (no `any`). */\ntype CrudDataEngine = Parameters<typeof emitCrudSideEffects>[0]['dataEngine']\n\nexport type CreateAttachmentFromBufferInput = {\n em: EntityManager\n /** When provided, fires CRUD side effects (events + indexing). Omit to skip. */\n dataEngine?: CrudDataEngine\n tenantId: string\n organizationId: string\n /** Domain entity this attachment is linked to (e.g. 'customers:deal'). */\n entityId: string\n recordId: string\n fileName: string\n mimeType: string\n buffer: Buffer\n /** Storage partition; defaults to the entity's default partition. */\n partitionCode?: string | null\n}\n\nexport type CreatedAttachment = {\n id: string\n fileName: string\n mimeType: string\n fileSize: number\n url: string\n}\n\n/**\n * Create a durable `Attachment` from an in-memory buffer, linked to a domain\n * record. Additive reusable seam extracted from the upload route's store block so\n * server-side producers (e.g. the agent file-plane artifact-promotion effector,\n * #12) can materialize an attachment without going through the multipart HTTP\n * route. Resolves the partition + driver, stores the bytes, persists the row\n * atomically, and (when a `dataEngine` is passed) fires CRUD side effects so the\n * attachment is indexed and its events emitted \u2014 exactly like an uploaded file.\n */\nexport async function createAttachmentFromBuffer(input: CreateAttachmentFromBufferInput): Promise<CreatedAttachment> {\n const { em } = input\n const code = (input.partitionCode && input.partitionCode.length > 0)\n ? input.partitionCode\n : resolveDefaultPartitionCode(input.entityId)\n\n let partition = await em.findOne(AttachmentPartition, { code })\n if (!partition) {\n assertAttachmentStoragePartitionExists(false)\n await ensureDefaultPartitions(em)\n partition = await em.findOne(AttachmentPartition, { code })\n }\n if (!partition) throw new Error(`[internal] attachment partition \"${code}\" is not available`)\n\n await ensureAttachmentPartitionProtection(em, partition)\n\n const driver = await new StorageDriverFactory(em).resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: input.fileName,\n buffer: input.buffer,\n })\n\n const metadata = mergeAttachmentMetadata(null, {\n assignments: upsertAssignment([], { type: input.entityId, id: input.recordId }),\n })\n const attachmentId = randomUUID()\n assertAttachmentScopeInvariant({ tenantId: input.tenantId, organizationId: input.organizationId })\n const attachment = em.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: input.fileName,\n mimeType: input.mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: stored.storagePath,\n url: buildAttachmentFileUrl(attachmentId),\n content: null,\n storageMetadata: metadata,\n })\n await em.transactional(async (tx) => {\n await tx.persist(attachment).flush()\n })\n\n if (input.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: input.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n }\n\n return {\n id: attachmentId,\n fileName: input.fileName,\n mimeType: input.mimeType,\n fileSize: input.buffer.length,\n url: attachment.url,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC,SAAS,YAAY,2BAA2B;AAChD,SAAS,4BAA4B;AACrC,SAAS,8CAA8C;AACvD,SAAS,yBAAyB,mCAAmC;AACrE,SAAS,8BAA8B;AACvC,SAAS,yBAAyB,wBAAwB;AAC1D,SAAS,sCAAsC;AAC/C,SAAS,sBAAsB,6BAA6B;AAC5D,SAAS,2CAA2C;AAsCpD,eAAsB,2BAA2B,OAAoE;AACnH,QAAM,EAAE,GAAG,IAAI;AACf,QAAM,OAAQ,MAAM,iBAAiB,MAAM,cAAc,SAAS,IAC9D,MAAM,gBACN,4BAA4B,MAAM,QAAQ;AAE9C,MAAI,YAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,KAAK,CAAC;AAC9D,MAAI,CAAC,WAAW;AACd,2CAAuC,KAAK;AAC5C,UAAM,wBAAwB,EAAE;AAChC,gBAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,KAAK,CAAC;AAAA,EAC5D;AACA,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,oCAAoC,IAAI,oBAAoB;AAE5F,QAAM,oCAAoC,IAAI,SAAS;AAEvD,QAAM,SAAS,MAAM,IAAI,qBAAqB,EAAE,EAAE,oBAAoB,UAAU,MAAM;AAAA,IACpF,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AACD,QAAM,SAAS,MAAM,OAAO,MAAM;AAAA,IAChC,eAAe,UAAU;AAAA,IACzB,OAAO,MAAM;AAAA,IACb,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,QAAQ,MAAM;AAAA,EAChB,CAAC;AAED,QAAM,WAAW,wBAAwB,MAAM;AAAA,IAC7C,aAAa,iBAAiB,CAAC,GAAG,EAAE,MAAM,MAAM,UAAU,IAAI,MAAM,SAAS,CAAC;AAAA,EAChF,CAAC;AACD,QAAM,eAAe,WAAW;AAChC,iCAA+B,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe,CAAC;AACjG,QAAM,aAAa,GAAG,OAAO,YAAY;AAAA,IACvC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM,OAAO;AAAA,IACvB,eAAe,UAAU;AAAA,IACzB,eAAe,UAAU,iBAAiB;AAAA,IAC1C,aAAa,OAAO;AAAA,IACpB,KAAK,uBAAuB,YAAY;AAAA,IACxC,SAAS;AAAA,IACT,iBAAiB;AAAA,EACnB,CAAC;AACD,QAAM,GAAG,cAAc,OAAO,OAAO;AACnC,UAAM,GAAG,QAAQ,UAAU,EAAE,MAAM;AAAA,EACrC,CAAC;AAED,MAAI,MAAM,YAAY;AACpB,UAAM,oBAAoB;AAAA,MACxB,YAAY,MAAM;AAAA,MAClB,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,WAAW;AAAA,QACf,gBAAgB,WAAW,kBAAkB;AAAA,QAC7C,UAAU,WAAW,YAAY;AAAA,MACnC;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM,OAAO;AAAA,IACvB,KAAK,WAAW;AAAA,EAClB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { LocalStorageDriver } from "./localDriver.js";
|
|
2
2
|
import { LegacyPublicStorageDriver } from "./legacyPublicDriver.js";
|
|
3
3
|
import { AttachmentPartition } from "../../data/entities.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
AttachmentStorageConfigurationError,
|
|
6
|
+
assertAttachmentStoragePartitionExists,
|
|
7
|
+
getAttachmentStoragePolicy,
|
|
8
|
+
validateAttachmentStorageConfiguration
|
|
9
|
+
} from "./storageValidation.js";
|
|
10
|
+
const REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/AttachmentStorageDrivers");
|
|
11
|
+
function moduleRegistry() {
|
|
12
|
+
const registry = globalThis;
|
|
13
|
+
return registry[REGISTRY_KEY] ??= { drivers: /* @__PURE__ */ new Map(), enhancers: /* @__PURE__ */ new Map() };
|
|
14
|
+
}
|
|
8
15
|
function registerExternalStorageDriver(key, factory) {
|
|
9
|
-
|
|
16
|
+
moduleRegistry().drivers.set(key, factory);
|
|
10
17
|
}
|
|
11
18
|
function registerExternalCredentialEnhancer(key, enhancer) {
|
|
12
|
-
|
|
19
|
+
moduleRegistry().enhancers.set(key, enhancer);
|
|
13
20
|
}
|
|
14
21
|
class StorageDriverFactory {
|
|
15
22
|
constructor(em) {
|
|
16
23
|
this.em = em;
|
|
17
24
|
this.cache = /* @__PURE__ */ new Map();
|
|
18
|
-
this.localDriver = new LocalStorageDriver();
|
|
19
|
-
this.legacyPublicDriver = new LegacyPublicStorageDriver();
|
|
20
25
|
this.externalDrivers = /* @__PURE__ */ new Map();
|
|
21
26
|
this.credentialEnhancers = /* @__PURE__ */ new Map();
|
|
22
27
|
}
|
|
@@ -27,38 +32,54 @@ class StorageDriverFactory {
|
|
|
27
32
|
this.credentialEnhancers.set(key, enhancer);
|
|
28
33
|
}
|
|
29
34
|
resolveForAttachment(storageDriver, configJson) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return this.localDriver;
|
|
35
|
-
default: {
|
|
36
|
-
const externalFactory = this.externalDrivers.get(storageDriver) ?? moduleDriverRegistry.get(storageDriver)?.factory;
|
|
37
|
-
if (externalFactory) {
|
|
38
|
-
const cacheKey = `${storageDriver}:${JSON.stringify(configJson ?? {})}`;
|
|
39
|
-
const cached = this.cache.get(cacheKey);
|
|
40
|
-
if (cached) return cached;
|
|
41
|
-
const driver = externalFactory(configJson ?? {});
|
|
42
|
-
this.cache.set(cacheKey, driver);
|
|
43
|
-
return driver;
|
|
44
|
-
}
|
|
45
|
-
return this.localDriver;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
35
|
+
const config = configJson ?? {};
|
|
36
|
+
this.validate(storageDriver, config, "configured");
|
|
37
|
+
this.validate(storageDriver, config, "resolved");
|
|
38
|
+
return this.resolveValidated(storageDriver, config);
|
|
48
39
|
}
|
|
49
40
|
async resolveForPartition(partitionCode, scope) {
|
|
41
|
+
getAttachmentStoragePolicy();
|
|
50
42
|
const partition = await this.em.findOne(AttachmentPartition, { code: partitionCode });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
logger.debug("Resolving storage driver for partition", { partition: partitionCode, driverKey, moduleDrivers: [...moduleDriverRegistry.keys()], instanceDrivers: [...this.externalDrivers.keys()] });
|
|
43
|
+
assertAttachmentStoragePartitionExists(Boolean(partition));
|
|
44
|
+
if (!partition) return this.localDriver ??= new LocalStorageDriver();
|
|
54
45
|
let config = partition.configJson ?? {};
|
|
55
|
-
|
|
46
|
+
this.validate(partition.storageDriver, config, "configured", partitionCode, scope);
|
|
47
|
+
const driverKey = partition.storageDriver ?? "local";
|
|
48
|
+
const activeEnhancer = this.credentialEnhancers.get(driverKey) ?? moduleRegistry().enhancers.get(driverKey);
|
|
56
49
|
if (scope && activeEnhancer) {
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
try {
|
|
51
|
+
config = await activeEnhancer(config, scope);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (getAttachmentStoragePolicy() === "strict") {
|
|
54
|
+
throw new AttachmentStorageConfigurationError("enhancement_failed");
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
this.validate(driverKey, config, "resolved", partitionCode, scope);
|
|
59
|
+
const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey);
|
|
59
60
|
if (externalFactory) return externalFactory(config);
|
|
61
|
+
} else {
|
|
62
|
+
this.validate(driverKey, config, "resolved", partitionCode, scope);
|
|
63
|
+
}
|
|
64
|
+
return this.resolveValidated(driverKey, config);
|
|
65
|
+
}
|
|
66
|
+
validate(driverKey, config, stage, partitionCode, scope) {
|
|
67
|
+
const registered = typeof driverKey === "string" && (driverKey === "local" || driverKey === "legacyPublic" || this.externalDrivers.has(driverKey) || moduleRegistry().drivers.has(driverKey));
|
|
68
|
+
validateAttachmentStorageConfiguration({ driverKey, config, stage, partitionCode, scope }, registered);
|
|
69
|
+
}
|
|
70
|
+
resolveValidated(driverKey, config) {
|
|
71
|
+
if (driverKey === "legacyPublic") return this.legacyPublicDriver ??= new LegacyPublicStorageDriver();
|
|
72
|
+
if (driverKey === "local") return this.localDriver ??= new LocalStorageDriver();
|
|
73
|
+
const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey);
|
|
74
|
+
if (externalFactory) {
|
|
75
|
+
const cacheKey = `${driverKey}:${JSON.stringify(config)}`;
|
|
76
|
+
const cached = this.cache.get(cacheKey);
|
|
77
|
+
if (cached) return cached;
|
|
78
|
+
const driver = externalFactory(config);
|
|
79
|
+
this.cache.set(cacheKey, driver);
|
|
80
|
+
return driver;
|
|
60
81
|
}
|
|
61
|
-
return this.
|
|
82
|
+
return this.localDriver ??= new LocalStorageDriver();
|
|
62
83
|
}
|
|
63
84
|
}
|
|
64
85
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/lib/drivers/driverFactory.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { StorageDriver } from './types'\nimport { LocalStorageDriver } from './localDriver'\nimport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nimport { AttachmentPartition } from '../../data/entities'\nimport {
|
|
5
|
-
"mappings": "AAEA,SAAS,0BAA0B;AACnC,SAAS,iCAAiC;AAC1C,SAAS,2BAA2B;AACpC,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { StorageDriver } from './types'\nimport { LocalStorageDriver } from './localDriver'\nimport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nimport { AttachmentPartition } from '../../data/entities'\nimport {\n AttachmentStorageConfigurationError,\n assertAttachmentStoragePartitionExists,\n getAttachmentStoragePolicy,\n validateAttachmentStorageConfiguration,\n type AttachmentStorageScope,\n type AttachmentStorageValidationStage,\n} from './storageValidation'\n\ntype DriverScope = AttachmentStorageScope\ntype CredentialEnhancer = (config: Record<string, unknown>, scope: DriverScope) => Promise<Record<string, unknown>>\ntype DriverFactory = (config: Record<string, unknown>) => StorageDriver\n\ntype ModuleRegistry = {\n drivers: Map<string, DriverFactory>\n enhancers: Map<string, CredentialEnhancer>\n}\nconst REGISTRY_KEY = Symbol.for('@open-mercato/AttachmentStorageDrivers')\n\nfunction moduleRegistry(): ModuleRegistry {\n const registry = globalThis as typeof globalThis & { [REGISTRY_KEY]?: ModuleRegistry }\n return registry[REGISTRY_KEY] ??= { drivers: new Map(), enhancers: new Map() }\n}\n\nexport function registerExternalStorageDriver(key: string, factory: DriverFactory): void {\n moduleRegistry().drivers.set(key, factory)\n}\n\nexport function registerExternalCredentialEnhancer(key: string, enhancer: CredentialEnhancer): void {\n moduleRegistry().enhancers.set(key, enhancer)\n}\n\nexport class StorageDriverFactory {\n private readonly cache = new Map<string, StorageDriver>()\n private localDriver?: LocalStorageDriver\n private legacyPublicDriver?: LegacyPublicStorageDriver\n private readonly externalDrivers = new Map<string, DriverFactory>()\n private readonly credentialEnhancers = new Map<string, CredentialEnhancer>()\n\n constructor(private readonly em: EntityManager) {}\n\n registerDriver(key: string, factory: DriverFactory): void {\n this.externalDrivers.set(key, factory)\n }\n\n registerCredentialEnhancer(key: string, enhancer: CredentialEnhancer): void {\n this.credentialEnhancers.set(key, enhancer)\n }\n\n resolveForAttachment(storageDriver: string, configJson?: Record<string, unknown> | null): StorageDriver {\n const config = configJson ?? {}\n this.validate(storageDriver, config, 'configured')\n this.validate(storageDriver, config, 'resolved')\n return this.resolveValidated(storageDriver, config)\n }\n\n async resolveForPartition(partitionCode: string, scope?: DriverScope): Promise<StorageDriver> {\n getAttachmentStoragePolicy()\n const partition = await this.em.findOne(AttachmentPartition, { code: partitionCode })\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) return this.localDriver ??= new LocalStorageDriver()\n\n let config: Record<string, unknown> = partition.configJson ?? {}\n this.validate(partition.storageDriver, config, 'configured', partitionCode, scope)\n const driverKey = partition.storageDriver ?? 'local'\n const activeEnhancer = this.credentialEnhancers.get(driverKey) ?? moduleRegistry().enhancers.get(driverKey)\n if (scope && activeEnhancer) {\n try {\n config = await activeEnhancer(config, scope)\n } catch (error) {\n if (getAttachmentStoragePolicy() === 'strict') {\n throw new AttachmentStorageConfigurationError('enhancement_failed')\n }\n throw error\n }\n this.validate(driverKey, config, 'resolved', partitionCode, scope)\n const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey)\n if (externalFactory) return externalFactory(config)\n } else {\n this.validate(driverKey, config, 'resolved', partitionCode, scope)\n }\n return this.resolveValidated(driverKey, config)\n }\n\n private validate(\n driverKey: unknown,\n config: unknown,\n stage: AttachmentStorageValidationStage,\n partitionCode?: string,\n scope?: DriverScope,\n ): void {\n const registered = typeof driverKey === 'string' && (\n driverKey === 'local' || driverKey === 'legacyPublic'\n || this.externalDrivers.has(driverKey) || moduleRegistry().drivers.has(driverKey)\n )\n validateAttachmentStorageConfiguration({ driverKey, config, stage, partitionCode, scope }, registered)\n }\n\n private resolveValidated(driverKey: string, config: Record<string, unknown>): StorageDriver {\n if (driverKey === 'legacyPublic') return this.legacyPublicDriver ??= new LegacyPublicStorageDriver()\n if (driverKey === 'local') return this.localDriver ??= new LocalStorageDriver()\n const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey)\n if (externalFactory) {\n const cacheKey = `${driverKey}:${JSON.stringify(config)}`\n const cached = this.cache.get(cacheKey)\n if (cached) return cached\n const driver = externalFactory(config)\n this.cache.set(cacheKey, driver)\n return driver\n }\n return this.localDriver ??= new LocalStorageDriver()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { LocalStorageDriver } from "./localDriver.js";
|
|
2
2
|
import { LegacyPublicStorageDriver } from "./legacyPublicDriver.js";
|
|
3
3
|
import { StorageDriverFactory, registerExternalStorageDriver, registerExternalCredentialEnhancer } from "./driverFactory.js";
|
|
4
|
+
import {
|
|
5
|
+
AttachmentStorageConfigurationError,
|
|
6
|
+
getAttachmentStoragePolicy,
|
|
7
|
+
isAttachmentStorageConfigurationError,
|
|
8
|
+
registerStorageDriverValidator
|
|
9
|
+
} from "./storageValidation.js";
|
|
4
10
|
export {
|
|
11
|
+
AttachmentStorageConfigurationError,
|
|
5
12
|
LegacyPublicStorageDriver,
|
|
6
13
|
LocalStorageDriver,
|
|
7
14
|
StorageDriverFactory,
|
|
15
|
+
getAttachmentStoragePolicy,
|
|
16
|
+
isAttachmentStorageConfigurationError,
|
|
8
17
|
registerExternalCredentialEnhancer,
|
|
9
|
-
registerExternalStorageDriver
|
|
18
|
+
registerExternalStorageDriver,
|
|
19
|
+
registerStorageDriverValidator
|
|
10
20
|
};
|
|
11
21
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/lib/drivers/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type { PrepareFilePayload, StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\nexport { LocalStorageDriver } from './localDriver'\nexport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nexport { StorageDriverFactory, registerExternalStorageDriver, registerExternalCredentialEnhancer } from './driverFactory'\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,iCAAiC;AAC1C,SAAS,sBAAsB,+BAA+B,0CAA0C;",
|
|
4
|
+
"sourcesContent": ["export type { PrepareFilePayload, StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\nexport { LocalStorageDriver } from './localDriver'\nexport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nexport { StorageDriverFactory, registerExternalStorageDriver, registerExternalCredentialEnhancer } from './driverFactory'\n\nexport {\n AttachmentStorageConfigurationError,\n getAttachmentStoragePolicy,\n isAttachmentStorageConfigurationError,\n registerStorageDriverValidator,\n type AttachmentStoragePolicy,\n type AttachmentStorageScope,\n type AttachmentStorageValidationStage,\n type AttachmentStorageValidationContext,\n type AttachmentStorageValidator,\n type AttachmentStorageConfigurationReason,\n} from './storageValidation'\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,iCAAiC;AAC1C,SAAS,sBAAsB,+BAA+B,0CAA0C;AAExG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAOK;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var _a, _b;
|
|
2
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
3
|
+
import { attachmentStorageConfigurationSchema, attachmentStorageScopeSchema } from "../../data/validators.js";
|
|
4
|
+
const ERROR_MARKER = /* @__PURE__ */ Symbol.for("@open-mercato/AttachmentStorageConfigurationError");
|
|
5
|
+
const VALIDATORS_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/AttachmentStorageValidators");
|
|
6
|
+
function validators() {
|
|
7
|
+
const registry = globalThis;
|
|
8
|
+
return registry[VALIDATORS_KEY] ??= /* @__PURE__ */ new Map();
|
|
9
|
+
}
|
|
10
|
+
class AttachmentStorageConfigurationError extends (_b = CrudHttpError, _a = ERROR_MARKER, _b) {
|
|
11
|
+
constructor(reason) {
|
|
12
|
+
super(503, {
|
|
13
|
+
error: "attachments.errors.storageConfigurationInvalid",
|
|
14
|
+
code: "ATTACHMENT_STORAGE_CONFIGURATION_INVALID",
|
|
15
|
+
reason
|
|
16
|
+
});
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
this[_a] = true;
|
|
19
|
+
this.code = "ATTACHMENT_STORAGE_CONFIGURATION_INVALID";
|
|
20
|
+
this.name = "AttachmentStorageConfigurationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isAttachmentStorageConfigurationError(error) {
|
|
24
|
+
return Boolean(error) && typeof error === "object" && error[ERROR_MARKER] === true;
|
|
25
|
+
}
|
|
26
|
+
function getAttachmentStoragePolicy() {
|
|
27
|
+
const policy = process.env.OM_ATTACHMENT_STORAGE_POLICY?.trim();
|
|
28
|
+
if (!policy || policy === "legacy") return "legacy";
|
|
29
|
+
if (policy === "strict") return "strict";
|
|
30
|
+
throw new AttachmentStorageConfigurationError("invalid_policy");
|
|
31
|
+
}
|
|
32
|
+
function registerStorageDriverValidator(key, validator) {
|
|
33
|
+
const registry = validators();
|
|
34
|
+
registry.set(key, validator);
|
|
35
|
+
return () => {
|
|
36
|
+
if (registry.get(key) === validator) registry.delete(key);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function assertAttachmentStoragePartitionExists(exists) {
|
|
40
|
+
if (getAttachmentStoragePolicy() === "strict" && !exists) {
|
|
41
|
+
throw new AttachmentStorageConfigurationError("partition_missing");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function validateAttachmentStorageConfiguration(context, registered) {
|
|
45
|
+
if (getAttachmentStoragePolicy() === "legacy") return;
|
|
46
|
+
if (typeof context.driverKey !== "string" || !context.driverKey.trim()) {
|
|
47
|
+
throw new AttachmentStorageConfigurationError("driver_missing");
|
|
48
|
+
}
|
|
49
|
+
if (!registered) throw new AttachmentStorageConfigurationError("unknown_driver");
|
|
50
|
+
if (!attachmentStorageConfigurationSchema.safeParse(context.config).success) {
|
|
51
|
+
throw new AttachmentStorageConfigurationError("invalid_config");
|
|
52
|
+
}
|
|
53
|
+
if (context.scope && !attachmentStorageScopeSchema.safeParse(context.scope).success) {
|
|
54
|
+
throw new AttachmentStorageConfigurationError("invalid_scope");
|
|
55
|
+
}
|
|
56
|
+
const validator = validators().get(context.driverKey);
|
|
57
|
+
if (!validator) {
|
|
58
|
+
if (context.driverKey === "local" || context.driverKey === "legacyPublic") return;
|
|
59
|
+
throw new AttachmentStorageConfigurationError("validator_missing");
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const accepted = validator({
|
|
63
|
+
...context,
|
|
64
|
+
driverKey: context.driverKey,
|
|
65
|
+
config: context.config
|
|
66
|
+
});
|
|
67
|
+
if (accepted !== true) {
|
|
68
|
+
if (accepted && typeof accepted === "object" && "then" in accepted) {
|
|
69
|
+
void Promise.resolve(accepted).catch(() => void 0);
|
|
70
|
+
}
|
|
71
|
+
throw new AttachmentStorageConfigurationError("validator_rejected");
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
throw new AttachmentStorageConfigurationError("validator_rejected");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
AttachmentStorageConfigurationError,
|
|
79
|
+
assertAttachmentStoragePartitionExists,
|
|
80
|
+
getAttachmentStoragePolicy,
|
|
81
|
+
isAttachmentStorageConfigurationError,
|
|
82
|
+
registerStorageDriverValidator,
|
|
83
|
+
validateAttachmentStorageConfiguration
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=storageValidation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/attachments/lib/drivers/storageValidation.ts"],
|
|
4
|
+
"sourcesContent": ["import type { z } from 'zod'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { attachmentStorageConfigurationSchema, attachmentStorageScopeSchema } from '../../data/validators'\n\nexport type AttachmentStoragePolicy = 'legacy' | 'strict'\nexport type AttachmentStorageScope = z.infer<typeof attachmentStorageScopeSchema>\nexport type AttachmentStorageValidationStage = 'configured' | 'resolved'\nexport type AttachmentStorageValidationContext = {\n readonly driverKey: string\n readonly config: Readonly<Record<string, unknown>>\n readonly partitionCode?: string\n readonly scope?: Readonly<AttachmentStorageScope>\n readonly stage: AttachmentStorageValidationStage\n}\nexport type AttachmentStorageValidator = (context: AttachmentStorageValidationContext) => boolean\nexport type AttachmentStorageConfigurationReason =\n | 'invalid_policy'\n | 'partition_missing'\n | 'driver_missing'\n | 'unknown_driver'\n | 'invalid_config'\n | 'invalid_scope'\n | 'validator_missing'\n | 'validator_rejected'\n | 'enhancement_failed'\n\nconst ERROR_MARKER = Symbol.for('@open-mercato/AttachmentStorageConfigurationError')\nconst VALIDATORS_KEY = Symbol.for('@open-mercato/AttachmentStorageValidators')\ntype ValidatorRegistry = Map<string, AttachmentStorageValidator>\n\nfunction validators(): ValidatorRegistry {\n const registry = globalThis as typeof globalThis & { [VALIDATORS_KEY]?: ValidatorRegistry }\n return registry[VALIDATORS_KEY] ??= new Map()\n}\n\nexport class AttachmentStorageConfigurationError extends CrudHttpError {\n readonly [ERROR_MARKER] = true\n readonly code = 'ATTACHMENT_STORAGE_CONFIGURATION_INVALID'\n\n constructor(readonly reason: AttachmentStorageConfigurationReason) {\n super(503, {\n error: 'attachments.errors.storageConfigurationInvalid',\n code: 'ATTACHMENT_STORAGE_CONFIGURATION_INVALID',\n reason,\n })\n this.name = 'AttachmentStorageConfigurationError'\n }\n}\n\nexport function isAttachmentStorageConfigurationError(error: unknown): error is AttachmentStorageConfigurationError {\n return Boolean(error) && typeof error === 'object'\n && (error as Record<symbol, unknown>)[ERROR_MARKER] === true\n}\n\nexport function getAttachmentStoragePolicy(): AttachmentStoragePolicy {\n const policy = process.env.OM_ATTACHMENT_STORAGE_POLICY?.trim()\n if (!policy || policy === 'legacy') return 'legacy'\n if (policy === 'strict') return 'strict'\n throw new AttachmentStorageConfigurationError('invalid_policy')\n}\n\nexport function registerStorageDriverValidator(key: string, validator: AttachmentStorageValidator): () => void {\n const registry = validators()\n registry.set(key, validator)\n return () => {\n if (registry.get(key) === validator) registry.delete(key)\n }\n}\n\nexport function assertAttachmentStoragePartitionExists(exists: boolean): void {\n if (getAttachmentStoragePolicy() === 'strict' && !exists) {\n throw new AttachmentStorageConfigurationError('partition_missing')\n }\n}\n\nexport function validateAttachmentStorageConfiguration(\n context: Omit<AttachmentStorageValidationContext, 'driverKey' | 'config'> & {\n driverKey: unknown\n config: unknown\n },\n registered: boolean,\n): void {\n if (getAttachmentStoragePolicy() === 'legacy') return\n if (typeof context.driverKey !== 'string' || !context.driverKey.trim()) {\n throw new AttachmentStorageConfigurationError('driver_missing')\n }\n if (!registered) throw new AttachmentStorageConfigurationError('unknown_driver')\n if (!attachmentStorageConfigurationSchema.safeParse(context.config).success) {\n throw new AttachmentStorageConfigurationError('invalid_config')\n }\n if (context.scope && !attachmentStorageScopeSchema.safeParse(context.scope).success) {\n throw new AttachmentStorageConfigurationError('invalid_scope')\n }\n const validator = validators().get(context.driverKey)\n if (!validator) {\n if (context.driverKey === 'local' || context.driverKey === 'legacyPublic') return\n throw new AttachmentStorageConfigurationError('validator_missing')\n }\n try {\n const accepted: unknown = validator({\n ...context,\n driverKey: context.driverKey,\n config: context.config as Record<string, unknown>,\n })\n if (accepted !== true) {\n if (accepted && typeof accepted === 'object' && 'then' in accepted) {\n void Promise.resolve(accepted).catch(() => undefined)\n }\n throw new AttachmentStorageConfigurationError('validator_rejected')\n }\n } catch {\n throw new AttachmentStorageConfigurationError('validator_rejected')\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AACA,SAAS,qBAAqB;AAC9B,SAAS,sCAAsC,oCAAoC;AAwBnF,MAAM,eAAe,uBAAO,IAAI,mDAAmD;AACnF,MAAM,iBAAiB,uBAAO,IAAI,2CAA2C;AAG7E,SAAS,aAAgC;AACvC,QAAM,WAAW;AACjB,SAAO,SAAS,cAAc,MAAM,oBAAI,IAAI;AAC9C;AAEO,MAAM,6CAA4C,oBAC7C,mBAD6C,IAAc;AAAA,EAIrE,YAAqB,QAA8C;AACjE,UAAM,KAAK;AAAA,MACT,OAAO;AAAA,MACP,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AALkB;AAHrB,SAAU,MAAgB;AAC1B,SAAS,OAAO;AAQd,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,sCAAsC,OAA8D;AAClH,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YACpC,MAAkC,YAAY,MAAM;AAC5D;AAEO,SAAS,6BAAsD;AACpE,QAAM,SAAS,QAAQ,IAAI,8BAA8B,KAAK;AAC9D,MAAI,CAAC,UAAU,WAAW,SAAU,QAAO;AAC3C,MAAI,WAAW,SAAU,QAAO;AAChC,QAAM,IAAI,oCAAoC,gBAAgB;AAChE;AAEO,SAAS,+BAA+B,KAAa,WAAmD;AAC7G,QAAM,WAAW,WAAW;AAC5B,WAAS,IAAI,KAAK,SAAS;AAC3B,SAAO,MAAM;AACX,QAAI,SAAS,IAAI,GAAG,MAAM,UAAW,UAAS,OAAO,GAAG;AAAA,EAC1D;AACF;AAEO,SAAS,uCAAuC,QAAuB;AAC5E,MAAI,2BAA2B,MAAM,YAAY,CAAC,QAAQ;AACxD,UAAM,IAAI,oCAAoC,mBAAmB;AAAA,EACnE;AACF;AAEO,SAAS,uCACd,SAIA,YACM;AACN,MAAI,2BAA2B,MAAM,SAAU;AAC/C,MAAI,OAAO,QAAQ,cAAc,YAAY,CAAC,QAAQ,UAAU,KAAK,GAAG;AACtE,UAAM,IAAI,oCAAoC,gBAAgB;AAAA,EAChE;AACA,MAAI,CAAC,WAAY,OAAM,IAAI,oCAAoC,gBAAgB;AAC/E,MAAI,CAAC,qCAAqC,UAAU,QAAQ,MAAM,EAAE,SAAS;AAC3E,UAAM,IAAI,oCAAoC,gBAAgB;AAAA,EAChE;AACA,MAAI,QAAQ,SAAS,CAAC,6BAA6B,UAAU,QAAQ,KAAK,EAAE,SAAS;AACnF,UAAM,IAAI,oCAAoC,eAAe;AAAA,EAC/D;AACA,QAAM,YAAY,WAAW,EAAE,IAAI,QAAQ,SAAS;AACpD,MAAI,CAAC,WAAW;AACd,QAAI,QAAQ,cAAc,WAAW,QAAQ,cAAc,eAAgB;AAC3E,UAAM,IAAI,oCAAoC,mBAAmB;AAAA,EACnE;AACA,MAAI;AACF,UAAM,WAAoB,UAAU;AAAA,MAClC,GAAG;AAAA,MACH,WAAW,QAAQ;AAAA,MACnB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AACD,QAAI,aAAa,MAAM;AACrB,UAAI,YAAY,OAAO,aAAa,YAAY,UAAU,UAAU;AAClE,aAAK,QAAQ,QAAQ,QAAQ,EAAE,MAAM,MAAM,MAAS;AAAA,MACtD;AACA,YAAM,IAAI,oCAAoC,oBAAoB;AAAA,IACpE;AAAA,EACF,QAAQ;AACN,UAAM,IAAI,oCAAoC,oBAAoB;AAAA,EACpE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -5,6 +5,7 @@ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
|
5
5
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
6
6
|
import { Attachment, AttachmentPartition } from "../data/entities.js";
|
|
7
7
|
import { attachmentCrudEvents, attachmentCrudIndexer } from "./crud.js";
|
|
8
|
+
import { assertAttachmentStoragePartitionExists, getAttachmentStoragePolicy } from "./drivers/storageValidation.js";
|
|
8
9
|
import { buildAttachmentFileUrl } from "./imageUrls.js";
|
|
9
10
|
import { ensureAttachmentPartitionProtection } from "./access-protection.js";
|
|
10
11
|
import { ensureDefaultPartitions, resolveDefaultPartitionCode } from "./partitions.js";
|
|
@@ -54,7 +55,7 @@ class ScopedAttachmentUploadService {
|
|
|
54
55
|
throw new ScopedAttachmentUploadError("active_content", 400);
|
|
55
56
|
}
|
|
56
57
|
const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps;
|
|
57
|
-
await ensureDefaultPartitions(em);
|
|
58
|
+
if (getAttachmentStoragePolicy() === "legacy") await ensureDefaultPartitions(em);
|
|
58
59
|
const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId);
|
|
59
60
|
const partition = await em.findOne(AttachmentPartition, {
|
|
60
61
|
code: partitionCode,
|
|
@@ -63,6 +64,7 @@ class ScopedAttachmentUploadService {
|
|
|
63
64
|
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
64
65
|
]
|
|
65
66
|
});
|
|
67
|
+
assertAttachmentStoragePartitionExists(Boolean(partition));
|
|
66
68
|
if (!partition) throw new ScopedAttachmentUploadError("partition_unavailable", 400);
|
|
67
69
|
if (input.requirePrivatePartition && partition.isPublic) {
|
|
68
70
|
throw new ScopedAttachmentUploadError("partition_unavailable", 403);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/scoped-upload-service.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport type { StorageDriverFactory } from './drivers'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { ensureAttachmentPartitionProtection } from './access-protection'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { extractAttachmentContent } from './textExtraction'\nimport { requestOcrProcessing } from './ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from './ocrService'\nimport { assertAttachmentScopeInvariant } from './access'\nimport {\n mergeAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from './metadata'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from './security'\nimport { resolveAttachmentMaxBytes } from './upload-limits'\nimport type { AttachmentQuotaService } from './quota-service'\n\nconst logger = createLogger('attachments')\n\nexport type ScopedAttachmentUploadErrorCode =\n | 'dangerous_executable'\n | 'max_upload_size'\n | 'active_content'\n | 'partition_unavailable'\n | 'quota_exceeded'\n | 'quota_target_exists'\n | 'quota_unavailable'\n | 'quota_recovery_unsupported'\n | 'storage_failed'\n | 'persistence_failed'\n\n// Use Symbol.for so the marker survives module duplication across bundle\n// boundaries. The production build emits this module into several server\n// chunks, so a consumer's `instanceof` compares against a different copy of the\n// class than the DI-resolved service threw from: the check silently fails, the\n// error escapes the caller's catch, and a deliberate 400/413 surfaces as a 500.\nconst SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER = Symbol.for('@open-mercato/ScopedAttachmentUploadError')\n\nexport class ScopedAttachmentUploadError extends Error {\n readonly [SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] = true\n\n constructor(\n public readonly code: ScopedAttachmentUploadErrorCode,\n public readonly status: number,\n ) {\n super(code)\n this.name = 'ScopedAttachmentUploadError'\n }\n}\n\n/**\n * Bundle-safe check for `ScopedAttachmentUploadError`. Prefer this over\n * `instanceof` at every call site \u2014 the class is duplicated across server\n * chunks, so `instanceof` is only reliable inside this module.\n */\nexport function isScopedAttachmentUploadError(error: unknown): error is ScopedAttachmentUploadError {\n return Boolean(error)\n && typeof error === 'object'\n && (error as Record<symbol, unknown>)[SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] === true\n}\n\nexport type ScopedAttachmentUploadInput = {\n tenantId: string\n organizationId: string\n entityId: string\n recordId: string\n fileName: string\n declaredMimeType?: string | null\n buffer: Buffer\n tags?: string[]\n assignments?: AttachmentAssignment[]\n partitionCode?: string | null\n maxBytes?: number\n /**\n * Reject a partition flagged public, so a module-owned upload (documents,\n * warranty claims) never lands where an anonymous reader can reach it. The\n * tenant/organization half of that guarantee is not this flag's job: the\n * partition lookup below already matches only a global partition or one owned\n * by this tenant and organization, whether or not the flag is set.\n */\n requirePrivatePartition?: boolean\n /**\n * Persists a module-owned link row inside the same transaction as the\n * Attachment, so a link failure cannot leave a committed orphan attachment.\n * The callback receives only the generated id, never the entity or the\n * storage implementation.\n */\n persistLink?: (tx: EntityManager, attachmentId: string) => Promise<void> | void\n}\n\ntype QuotaRecoveryScheduler = (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n) => Promise<void>\n\nexport class ScopedAttachmentUploadService {\n constructor(private readonly deps: {\n em: EntityManager\n dataEngine?: DataEngine | null\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: QuotaRecoveryScheduler\n }) {}\n\n async upload(input: ScopedAttachmentUploadInput): Promise<Attachment> {\n const safeName = sanitizeUploadedFileName(input.fileName)\n if (hasDangerousExecutableExtension(safeName)) {\n throw new ScopedAttachmentUploadError('dangerous_executable', 400)\n }\n if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {\n throw new ScopedAttachmentUploadError('max_upload_size', 413)\n }\n const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? '')\n if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {\n throw new ScopedAttachmentUploadError('active_content', 400)\n }\n\n const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps\n await ensureDefaultPartitions(em)\n const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId)\n const partition = await em.findOne(AttachmentPartition, {\n code: partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n if (!partition) throw new ScopedAttachmentUploadError('partition_unavailable', 400)\n if (input.requirePrivatePartition && partition.isPublic) {\n throw new ScopedAttachmentUploadError('partition_unavailable', 403)\n }\n\n await ensureAttachmentPartitionProtection(em, partition)\n const driver = await storageDriverFactory.resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const preparedStoragePath = driver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n })\n if (!preparedStoragePath) {\n throw new ScopedAttachmentUploadError('quota_recovery_unsupported', 500)\n }\n\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n try {\n reservation = await attachmentQuotaService.reserve({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n bytes: input.buffer.length,\n source: 'attachment',\n storageDriver: driver.key,\n storagePath: preparedStoragePath,\n partitionCode: partition.code,\n })\n await attachmentQuotaRecoveryScheduler({\n reservationId: reservation.id,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => undefined)\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') throw new ScopedAttachmentUploadError('quota_exceeded', 413)\n if (code === 'quota_target_exists') throw new ScopedAttachmentUploadError('quota_target_exists', 409)\n throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n }\n\n if (!reservation) throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n\n let storedPath = preparedStoragePath\n try {\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n buffer: input.buffer,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken)\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment storage failed', { err: error })\n throw new ScopedAttachmentUploadError('storage_failed', 500)\n }\n\n let extractedContent: string | null = null\n const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n if (partition.requiresOcr && !useLlmOcr) {\n try {\n const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({ filePath, mimeType })\n } finally {\n await cleanup().catch(() => undefined)\n }\n } catch (error) {\n logger.error('Scoped attachment text extraction failed', { err: error })\n }\n }\n\n let assignments = input.assignments?.slice() ?? []\n assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId })\n const metadata = mergeAttachmentMetadata(null, {\n assignments,\n tags: input.tags ?? [],\n })\n const attachmentId = randomUUID()\n let attachment!: Attachment\n\n try {\n await em.transactional(async (tx) => {\n attachment = tx.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId })\n await tx.persist(attachment).flush()\n await input.persistLink?.(tx, attachmentId)\n await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx)\n })\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment persistence failed', { err: error })\n // `persistLink` is where callers run their own authorization (the\n // documents module re-checks the edit capability inside this\n // transaction), so a deliberate HTTP error from it must reach the route\n // with its own status. Flattening it to 500 turns a documented 403 into\n // an apparent server fault.\n if (isCrudHttpError(error)) throw error\n throw new ScopedAttachmentUploadError('persistence_failed', 500)\n }\n\n if (useLlmOcr) {\n try {\n await requestOcrProcessing(em, attachment, driver, storedPath)\n } catch (error) {\n logger.error('Scoped attachment OCR scheduling failed', { err: error })\n }\n }\n if (this.deps.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: this.deps.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await this.deps.dataEngine.flushOrmEntityChanges()\n }\n return attachment\n }\n\n private async compensateStorage(\n driver: Awaited<ReturnType<StorageDriverFactory['resolveForPartition']>>,\n partitionCode: string,\n storagePath: string,\n reservation: { id: string; leaseToken: string },\n ): Promise<void> {\n try {\n await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath))\n await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken)\n } catch (error) {\n logger.error('Scoped attachment compensation failed', { err: error })\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA;AAAA,SAAS,kBAAkB;AAG3B,SAAS,2BAA2B;AACpC,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,YAAY,2BAA2B;AAChD,SAAS,sBAAsB,6BAA6B;AAE5D,SAAS,8BAA8B;AACvC,SAAS,2CAA2C;AACpD,SAAS,yBAAyB,mCAAmC;AACrE,SAAS,gCAAgC;AACzC,SAAS,4BAA4B;AACrC,SAAS,YAAY,uBAAuB;AAC5C,SAAS,sCAAsC;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAG1C,MAAM,SAAS,aAAa,aAAa;AAmBzC,MAAM,wCAAwC,uBAAO,IAAI,2CAA2C;AAE7F,MAAM,qCAAoC,YACrC,4CADqC,IAAM;AAAA,EAGrD,YACkB,MACA,QAChB;AACA,UAAM,IAAI;AAHM;AACA;AAJlB,SAAU,MAAyC;AAOjD,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,8BAA8B,OAAsD;AAClG,SAAO,QAAQ,KAAK,KACf,OAAO,UAAU,YAChB,MAAkC,qCAAqC,MAAM;AACrF;AAoCO,MAAM,8BAA8B;AAAA,EACzC,YAA6B,MAM1B;AAN0B;AAAA,EAMzB;AAAA,EAEJ,MAAM,OAAO,OAAyD;AACpE,UAAM,WAAW,yBAAyB,MAAM,QAAQ;AACxD,QAAI,gCAAgC,QAAQ,GAAG;AAC7C,YAAM,IAAI,4BAA4B,wBAAwB,GAAG;AAAA,IACnE;AACA,QAAI,MAAM,OAAO,UAAU,MAAM,YAAY,0BAA0B,IAAI;AACzE,YAAM,IAAI,4BAA4B,mBAAmB,GAAG;AAAA,IAC9D;AACA,UAAM,WAAW,yBAAyB,MAAM,QAAQ,UAAU,MAAM,oBAAoB,EAAE;AAC9F,QAAI,0BAA0B,MAAM,QAAQ,UAAU,QAAQ,GAAG;AAC/D,YAAM,IAAI,4BAA4B,kBAAkB,GAAG;AAAA,IAC7D;AAEA,UAAM,EAAE,IAAI,sBAAsB,wBAAwB,iCAAiC,IAAI,KAAK;AACpG,
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport type { StorageDriverFactory } from './drivers'\nimport { assertAttachmentStoragePartitionExists, getAttachmentStoragePolicy } from './drivers/storageValidation'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { ensureAttachmentPartitionProtection } from './access-protection'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { extractAttachmentContent } from './textExtraction'\nimport { requestOcrProcessing } from './ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from './ocrService'\nimport { assertAttachmentScopeInvariant } from './access'\nimport {\n mergeAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from './metadata'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from './security'\nimport { resolveAttachmentMaxBytes } from './upload-limits'\nimport type { AttachmentQuotaService } from './quota-service'\n\nconst logger = createLogger('attachments')\n\nexport type ScopedAttachmentUploadErrorCode =\n | 'dangerous_executable'\n | 'max_upload_size'\n | 'active_content'\n | 'partition_unavailable'\n | 'quota_exceeded'\n | 'quota_target_exists'\n | 'quota_unavailable'\n | 'quota_recovery_unsupported'\n | 'storage_failed'\n | 'persistence_failed'\n\n// Use Symbol.for so the marker survives module duplication across bundle\n// boundaries. The production build emits this module into several server\n// chunks, so a consumer's `instanceof` compares against a different copy of the\n// class than the DI-resolved service threw from: the check silently fails, the\n// error escapes the caller's catch, and a deliberate 400/413 surfaces as a 500.\nconst SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER = Symbol.for('@open-mercato/ScopedAttachmentUploadError')\n\nexport class ScopedAttachmentUploadError extends Error {\n readonly [SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] = true\n\n constructor(\n public readonly code: ScopedAttachmentUploadErrorCode,\n public readonly status: number,\n ) {\n super(code)\n this.name = 'ScopedAttachmentUploadError'\n }\n}\n\n/**\n * Bundle-safe check for `ScopedAttachmentUploadError`. Prefer this over\n * `instanceof` at every call site \u2014 the class is duplicated across server\n * chunks, so `instanceof` is only reliable inside this module.\n */\nexport function isScopedAttachmentUploadError(error: unknown): error is ScopedAttachmentUploadError {\n return Boolean(error)\n && typeof error === 'object'\n && (error as Record<symbol, unknown>)[SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] === true\n}\n\nexport type ScopedAttachmentUploadInput = {\n tenantId: string\n organizationId: string\n entityId: string\n recordId: string\n fileName: string\n declaredMimeType?: string | null\n buffer: Buffer\n tags?: string[]\n assignments?: AttachmentAssignment[]\n partitionCode?: string | null\n maxBytes?: number\n /**\n * Reject a partition flagged public, so a module-owned upload (documents,\n * warranty claims) never lands where an anonymous reader can reach it. The\n * tenant/organization half of that guarantee is not this flag's job: the\n * partition lookup below already matches only a global partition or one owned\n * by this tenant and organization, whether or not the flag is set.\n */\n requirePrivatePartition?: boolean\n /**\n * Persists a module-owned link row inside the same transaction as the\n * Attachment, so a link failure cannot leave a committed orphan attachment.\n * The callback receives only the generated id, never the entity or the\n * storage implementation.\n */\n persistLink?: (tx: EntityManager, attachmentId: string) => Promise<void> | void\n}\n\ntype QuotaRecoveryScheduler = (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n) => Promise<void>\n\nexport class ScopedAttachmentUploadService {\n constructor(private readonly deps: {\n em: EntityManager\n dataEngine?: DataEngine | null\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: QuotaRecoveryScheduler\n }) {}\n\n async upload(input: ScopedAttachmentUploadInput): Promise<Attachment> {\n const safeName = sanitizeUploadedFileName(input.fileName)\n if (hasDangerousExecutableExtension(safeName)) {\n throw new ScopedAttachmentUploadError('dangerous_executable', 400)\n }\n if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {\n throw new ScopedAttachmentUploadError('max_upload_size', 413)\n }\n const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? '')\n if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {\n throw new ScopedAttachmentUploadError('active_content', 400)\n }\n\n const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps\n if (getAttachmentStoragePolicy() === 'legacy') await ensureDefaultPartitions(em)\n const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId)\n const partition = await em.findOne(AttachmentPartition, {\n code: partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) throw new ScopedAttachmentUploadError('partition_unavailable', 400)\n if (input.requirePrivatePartition && partition.isPublic) {\n throw new ScopedAttachmentUploadError('partition_unavailable', 403)\n }\n\n await ensureAttachmentPartitionProtection(em, partition)\n const driver = await storageDriverFactory.resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const preparedStoragePath = driver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n })\n if (!preparedStoragePath) {\n throw new ScopedAttachmentUploadError('quota_recovery_unsupported', 500)\n }\n\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n try {\n reservation = await attachmentQuotaService.reserve({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n bytes: input.buffer.length,\n source: 'attachment',\n storageDriver: driver.key,\n storagePath: preparedStoragePath,\n partitionCode: partition.code,\n })\n await attachmentQuotaRecoveryScheduler({\n reservationId: reservation.id,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => undefined)\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') throw new ScopedAttachmentUploadError('quota_exceeded', 413)\n if (code === 'quota_target_exists') throw new ScopedAttachmentUploadError('quota_target_exists', 409)\n throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n }\n\n if (!reservation) throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n\n let storedPath = preparedStoragePath\n try {\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n buffer: input.buffer,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken)\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment storage failed', { err: error })\n throw new ScopedAttachmentUploadError('storage_failed', 500)\n }\n\n let extractedContent: string | null = null\n const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n if (partition.requiresOcr && !useLlmOcr) {\n try {\n const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({ filePath, mimeType })\n } finally {\n await cleanup().catch(() => undefined)\n }\n } catch (error) {\n logger.error('Scoped attachment text extraction failed', { err: error })\n }\n }\n\n let assignments = input.assignments?.slice() ?? []\n assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId })\n const metadata = mergeAttachmentMetadata(null, {\n assignments,\n tags: input.tags ?? [],\n })\n const attachmentId = randomUUID()\n let attachment!: Attachment\n\n try {\n await em.transactional(async (tx) => {\n attachment = tx.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId })\n await tx.persist(attachment).flush()\n await input.persistLink?.(tx, attachmentId)\n await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx)\n })\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment persistence failed', { err: error })\n // `persistLink` is where callers run their own authorization (the\n // documents module re-checks the edit capability inside this\n // transaction), so a deliberate HTTP error from it must reach the route\n // with its own status. Flattening it to 500 turns a documented 403 into\n // an apparent server fault.\n if (isCrudHttpError(error)) throw error\n throw new ScopedAttachmentUploadError('persistence_failed', 500)\n }\n\n if (useLlmOcr) {\n try {\n await requestOcrProcessing(em, attachment, driver, storedPath)\n } catch (error) {\n logger.error('Scoped attachment OCR scheduling failed', { err: error })\n }\n }\n if (this.deps.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: this.deps.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await this.deps.dataEngine.flushOrmEntityChanges()\n }\n return attachment\n }\n\n private async compensateStorage(\n driver: Awaited<ReturnType<StorageDriverFactory['resolveForPartition']>>,\n partitionCode: string,\n storagePath: string,\n reservation: { id: string; leaseToken: string },\n ): Promise<void> {\n try {\n await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath))\n await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken)\n } catch (error) {\n logger.error('Scoped attachment compensation failed', { err: error })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
3
|
+
import { isAttachmentStorageConfigurationError } from "./drivers/storageValidation.js";
|
|
4
|
+
function withAttachmentStorageErrors(handler) {
|
|
5
|
+
return async (...args) => {
|
|
6
|
+
try {
|
|
7
|
+
return await handler(...args);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (!isAttachmentStorageConfigurationError(error)) throw error;
|
|
10
|
+
const { t } = await resolveTranslations();
|
|
11
|
+
return NextResponse.json({
|
|
12
|
+
error: t("attachments.errors.storageConfigurationInvalid", "Attachment storage configuration is unavailable."),
|
|
13
|
+
code: error.code,
|
|
14
|
+
reason: error.reason
|
|
15
|
+
}, { status: 503 });
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
withAttachmentStorageErrors
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=storageErrors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/attachments/lib/storageErrors.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isAttachmentStorageConfigurationError } from './drivers/storageValidation'\n\nexport function withAttachmentStorageErrors<Args extends unknown[]>(\n handler: (...args: Args) => Promise<Response>,\n): (...args: Args) => Promise<Response> {\n return async (...args) => {\n try {\n return await handler(...args)\n } catch (error) {\n if (!isAttachmentStorageConfigurationError(error)) throw error\n const { t } = await resolveTranslations()\n return NextResponse.json({\n error: t('attachments.errors.storageConfigurationInvalid', 'Attachment storage configuration is unavailable.'),\n code: error.code,\n reason: error.reason,\n }, { status: 503 })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,6CAA6C;AAE/C,SAAS,4BACd,SACsC;AACtC,SAAO,UAAU,SAAS;AACxB,QAAI;AACF,aAAO,MAAM,QAAQ,GAAG,IAAI;AAAA,IAC9B,SAAS,OAAO;AACd,UAAI,CAAC,sCAAsC,KAAK,EAAG,OAAM;AACzD,YAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,aAAO,aAAa,KAAK;AAAA,QACvB,OAAO,EAAE,kDAAkD,kDAAkD;AAAA,QAC7G,MAAM,MAAM;AAAA,QACZ,QAAQ,MAAM;AAAA,MAChB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpB;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -220,6 +220,21 @@ async function GET(req) {
|
|
|
220
220
|
if (superAdminUserIds.size) {
|
|
221
221
|
filters.push({ id: { $nin: Array.from(superAdminUserIds) } });
|
|
222
222
|
}
|
|
223
|
+
if (userIdFilter.kind !== "ids") {
|
|
224
|
+
const scope = await resolveOrganizationScopeForRequest({
|
|
225
|
+
container,
|
|
226
|
+
auth,
|
|
227
|
+
request: req,
|
|
228
|
+
tenantId: actorTenantId
|
|
229
|
+
});
|
|
230
|
+
effectiveSelectedOrganizationId = scope.selectedId;
|
|
231
|
+
if (Array.isArray(scope.filterIds)) {
|
|
232
|
+
if (scope.filterIds.length === 0) {
|
|
233
|
+
return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin });
|
|
234
|
+
}
|
|
235
|
+
effectiveOrganizationIds = scope.filterIds;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
223
238
|
} else {
|
|
224
239
|
const selectedTenantId = getSelectedTenantFromRequest(req);
|
|
225
240
|
if (typeof selectedTenantId === "string" && selectedTenantId.trim().length > 0) {
|
|
@@ -631,7 +646,7 @@ const openApi = {
|
|
|
631
646
|
methods: {
|
|
632
647
|
GET: {
|
|
633
648
|
summary: "List users",
|
|
634
|
-
description: "Non-superadmins with auth.users.list
|
|
649
|
+
description: "Non-superadmins with auth.users.list browse users in the selected organization and its descendants, bounded by their tenant and organization visibility, excluding superadmin accounts. Selecting all organizations remains bounded by organization visibility. Explicit id/ids lookups retain tenant-wide staff-reference visibility, excluding superadmin accounts. Directory visibility does not grant access to organization-owned records. Super administrators may scope the response via the topbar tenant and organization selection; without a selected tenant, their list can span tenants. Search matches email, organization name, and role name. Pass organizationId for an exact organization filter, or scopeToActiveOrganization=1 to restrict results to the caller's exact active organization (used by recipient/assignee pickers); when the caller has no active organization, this selects only users with no organization. Both filters apply when supplied together. These optional list filters further narrow the effective scope. Pass ids=<uuid>,<uuid> (max 100) to resolve a known set of users in one request, for example to label a list of foreign keys; it intersects with id and roleId, and a supplied ids value that contains no valid identifier matches nothing.",
|
|
635
650
|
query: querySchema,
|
|
636
651
|
responses: [
|
|
637
652
|
{ status: 200, description: "User collection", schema: userListResponseSchema }
|