@open-mercato/core 0.10.1-develop.7847.1.7d83dc5794 → 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.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/file/[id]/route.js +6 -1
  3. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  4. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +6 -1
  5. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  6. package/dist/modules/attachments/api/library/[id]/route.js +16 -1
  7. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  8. package/dist/modules/attachments/api/route.js +11 -3
  9. package/dist/modules/attachments/api/route.js.map +2 -2
  10. package/dist/modules/attachments/cli.js +2 -0
  11. package/dist/modules/attachments/cli.js.map +2 -2
  12. package/dist/modules/attachments/data/validators.js +11 -0
  13. package/dist/modules/attachments/data/validators.js.map +2 -2
  14. package/dist/modules/attachments/index.js +10 -0
  15. package/dist/modules/attachments/index.js.map +2 -2
  16. package/dist/modules/attachments/lib/access-query.js +5 -1
  17. package/dist/modules/attachments/lib/access-query.js.map +2 -2
  18. package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
  19. package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
  20. package/dist/modules/attachments/lib/drivers/driverFactory.js +54 -33
  21. package/dist/modules/attachments/lib/drivers/driverFactory.js.map +2 -2
  22. package/dist/modules/attachments/lib/drivers/index.js +11 -1
  23. package/dist/modules/attachments/lib/drivers/index.js.map +2 -2
  24. package/dist/modules/attachments/lib/drivers/storageValidation.js +85 -0
  25. package/dist/modules/attachments/lib/drivers/storageValidation.js.map +7 -0
  26. package/dist/modules/attachments/lib/scoped-upload-service.js +3 -1
  27. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  28. package/dist/modules/attachments/lib/storageErrors.js +22 -0
  29. package/dist/modules/attachments/lib/storageErrors.js.map +7 -0
  30. package/dist/modules/customers/api/companies/[id]/route.js +4 -9
  31. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  32. package/dist/modules/customers/api/people/[id]/route.js +4 -9
  33. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/components/detail/TaskDialog.js +2 -0
  35. package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
  36. package/dist/modules/customers/lib/todoCompatibility.js +23 -0
  37. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  38. package/jest.config.cjs +1 -1
  39. package/package.json +8 -8
  40. package/src/modules/attachments/api/file/[id]/route.ts +7 -1
  41. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -1
  42. package/src/modules/attachments/api/library/[id]/route.ts +14 -1
  43. package/src/modules/attachments/api/route.ts +11 -3
  44. package/src/modules/attachments/cli.ts +2 -0
  45. package/src/modules/attachments/data/validators.ts +11 -0
  46. package/src/modules/attachments/i18n/de.json +1 -0
  47. package/src/modules/attachments/i18n/en.json +1 -0
  48. package/src/modules/attachments/i18n/es.json +1 -0
  49. package/src/modules/attachments/i18n/ko.json +1 -0
  50. package/src/modules/attachments/i18n/pl.json +1 -0
  51. package/src/modules/attachments/index.ts +12 -0
  52. package/src/modules/attachments/lib/access-query.ts +5 -1
  53. package/src/modules/attachments/lib/createFromBuffer.ts +2 -0
  54. package/src/modules/attachments/lib/drivers/driverFactory.ts +79 -55
  55. package/src/modules/attachments/lib/drivers/index.ts +13 -0
  56. package/src/modules/attachments/lib/drivers/storageValidation.ts +114 -0
  57. package/src/modules/attachments/lib/scoped-upload-service.ts +3 -1
  58. package/src/modules/attachments/lib/storageErrors.ts +21 -0
  59. package/src/modules/customer_accounts/AGENTS.md +1 -1
  60. package/src/modules/customers/api/companies/[id]/route.ts +6 -13
  61. package/src/modules/customers/api/people/[id]/route.ts +6 -13
  62. package/src/modules/customers/components/detail/TaskDialog.tsx +2 -0
  63. package/src/modules/customers/lib/todoCompatibility.ts +28 -0
  64. package/src/modules/sales/AGENTS.md +3 -3
@@ -1,4 +1,4 @@
1
- [build:core] found 6244 entry points
1
+ [build:core] found 6248 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -1,3 +1,5 @@
1
+ import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
2
+ import { assertAttachmentStoragePartitionExists } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
1
3
  import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../lib/access-runner.js";
2
4
  import { attachmentAccessErrorResponse } from "../../../lib/access-errors.js";
3
5
  import { NextResponse } from "next/server";
@@ -19,7 +21,7 @@ import { resolveAttachmentRequestScope } from "@open-mercato/core/modules/attach
19
21
  const metadata = {
20
22
  GET: { requireAuth: false }
21
23
  };
22
- async function GET(req, context) {
24
+ async function readAttachmentFile(req, context) {
23
25
  const { id } = await context.params;
24
26
  if (!id) {
25
27
  return NextResponse.json(
@@ -54,6 +56,7 @@ async function GET(req, context) {
54
56
  const partition = await em.findOne(AttachmentPartition, {
55
57
  code: attachment.partitionCode
56
58
  });
59
+ assertAttachmentStoragePartitionExists(Boolean(partition));
57
60
  if (!partition) {
58
61
  return NextResponse.json(
59
62
  { error: "Partition misconfigured" },
@@ -98,6 +101,7 @@ async function GET(req, context) {
98
101
  const responseBody = new Uint8Array(buffer);
99
102
  return new NextResponse(responseBody, { status: 200, headers });
100
103
  }
104
+ const GET = withAttachmentStorageErrors(readAttachmentFile);
101
105
  const openApi = {
102
106
  tag: attachmentsTag,
103
107
  summary: "Download attachment file",
@@ -113,6 +117,7 @@ const openApi = {
113
117
  }
114
118
  ],
115
119
  errors: [
120
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
116
121
  {
117
122
  status: 400,
118
123
  description: "Missing attachment ID",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/attachments/api/file/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from \"next/server\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope';\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string }> },\n) {\n const { id } = await context.params;\n if (!id) {\n return NextResponse.json(\n { error: \"Attachment id is required\" },\n { status: 400 },\n );\n }\n const auth = await getAuthFromRequest(req);\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const storageDriverFactory =\n (container.resolve(\"storageDriverFactory\") as StorageDriverFactory | null) ??\n new StorageDriverFactory(em);\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req);\n if (requestScope.denied) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth;\n const findFilter: Record<string, unknown> = { id };\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: renderInline ? 'render' : 'read',\n context: createAttachmentAccessContext(container),\n });\n if (!access.ok) return attachmentAccessErrorResponse(access);\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n });\n let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const headers: Record<string, string> = {\n \"Cache-Control\": access.protected ? \"private, no-store\" : checkAttachmentAccess(null, attachment, partition).ok\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'\nimport { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from \"next/server\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope';\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nasync function readAttachmentFile(\n req: NextRequest,\n context: { params: Promise<{ id: string }> },\n) {\n const { id } = await context.params;\n if (!id) {\n return NextResponse.json(\n { error: \"Attachment id is required\" },\n { status: 400 },\n );\n }\n const auth = await getAuthFromRequest(req);\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const storageDriverFactory =\n (container.resolve(\"storageDriverFactory\") as StorageDriverFactory | null) ??\n new StorageDriverFactory(em);\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req);\n if (requestScope.denied) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth;\n const findFilter: Record<string, unknown> = { id };\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: renderInline ? 'render' : 'read',\n context: createAttachmentAccessContext(container),\n });\n if (!access.ok) return attachmentAccessErrorResponse(access);\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n });\n let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const headers: Record<string, string> = {\n \"Cache-Control\": access.protected ? \"private, no-store\" : checkAttachmentAccess(null, attachment, partition).ok\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const GET = withAttachmentStorageErrors(readAttachmentFile)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,5 @@
1
+ import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
2
+ import { assertAttachmentStoragePartitionExists } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
1
3
  import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../../lib/access-runner.js";
2
4
  import { attachmentAccessErrorResponse } from "../../../../lib/access-errors.js";
3
5
  import { NextResponse } from "next/server";
@@ -31,7 +33,7 @@ const querySchema = z.object({
31
33
  const metadata = {
32
34
  GET: { requireAuth: false }
33
35
  };
34
- async function GET(req, context) {
36
+ async function readAttachmentImage(req, context) {
35
37
  const auth = await getAuthFromRequest(req);
36
38
  const { id } = await context.params;
37
39
  if (!id) {
@@ -62,6 +64,7 @@ async function GET(req, context) {
62
64
  return NextResponse.json({ error: "Not found" }, { status: 404 });
63
65
  }
64
66
  const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode });
67
+ assertAttachmentStoragePartitionExists(Boolean(partition));
65
68
  if (!partition) {
66
69
  return NextResponse.json({ error: "Partition misconfigured" }, { status: 500 });
67
70
  }
@@ -134,6 +137,7 @@ async function GET(req, context) {
134
137
  return NextResponse.json({ error: "Failed to render image" }, { status: 500 });
135
138
  }
136
139
  }
140
+ const GET = withAttachmentStorageErrors(readAttachmentImage);
137
141
  const openApi = {
138
142
  tag: attachmentsTag,
139
143
  summary: "Serve resized images",
@@ -149,6 +153,7 @@ const openApi = {
149
153
  }
150
154
  ],
151
155
  errors: [
156
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
152
157
  { status: 400, description: "Invalid parameters, missing ID, or non-image attachment", schema: attachmentErrorSchema },
153
158
  { status: 401, description: "Unauthorized - authentication required for private partitions", schema: attachmentErrorSchema },
154
159
  { status: 403, description: "Forbidden - insufficient permissions", schema: attachmentErrorSchema },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/attachments/api/image/%5Bid%5D/%5B%5B...slug%5D%5D/route.ts"],
4
- "sourcesContent": ["import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req)\n if (requestScope.denied) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: 'render', context: createAttachmentAccessContext(container),\n })\n if (!access.ok) return attachmentAccessErrorResponse(access)\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: ResizeOptions = {\n width: width || undefined,\n height: height || undefined,\n fit: cropType === 'contain' ? 'contain' : 'cover',\n }\n if (cropType === 'contain') {\n resizeOptions.background = { r: 0, g: 0, b: 0, alpha: 0 }\n }\n transformer = transformer.resize(resizeOptions)\n }\n buffer = await transformer.toBuffer()\n if (cacheKey) {\n void writeThumbnailCache(attachment.partitionCode, attachment.id, cacheKey, buffer).catch((cacheError) => {\n logger.error('Thumbnail cache write failed', { err: cacheError })\n })\n }\n }\n if (!buffer) {\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n const responseBody = new Uint8Array(buffer)\n\n return new NextResponse(responseBody, {\n headers: {\n 'Content-Type': attachment.mimeType || 'image/jpeg',\n 'Cache-Control': access.protected ? 'private, no-store' : checkAttachmentAccess(null, attachment, partition).ok ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'\nimport { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nasync function readAttachmentImage(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req)\n if (requestScope.denied) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: 'render', context: createAttachmentAccessContext(container),\n })\n if (!access.ok) return attachmentAccessErrorResponse(access)\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: ResizeOptions = {\n width: width || undefined,\n height: height || undefined,\n fit: cropType === 'contain' ? 'contain' : 'cover',\n }\n if (cropType === 'contain') {\n resizeOptions.background = { r: 0, g: 0, b: 0, alpha: 0 }\n }\n transformer = transformer.resize(resizeOptions)\n }\n buffer = await transformer.toBuffer()\n if (cacheKey) {\n void writeThumbnailCache(attachment.partitionCode, attachment.id, cacheKey, buffer).catch((cacheError) => {\n logger.error('Thumbnail cache write failed', { err: cacheError })\n })\n }\n }\n if (!buffer) {\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n const responseBody = new Uint8Array(buffer)\n\n return new NextResponse(responseBody, {\n headers: {\n 'Content-Type': attachment.mimeType || 'image/jpeg',\n 'Cache-Control': access.protected ? 'private, no-store' : checkAttachmentAccess(null, attachment, partition).ok ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const GET = withAttachmentStorageErrors(readAttachmentImage)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,5 @@
1
+ import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
2
+ import { getAttachmentStoragePolicy } from "../../../lib/drivers/storageValidation.js";
1
3
  import { LockMode } from "@mikro-orm/core";
2
4
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
5
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
@@ -244,6 +246,18 @@ async function deleteAttachment(req, ctx) {
244
246
  tenantId: auth.tenantId,
245
247
  organizationId: auth.orgId
246
248
  };
249
+ if (getAttachmentStoragePolicy() === "strict") {
250
+ const preflightRecord = await findOneWithDecryption(em, Attachment, deleteFilter, void 0, {
251
+ tenantId: auth.tenantId,
252
+ organizationId: auth.orgId
253
+ });
254
+ if (!preflightRecord) return throwAttachmentAccessError(404);
255
+ await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: preflightRecord, action: "delete" });
256
+ await storageDriverFactory.resolveForPartition(preflightRecord.partitionCode, {
257
+ tenantId: preflightRecord.tenantId ?? auth.tenantId,
258
+ organizationId: preflightRecord.organizationId ?? auth.orgId
259
+ });
260
+ }
247
261
  const outcome = await em.transactional(async (tx) => {
248
262
  const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, void 0, {
249
263
  tenantId: auth.tenantId,
@@ -289,7 +303,7 @@ async function deleteAttachment(req, ctx) {
289
303
  return NextResponse.json({ ok: true });
290
304
  }
291
305
  const PATCH = withAttachmentAccessErrors(updateAttachment);
292
- const DELETE = withAttachmentAccessErrors(deleteAttachment);
306
+ const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment));
293
307
  const openApi = {
294
308
  tag: attachmentsTag,
295
309
  summary: "Attachment detail management",
@@ -334,6 +348,7 @@ const openApi = {
334
348
  { status: 200, description: "Attachment deleted successfully", schema: z.object({ ok: z.literal(true) }) }
335
349
  ],
336
350
  errors: [
351
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
337
352
  { status: 400, description: "Invalid attachment ID", schema: attachmentErrorSchema },
338
353
  { status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
339
354
  { status: 404, description: "Attachment not found", schema: attachmentErrorSchema },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/attachments/api/library/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { prepareAttachmentMutation } from '../../../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../../../lib/access-errors'\nimport { createAttachmentAccessContext } from '../../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'metadata' })\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n }, { headers: { 'Cache-Control': 'private, no-store' } })\n}\n\nexport const GET = withAttachmentAccessErrors(getAttachment)\n\nasync function updateAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await readJsonSafe(req, null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const record = await findOneWithDecryption(tx, Attachment, patchFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update',\n payload: { ...parsed.data, customFields: custom },\n })\n const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom })\n const effectiveParsed = updateSchema.safeParse(effective.base)\n if (!effectiveParsed.success) return throwAttachmentAccessError(403)\n const patch: Record<string, unknown> = {}\n if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags)\n if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments)\n const nextMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'reassign' })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'metadata' })\n record.storageMetadata = nextMetadata\n await tx.flush()\n if (dataEngine && Object.keys(effective.custom).length) {\n await setCustomFieldsIfAny({ dataEngine, entityId: E.attachments.attachment, recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId, organizationId: record.organizationId ?? auth.orgId,\n values: effective.custom,\n })\n }\n return { record, custom: effective.custom, guard }\n })\n const { record } = outcome\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(outcome.custom ?? null),\n },\n })\n}\n\nasync function deleteAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!candidate) return throwAttachmentAccessError(404)\n const referenceIds = await lockAttachmentStorageReferences(tx, candidate)\n const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)\n if (!removal.removed) return throwAttachmentAccessError(404)\n const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'delete' })\n const driver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? auth.orgId!,\n })\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const PATCH = withAttachmentAccessErrors(updateAttachment)\nexport const DELETE = withAttachmentAccessErrors(deleteAttachment)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C,SAAS,qCAAqC;AAC9C,SAAS,mCAAmC;AAC5C,SAAS,kCAAkC;AAC3C,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,YAAY,2BAA2B;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,iCAAiC,uCAAuC;AACjF,SAAS,yBAAyB,6BAA6B;AAC/D,SAAS,qBAAqB,4BAA4B;AAC1D,SAAS,oCAAoC;AAC7C,SAAS,SAAS;AAGlB,SAAS,sBAAsB,6BAA6B;AAC5D,SAAS,4BAA4B,oCAAoC;AACzE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EACnC,aAAa,EACV;AAAA,IACC,EAAE,OAAO;AAAA,MACP,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACtB,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACpB,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,MACrC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACxC,CAAC;AAAA,EACH,EACC,SAAS;AACd,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AAAA,EAChE,OAAO,EAAE,aAAa,MAAM,iBAAiB,CAAC,oBAAoB,EAAE;AAAA,EACpE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,oBAAoB,EAAE;AACvE;AAKA,eAAe,oBAAoB,KAA2C;AAC5E,QAAM,SAAS,KAAK;AACpB,MAAI;AACF,UAAM,EAAE,GAAG,IAAI,MAAM;AACrB,QAAI,OAAO,OAAO,YAAY,GAAG,KAAK,EAAE,QAAQ;AAC9C,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,cAAc,KAAkB,KAAmB;AAChE,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,YAAa,CAAC,KAAK,SAAS,CAAC,KAAK,cAAe;AAClE,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AACA,QAAM,eAAe,MAAM,oBAAoB,GAAG;AAClD,MAAI,CAAC,cAAc;AACjB,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AACA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,KAAK,QAAQ,IAAI;AACvB,MAAI,cAAkC;AACtC,MAAI;AACF,kBAAc,QAAQ,aAAa;AAAA,EACrC,QAAQ;AACN,kBAAc;AAAA,EAChB;AACA,QAAM,aAAsC;AAAA,IAC1C,IAAI;AAAA,IACJ,UAAU,KAAK;AAAA,EACjB;AACA,MAAI,KAAK,OAAO;AACd,eAAW,iBAAiB,KAAK;AAAA,EACnC;AACA,QAAM,SAAS,MAAM,GAAG,QAAQ,YAAY,UAAU;AACtD,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7E;AACA,QAAM,4BAA4B,EAAE,IAAI,SAAS,8BAA8B,SAAS,GAAG,MAAM,YAAY,QAAQ,QAAQ,WAAW,CAAC;AACzI,QAAMA,YAAW,uBAAuB,OAAO,eAAe;AAC9D,QAAM,YAAY,OAAO,gBACrB,MAAM,GAAG,QAAQ,qBAAqB,EAAE,MAAM,OAAO,cAAc,CAAC,IACpE;AACJ,QAAM,oBAAoB,MAAM,sBAAsB;AAAA,IACpD;AAAA,IACA,UAAU,EAAE,YAAY;AAAA,IACxB,WAAW,CAAC,OAAO,EAAE;AAAA,IACrB,kBAAkB,EAAE,CAAC,OAAO,EAAE,GAAG,OAAO,YAAY,KAAK,YAAY,KAAK;AAAA,IAC1E,wBAAwB,EAAE,CAAC,OAAO,EAAE,GAAG,OAAO,kBAAkB,KAAK,SAAS,KAAK;AAAA,IACnF,iBAAiB,CAAC,KAAK,YAAY,IAAI,EAAE,OAAO,CAAC,UAA2B,CAAC,CAAC,KAAK;AAAA,EACrF,CAAC;AACD,QAAM,eAAe,6BAA6B,kBAAkB,OAAO,EAAE,CAAC;AAC9E,QAAM,cAAcA,UAAS,eAAe,CAAC;AAC7C,QAAM,cAAc,MAAM,6BAA6B,aAAa;AAAA,IAClE;AAAA,IACA,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AACD,QAAM,sBAAsB,2BAA2B,aAAa,WAAW;AAC/E,SAAO,aAAa,KAAK;AAAA,IACvB,MAAM;AAAA,MACJ,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,eAAe,OAAO;AAAA,MACtB,gBAAgB,WAAW,SAAS;AAAA,MACpC,MAAMA,UAAS,QAAQ,CAAC;AAAA,MACxB,aAAa;AAAA,MACb,SAAS,OAAO,WAAW,OAAO,QAAQ,KAAK,IAAI,OAAO,UAAU;AAAA,MACpE;AAAA,IACF;AAAA,EACF,GAAG,EAAE,SAAS,EAAE,iBAAiB,oBAAoB,EAAE,CAAC;AAC1D;AAEO,MAAM,MAAM,2BAA2B,aAAa;AAE3D,eAAe,iBAAiB,KAAkB,KAAmB;AACnE,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC,KAAK,OAAO;AAC1C,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AACA,QAAM,eAAe,MAAM,oBAAoB,GAAG;AAClD,MAAI,CAAC,cAAc;AACjB,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AACA,QAAM,UAAU,MAAM,aAAa,KAAK,IAAI;AAC5C,QAAM,EAAE,MAAM,OAAO,IAAI,wBAAwB,OAAO;AACxD,QAAM,SAAS,aAAa,UAAU,IAAI;AAC1C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxE;AACA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,KAAK,QAAQ,IAAI;AACvB,MAAI,cAAkC;AACtC,MAAI;AACF,kBAAc,QAAQ,aAAa;AAAA,EACrC,QAAQ;AACN,kBAAc;AAAA,EAChB;AACA,QAAM,aAAa,QAAQ,YAAY;AACvC,QAAM,cAAuC;AAAA,IAC3C,IAAI;AAAA,IACJ,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB;AACA,QAAM,gBAAgB,8BAA8B,SAAS;AAC7D,QAAM,UAAU,MAAM,GAAG,cAAc,OAAO,OAAO;AACnD,UAAMC,UAAS,MAAM,sBAAsB,IAAI,YAAY,aAAa;AAAA,MACtE,UAAU,SAAS;AAAA,MAAmB,SAAS;AAAA,IACjD,GAAG,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,MAAM,CAAC;AAC1D,QAAI,CAACA,QAAQ,QAAO,2BAA2B,GAAG;AAClD,UAAM,QAAQ,MAAM,0BAA0B;AAAA,MAC5C;AAAA,MAAW;AAAA,MAAK;AAAA,MAAM,UAAUA,QAAO;AAAA,MAAI,WAAW;AAAA,MACtD,SAAS,EAAE,GAAG,OAAO,MAAM,cAAc,OAAO;AAAA,IAClD,CAAC;AACD,UAAM,YAAY,wBAAwB,MAAM,mBAAmB,EAAE,GAAG,OAAO,MAAM,cAAc,OAAO,CAAC;AAC3G,UAAM,kBAAkB,aAAa,UAAU,UAAU,IAAI;AAC7D,QAAI,CAAC,gBAAgB,QAAS,QAAO,2BAA2B,GAAG;AACnE,UAAM,QAAiC,CAAC;AACxC,QAAI,gBAAgB,KAAK,KAAM,OAAM,OAAO,wBAAwB,gBAAgB,KAAK,IAAI;AAC7F,QAAI,gBAAgB,KAAK,YAAa,OAAM,cAAc,+BAA+B,gBAAgB,KAAK,WAAW;AACzH,UAAM,eAAe,wBAAwBA,QAAO,iBAAiB,KAAK;AAC1E,UAAM,4BAA4B,EAAE,IAAI,IAAI,SAAS,eAAe,MAAM,YAAYA,SAAQ,QAAQ,YAAY,mBAAmB,KAAK,CAAC;AAC3I,UAAM,4BAA4B,EAAE,IAAI,IAAI,SAAS,eAAe,MAAM,YAAY,EAAE,GAAGA,SAAQ,iBAAiB,aAAa,GAAG,QAAQ,WAAW,CAAC;AACxJ,UAAM,4BAA4B,EAAE,IAAI,IAAI,SAAS,eAAe,MAAM,YAAY,EAAE,GAAGA,SAAQ,iBAAiB,aAAa,GAAG,QAAQ,WAAW,CAAC;AACxJ,IAAAA,QAAO,kBAAkB;AACzB,UAAM,GAAG,MAAM;AACf,QAAI,cAAc,OAAO,KAAK,UAAU,MAAM,EAAE,QAAQ;AACtD,YAAM,qBAAqB;AAAA,QAAE;AAAA,QAAY,UAAU,EAAE,YAAY;AAAA,QAAY,UAAUA,QAAO;AAAA,QAC5F,UAAUA,QAAO,YAAY,KAAK;AAAA,QAAU,gBAAgBA,QAAO,kBAAkB,KAAK;AAAA,QAC1F,QAAQ,UAAU;AAAA,MACpB,CAAC;AAAA,IACH;AACA,WAAO,EAAE,QAAAA,SAAQ,QAAQ,UAAU,QAAQ,MAAM;AAAA,EACnD,CAAC;AACD,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,QAAQ,MAAM,gBAAgB;AAEpC,MAAI,YAAY;AACd,UAAM,oBAAoB;AAAA,MACxB;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO;AAAA,QACX,gBAAgB,OAAO,kBAAkB,KAAK,SAAS;AAAA,QACvD,UAAU,OAAO,YAAY,KAAK,YAAY;AAAA,MAChD;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AACD,UAAM,WAAW,sBAAsB;AAAA,EACzC;AAEA,QAAMD,YAAW,uBAAuB,OAAO,eAAe;AAC9D,QAAM,cAAcA,UAAS,eAAe,CAAC;AAC7C,QAAM,cAAc,MAAM,6BAA6B,aAAa;AAAA,IAClE;AAAA,IACA,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AACD,QAAM,sBAAsB,2BAA2B,aAAa,WAAW;AAC/E,SAAO,aAAa,KAAK;AAAA,IACvB,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,IAAI,OAAO;AAAA,MACX,MAAMA,UAAS,QAAQ,CAAC;AAAA,MACxB,aAAa;AAAA,MACb,cAAc,6BAA6B,QAAQ,UAAU,IAAI;AAAA,IACnE;AAAA,EACF,CAAC;AACH;AAEA,eAAe,iBAAiB,KAAkB,KAAmB;AACnE,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC,KAAK,OAAO;AAC1C,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AACA,QAAM,eAAe,MAAM,oBAAoB,GAAG;AAClD,MAAI,CAAC,cAAc;AACjB,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AACA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,KAAK,QAAQ,IAAI;AACvB,QAAM,aAAa,QAAQ,YAAY;AACvC,QAAM,uBAAuB,QAAQ,sBAAsB;AAC3D,QAAM,eAAwC;AAAA,IAC5C,IAAI;AAAA,IACJ,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB;AACA,QAAM,UAAU,MAAM,GAAG,cAAc,OAAO,OAAO;AACnD,UAAM,YAAY,MAAM,sBAAsB,IAAI,YAAY,cAAc,QAAW;AAAA,MACrF,UAAU,KAAK;AAAA,MAAU,gBAAgB,KAAK;AAAA,IAChD,CAAC;AACD,QAAI,CAAC,UAAW,QAAO,2BAA2B,GAAG;AACrD,UAAM,eAAe,MAAM,gCAAgC,IAAI,SAAS;AACxE,UAAM,UAAU,gCAAgC,cAAc,UAAU,EAAE;AAC1E,QAAI,CAAC,QAAQ,QAAS,QAAO,2BAA2B,GAAG;AAC3D,UAAMC,UAAS,MAAM,sBAAsB,IAAI,YAAY,cAAc;AAAA,MACvE,UAAU,SAAS;AAAA,MAAmB,SAAS;AAAA,IACjD,GAAG,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,MAAM,CAAC;AAC1D,QAAI,CAACA,QAAQ,QAAO,2BAA2B,GAAG;AAClD,UAAM,QAAQ,MAAM,0BAA0B,EAAE,WAAW,KAAK,MAAM,UAAUA,QAAO,IAAI,WAAW,SAAS,CAAC;AAChH,UAAM,4BAA4B,EAAE,IAAI,IAAI,SAAS,8BAA8B,SAAS,GAAG,MAAM,YAAYA,SAAQ,QAAQ,SAAS,CAAC;AAC3I,UAAM,SAAS,MAAM,qBAAqB,oBAAoBA,QAAO,eAAe;AAAA,MAClF,UAAUA,QAAO,YAAY,KAAK;AAAA,MAAW,gBAAgBA,QAAO,kBAAkB,KAAK;AAAA,IAC7F,CAAC;AACD,OAAG,OAAOA,OAAM;AAChB,UAAM,GAAG,MAAM;AACf,WAAO,EAAE,QAAAA,SAAQ,QAAQ,OAAO,gBAAgB,QAAQ,eAAe;AAAA,EACzE,CAAC;AACD,QAAM,EAAE,OAAO,IAAI;AACnB,MAAI,QAAQ,eAAgB,OAAM,QAAQ,OAAO,OAAO,OAAO,eAAe,OAAO,WAAW;AAChG,QAAM,QAAQ,MAAM,gBAAgB;AAEpC,MAAI,YAAY;AACd,UAAM,oBAAoB;AAAA,MACxB;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO;AAAA,QACX,gBAAgB,OAAO,kBAAkB,KAAK,SAAS;AAAA,QACvD,UAAU,OAAO,YAAY,KAAK,YAAY;AAAA,MAChD;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AACD,UAAM,WAAW,sBAAsB;AAAA,EACzC;AAEA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEO,MAAM,QAAQ,2BAA2B,gBAAgB;AACzD,MAAM,SAAS,2BAA2B,gBAAgB;AAE1D,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,+BAA+B;AAAA,MAC3F;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,sBAAsB;AAAA,QACnF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,sBAAsB;AAAA,QAC1E,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,sBAAsB;AAAA,QAClF,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,sBAAsB;AAAA,QACxF,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,sBAAsB;AAAA,MAC7F;AAAA,IACF;AAAA,IACA,OAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mCAAmC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE;AAAA,MAC1H;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,oCAAoC,QAAQ,sBAAsB;AAAA,QAC9F,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,sBAAsB;AAAA,QAC1E,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,sBAAsB;AAAA,QAClF,EAAE,QAAQ,KAAK,aAAa,6BAA6B,QAAQ,sBAAsB;AAAA,QACvF,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,sBAAsB;AAAA,QACxF,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,sBAAsB;AAAA,MAC7F;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mCAAmC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE;AAAA,MAC3G;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,sBAAsB;AAAA,QACnF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,sBAAsB;AAAA,QAC1E,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,sBAAsB;AAAA,QAClF,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,sBAAsB;AAAA,QACxF,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,sBAAsB;AAAA,MAC7F;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { getAttachmentStoragePolicy } from '../../../lib/drivers/storageValidation'\nimport { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { prepareAttachmentMutation } from '../../../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../../../lib/access-errors'\nimport { createAttachmentAccessContext } from '../../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'metadata' })\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n }, { headers: { 'Cache-Control': 'private, no-store' } })\n}\n\nexport const GET = withAttachmentAccessErrors(getAttachment)\n\nasync function updateAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await readJsonSafe(req, null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const record = await findOneWithDecryption(tx, Attachment, patchFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update',\n payload: { ...parsed.data, customFields: custom },\n })\n const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom })\n const effectiveParsed = updateSchema.safeParse(effective.base)\n if (!effectiveParsed.success) return throwAttachmentAccessError(403)\n const patch: Record<string, unknown> = {}\n if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags)\n if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments)\n const nextMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'reassign' })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'metadata' })\n record.storageMetadata = nextMetadata\n await tx.flush()\n if (dataEngine && Object.keys(effective.custom).length) {\n await setCustomFieldsIfAny({ dataEngine, entityId: E.attachments.attachment, recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId, organizationId: record.organizationId ?? auth.orgId,\n values: effective.custom,\n })\n }\n return { record, custom: effective.custom, guard }\n })\n const { record } = outcome\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(outcome.custom ?? null),\n },\n })\n}\n\nasync function deleteAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n if (getAttachmentStoragePolicy() === 'strict') {\n const preflightRecord = await findOneWithDecryption(em, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!preflightRecord) return throwAttachmentAccessError(404)\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: preflightRecord, action: 'delete' })\n await storageDriverFactory.resolveForPartition(preflightRecord.partitionCode, {\n tenantId: preflightRecord.tenantId ?? auth.tenantId, organizationId: preflightRecord.organizationId ?? auth.orgId,\n })\n }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!candidate) return throwAttachmentAccessError(404)\n const referenceIds = await lockAttachmentStorageReferences(tx, candidate)\n const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)\n if (!removal.removed) return throwAttachmentAccessError(404)\n const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'delete' })\n const driver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? auth.orgId!,\n })\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const PATCH = withAttachmentAccessErrors(updateAttachment)\nexport const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment))\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["metadata", "record"]
7
7
  }
@@ -1,3 +1,6 @@
1
+ import { attachmentErrorSchema } from "./openapi.js";
2
+ import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
3
+ import { assertAttachmentStoragePartitionExists, getAttachmentStoragePolicy } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
1
4
  import { LockMode } from "@mikro-orm/core";
2
5
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
6
  import { prepareAttachmentMutation } from "../lib/access-mutation.js";
@@ -256,7 +259,7 @@ async function listRecordAttachments(req) {
256
259
  }, { headers: protectedScan ? { "Cache-Control": "private, no-store" } : void 0 });
257
260
  }
258
261
  const GET = withAttachmentAccessErrors(listRecordAttachments);
259
- async function POST(req) {
262
+ async function uploadAttachment(req) {
260
263
  const { t } = await resolveTranslations();
261
264
  const auth = await getAuthFromRequest(req);
262
265
  if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -334,7 +337,7 @@ async function POST(req) {
334
337
  if (uploadScope.denied) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
335
338
  const orgId = uploadScope.organizationId;
336
339
  if (!orgId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
337
- await ensureDefaultPartitions(em);
340
+ if (getAttachmentStoragePolicy() === "legacy") await ensureDefaultPartitions(em);
338
341
  let partitionFromField = null;
339
342
  let fieldMaxAttachmentSizeMb = null;
340
343
  if (fieldKey) {
@@ -407,6 +410,7 @@ async function POST(req) {
407
410
  let partition = null;
408
411
  for (const code of partitionCodeCandidates) {
409
412
  const record = await em.findOne(AttachmentPartition, { code });
413
+ assertAttachmentStoragePartitionExists(Boolean(record));
410
414
  if (record) {
411
415
  partition = record;
412
416
  break;
@@ -416,6 +420,7 @@ async function POST(req) {
416
420
  partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode });
417
421
  }
418
422
  if (!partition) {
423
+ assertAttachmentStoragePartitionExists(false);
419
424
  return NextResponse.json({ error: "Storage partition is not configured." }, { status: 400 });
420
425
  }
421
426
  await ensureAttachmentPartitionProtection(em, partition);
@@ -700,7 +705,8 @@ async function deleteAttachment(req) {
700
705
  }
701
706
  return NextResponse.json({ ok: true });
702
707
  }
703
- const DELETE = withAttachmentAccessErrors(deleteAttachment);
708
+ const POST = withAttachmentAccessErrors(withAttachmentStorageErrors(uploadAttachment));
709
+ const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment));
704
710
  const openApi = {
705
711
  summary: "Manage entity attachments",
706
712
  description: "Upload and list attachments associated with module entities and records.",
@@ -731,6 +737,7 @@ const openApi = {
731
737
  { status: 200, description: "Attachment stored successfully", schema: uploadResponseSchema }
732
738
  ],
733
739
  errors: [
740
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
734
741
  { status: 400, description: "Payload validation error", schema: errorSchema },
735
742
  { status: 401, description: "Unauthorized", schema: errorSchema },
736
743
  { status: 403, description: "Attachment violates field constraints", schema: errorSchema }
@@ -745,6 +752,7 @@ const openApi = {
745
752
  { status: 404, description: "Attachment not found", schema: errorSchema }
746
753
  ],
747
754
  errors: [
755
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
748
756
  { status: 400, description: "Missing attachment identifier", schema: errorSchema },
749
757
  { status: 401, description: "Unauthorized", schema: errorSchema },
750
758
  { status: 403, description: "Owner policy denies access", schema: errorSchema },