@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7748.1.d745ba61aa

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 (198) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/api_key/index.js +2 -0
  3. package/dist/generated/entities/api_key/index.js.map +2 -2
  4. package/dist/generated/entities/attachment_partition/index.js +2 -0
  5. package/dist/generated/entities/attachment_partition/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
  9. package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
  10. package/dist/modules/api_keys/data/entities.js +7 -0
  11. package/dist/modules/api_keys/data/entities.js.map +2 -2
  12. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
  13. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
  14. package/dist/modules/api_keys/services/apiKeyService.js +31 -9
  15. package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
  16. package/dist/modules/attachments/api/file/[id]/route.js +16 -10
  17. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  18. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
  19. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  20. package/dist/modules/attachments/api/library/[id]/route.js +100 -56
  21. package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
  22. package/dist/modules/attachments/api/library/route.js +56 -17
  23. package/dist/modules/attachments/api/library/route.js.map +2 -2
  24. package/dist/modules/attachments/api/partitions/route.js +2 -0
  25. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  26. package/dist/modules/attachments/api/route.js +74 -34
  27. package/dist/modules/attachments/api/route.js.map +3 -3
  28. package/dist/modules/attachments/api/transfer/route.js +63 -37
  29. package/dist/modules/attachments/api/transfer/route.js.map +2 -2
  30. package/dist/modules/attachments/data/entities.js +3 -0
  31. package/dist/modules/attachments/data/entities.js.map +2 -2
  32. package/dist/modules/attachments/data/validators.js +41 -0
  33. package/dist/modules/attachments/data/validators.js.map +7 -0
  34. package/dist/modules/attachments/di.js +5 -2
  35. package/dist/modules/attachments/di.js.map +2 -2
  36. package/dist/modules/attachments/generators.js +29 -0
  37. package/dist/modules/attachments/generators.js.map +7 -0
  38. package/dist/modules/attachments/index.js +8 -1
  39. package/dist/modules/attachments/index.js.map +2 -2
  40. package/dist/modules/attachments/lib/access-errors.js +35 -0
  41. package/dist/modules/attachments/lib/access-errors.js.map +7 -0
  42. package/dist/modules/attachments/lib/access-mutation.js +23 -0
  43. package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
  44. package/dist/modules/attachments/lib/access-protection.js +73 -0
  45. package/dist/modules/attachments/lib/access-protection.js.map +7 -0
  46. package/dist/modules/attachments/lib/access-query.js +74 -0
  47. package/dist/modules/attachments/lib/access-query.js.map +7 -0
  48. package/dist/modules/attachments/lib/access-registry.js +87 -0
  49. package/dist/modules/attachments/lib/access-registry.js.map +7 -0
  50. package/dist/modules/attachments/lib/access-runner.js +161 -0
  51. package/dist/modules/attachments/lib/access-runner.js.map +7 -0
  52. package/dist/modules/attachments/lib/access-types.js +1 -0
  53. package/dist/modules/attachments/lib/access-types.js.map +7 -0
  54. package/dist/modules/attachments/lib/attachment-service.js +14 -6
  55. package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
  56. package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
  57. package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
  58. package/dist/modules/attachments/lib/partitions.js +3 -2
  59. package/dist/modules/attachments/lib/partitions.js.map +2 -2
  60. package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
  61. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  62. package/dist/modules/attachments/lib/target-access-service.js +12 -3
  63. package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
  64. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
  65. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
  66. package/dist/modules/attachments/setup.js +10 -0
  67. package/dist/modules/attachments/setup.js.map +2 -2
  68. package/dist/modules/notifications/api/[id]/action/route.js +8 -0
  69. package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
  70. package/dist/modules/notifications/api/route.js +17 -0
  71. package/dist/modules/notifications/api/route.js.map +2 -2
  72. package/dist/modules/notifications/data/validators.js +1 -1
  73. package/dist/modules/notifications/data/validators.js.map +2 -2
  74. package/dist/modules/notifications/lib/notificationService.js +45 -3
  75. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  76. package/dist/modules/notifications/lib/routeHelpers.js +27 -0
  77. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  78. package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
  79. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  80. package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
  81. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  82. package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
  83. package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
  84. package/dist/modules/staff/notifications.js +4 -2
  85. package/dist/modules/staff/notifications.js.map +2 -2
  86. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
  87. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
  88. package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
  89. package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
  90. package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
  91. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  92. package/dist/modules/workflows/data/entities.js.map +2 -2
  93. package/dist/modules/workflows/lib/activity-executor.js +18 -4
  94. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  95. package/dist/modules/workflows/lib/activity-types.js +2 -2
  96. package/dist/modules/workflows/lib/activity-types.js.map +2 -2
  97. package/dist/modules/workflows/lib/condition-handler.js +7 -1
  98. package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
  99. package/dist/modules/workflows/lib/duration.js +13 -0
  100. package/dist/modules/workflows/lib/duration.js.map +2 -2
  101. package/dist/modules/workflows/lib/instance-status.js +16 -0
  102. package/dist/modules/workflows/lib/instance-status.js.map +7 -0
  103. package/dist/modules/workflows/lib/step-handler.js +9 -2
  104. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  105. package/dist/modules/workflows/lib/task-handler.js +16 -8
  106. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  107. package/dist/modules/workflows/lib/task-quick-action.js +13 -10
  108. package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
  109. package/dist/modules/workflows/lib/task-sla.js +10 -0
  110. package/dist/modules/workflows/lib/task-sla.js.map +2 -2
  111. package/dist/modules/workflows/lib/transition-handler.js +2 -1
  112. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  113. package/dist/modules/workflows/lib/workflow-executor.js +29 -1
  114. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  115. package/dist/modules/workflows/notifications.js +4 -0
  116. package/dist/modules/workflows/notifications.js.map +2 -2
  117. package/generated/entities/api_key/index.ts +1 -0
  118. package/generated/entities/attachment_partition/index.ts +1 -0
  119. package/generated/entity-fields-registry.ts +2 -0
  120. package/package.json +7 -7
  121. package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
  122. package/src/modules/api_keys/data/entities.ts +25 -1
  123. package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
  124. package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
  125. package/src/modules/api_keys/services/apiKeyService.ts +55 -9
  126. package/src/modules/attachments/api/file/[id]/route.ts +12 -9
  127. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
  128. package/src/modules/attachments/api/library/[id]/route.ts +87 -58
  129. package/src/modules/attachments/api/library/route.ts +40 -11
  130. package/src/modules/attachments/api/partitions/route.ts +2 -0
  131. package/src/modules/attachments/api/route.ts +68 -35
  132. package/src/modules/attachments/api/transfer/route.ts +65 -42
  133. package/src/modules/attachments/data/entities.ts +4 -0
  134. package/src/modules/attachments/data/validators.ts +36 -0
  135. package/src/modules/attachments/di.ts +4 -1
  136. package/src/modules/attachments/generators.ts +27 -0
  137. package/src/modules/attachments/i18n/de.json +4 -0
  138. package/src/modules/attachments/i18n/en.json +4 -0
  139. package/src/modules/attachments/i18n/es.json +4 -0
  140. package/src/modules/attachments/i18n/ko.json +4 -0
  141. package/src/modules/attachments/i18n/pl.json +4 -0
  142. package/src/modules/attachments/index.ts +18 -0
  143. package/src/modules/attachments/lib/access-errors.ts +32 -0
  144. package/src/modules/attachments/lib/access-mutation.ts +26 -0
  145. package/src/modules/attachments/lib/access-protection.ts +83 -0
  146. package/src/modules/attachments/lib/access-query.ts +86 -0
  147. package/src/modules/attachments/lib/access-registry.ts +117 -0
  148. package/src/modules/attachments/lib/access-runner.ts +183 -0
  149. package/src/modules/attachments/lib/access-types.ts +78 -0
  150. package/src/modules/attachments/lib/attachment-service.ts +8 -4
  151. package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
  152. package/src/modules/attachments/lib/partitions.ts +3 -2
  153. package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
  154. package/src/modules/attachments/lib/target-access-service.ts +10 -3
  155. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
  156. package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
  157. package/src/modules/attachments/setup.ts +10 -0
  158. package/src/modules/auth/i18n/de.json +3 -0
  159. package/src/modules/auth/i18n/en.json +3 -0
  160. package/src/modules/auth/i18n/es.json +3 -0
  161. package/src/modules/auth/i18n/ko.json +3 -0
  162. package/src/modules/auth/i18n/pl.json +3 -0
  163. package/src/modules/notifications/api/[id]/action/route.ts +8 -0
  164. package/src/modules/notifications/api/route.ts +17 -0
  165. package/src/modules/notifications/data/validators.ts +1 -1
  166. package/src/modules/notifications/i18n/de.json +2 -0
  167. package/src/modules/notifications/i18n/en.json +2 -0
  168. package/src/modules/notifications/i18n/es.json +2 -0
  169. package/src/modules/notifications/i18n/ko.json +2 -0
  170. package/src/modules/notifications/i18n/pl.json +2 -0
  171. package/src/modules/notifications/lib/notificationService.ts +67 -1
  172. package/src/modules/notifications/lib/routeHelpers.ts +38 -0
  173. package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
  174. package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
  175. package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
  176. package/src/modules/staff/notifications.ts +2 -0
  177. package/src/modules/workflows/AGENTS.md +2 -2
  178. package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
  179. package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
  180. package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
  181. package/src/modules/workflows/data/entities.ts +6 -0
  182. package/src/modules/workflows/i18n/de.json +2 -0
  183. package/src/modules/workflows/i18n/en.json +2 -0
  184. package/src/modules/workflows/i18n/es.json +2 -0
  185. package/src/modules/workflows/i18n/ko.json +2 -0
  186. package/src/modules/workflows/i18n/pl.json +2 -0
  187. package/src/modules/workflows/lib/activity-executor.ts +36 -4
  188. package/src/modules/workflows/lib/activity-types.ts +2 -2
  189. package/src/modules/workflows/lib/condition-handler.ts +16 -1
  190. package/src/modules/workflows/lib/duration.ts +25 -0
  191. package/src/modules/workflows/lib/instance-status.ts +29 -0
  192. package/src/modules/workflows/lib/step-handler.ts +16 -1
  193. package/src/modules/workflows/lib/task-handler.ts +18 -10
  194. package/src/modules/workflows/lib/task-quick-action.ts +19 -10
  195. package/src/modules/workflows/lib/task-sla.ts +13 -0
  196. package/src/modules/workflows/lib/transition-handler.ts +2 -1
  197. package/src/modules/workflows/lib/workflow-executor.ts +44 -1
  198. package/src/modules/workflows/notifications.ts +4 -0
@@ -1,3 +1,6 @@
1
+ import { createAttachmentAccessContext } from "../../lib/access-runner.js";
2
+ import { requiresAttachmentAccessScan, scanAuthorizedAttachments } from "../../lib/access-query.js";
3
+ import { withAttachmentAccessErrors } from "../../lib/access-errors.js";
1
4
  import { NextResponse } from "next/server";
2
5
  import { z } from "zod";
3
6
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
@@ -44,7 +47,7 @@ function formatDateValue(value) {
44
47
  };
45
48
  return toDate().toISOString();
46
49
  }
47
- async function GET(req) {
50
+ async function listAttachments(req) {
48
51
  const auth = await getAuthFromRequest(req);
49
52
  if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) {
50
53
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -57,7 +60,8 @@ async function GET(req) {
57
60
  const { page, pageSize, search, partition, tags, sortField, sortDir } = parsed.data;
58
61
  const tagList = buildTagFilter(tags);
59
62
  const offset = (page - 1) * pageSize;
60
- const { resolve } = await createRequestContainer();
63
+ const container = await createRequestContainer();
64
+ const { resolve } = container;
61
65
  const em = resolve("em");
62
66
  await ensureDefaultPartitions(em);
63
67
  let queryEngine = null;
@@ -88,14 +92,42 @@ async function GET(req) {
88
92
  createdAt: "a.created_at"
89
93
  };
90
94
  const orderColumn = orderMap[sortField ?? "createdAt"] ?? "a.created_at";
91
- qb.orderBy({ [orderColumn]: sortDir === "asc" ? "asc" : "desc" });
95
+ qb.orderBy({ [orderColumn]: sortDir === "asc" ? "asc" : "desc", "a.id": "asc" });
92
96
  qb.limit(pageSize).offset(offset);
93
- const partitionsPromise = em.find(
94
- AttachmentPartition,
95
- {},
96
- { orderBy: { title: "asc" }, fields: ["code", "title", "description"] }
97
- );
98
- const [records, total, partitions] = await Promise.all([qb.getResultList(), countQb.getCount("a.id", true), partitionsPromise]);
97
+ const partitions = await em.find(AttachmentPartition, {}, { orderBy: { title: "asc" } });
98
+ const protectedScan = requiresAttachmentAccessScan(partitions);
99
+ const accessContext = createAttachmentAccessContext(container);
100
+ const deadline = Date.now() + 15e3;
101
+ let records;
102
+ let total;
103
+ let authorizedTags = null;
104
+ if (protectedScan) {
105
+ const scanned = await scanAuthorizedAttachments({
106
+ auth,
107
+ context: accessContext,
108
+ partitions,
109
+ offset,
110
+ limit: pageSize,
111
+ deadline,
112
+ loadBatch: (batchOffset, limit) => qb.clone().limit(limit).offset(batchOffset).getResultList()
113
+ });
114
+ records = scanned.records;
115
+ total = scanned.total;
116
+ const facetQuery = em.createQueryBuilder(Attachment, "a").where(baseFilter).orderBy({ "a.id": "asc" });
117
+ const facets = await scanAuthorizedAttachments({
118
+ auth,
119
+ context: accessContext,
120
+ partitions,
121
+ limit: 0,
122
+ collectTags: true,
123
+ deadline,
124
+ loadBatch: (batchOffset, limit) => facetQuery.clone().limit(limit).offset(batchOffset).getResultList()
125
+ });
126
+ authorizedTags = facets.tags;
127
+ } else {
128
+ ;
129
+ [records, total] = await Promise.all([qb.getResultList(), countQb.getCount("a.id", true)]);
130
+ }
99
131
  const partitionTitleByCode = partitions.reduce((acc, entry) => {
100
132
  if (entry.code) acc[entry.code] = entry.title ?? entry.code;
101
133
  return acc;
@@ -135,13 +167,16 @@ async function GET(req) {
135
167
  assignments: applyAssignmentEnrichments(item.assignments ?? [], enrichments)
136
168
  })) : items;
137
169
  const totalPages = Math.max(1, Math.ceil(Number(total) / pageSize));
138
- const db = em.getKysely();
139
- let tagQuery = db.selectFrom("attachments").select(sql`distinct jsonb_array_elements_text(coalesce(storage_metadata->'tags', '[]'::jsonb))`.as("tag")).where("tenant_id", "=", auth.tenantId);
140
- if (auth.orgId) {
141
- tagQuery = tagQuery.where("organization_id", "=", auth.orgId);
170
+ let availableTags = authorizedTags ?? [];
171
+ if (!protectedScan) {
172
+ const db = em.getKysely();
173
+ let tagQuery = db.selectFrom("attachments").select(sql`distinct jsonb_array_elements_text(coalesce(storage_metadata->'tags', '[]'::jsonb))`.as("tag")).where("tenant_id", "=", auth.tenantId);
174
+ if (auth.orgId) {
175
+ tagQuery = tagQuery.where("organization_id", "=", auth.orgId);
176
+ }
177
+ const tagRows = await tagQuery.orderBy("tag", "asc").execute();
178
+ availableTags = tagRows.map((row) => typeof row.tag === "string" ? row.tag.trim() : "").filter((tag) => tag.length > 0);
142
179
  }
143
- const tagRows = await tagQuery.orderBy("tag", "asc").execute();
144
- const availableTags = tagRows.map((row) => typeof row.tag === "string" ? row.tag.trim() : "").filter((tag) => tag.length > 0);
145
180
  return NextResponse.json({
146
181
  items: enrichedItems,
147
182
  page,
@@ -155,8 +190,9 @@ async function GET(req) {
155
190
  description: entry.description ?? null,
156
191
  isPublic: entry.isPublic ?? false
157
192
  }))
158
- });
193
+ }, { headers: protectedScan ? { "Cache-Control": "private, no-store" } : void 0 });
159
194
  }
195
+ const GET = withAttachmentAccessErrors(listAttachments);
160
196
  const openApi = {
161
197
  tag: attachmentsTag,
162
198
  summary: "Attachment library management",
@@ -170,7 +206,10 @@ const openApi = {
170
206
  ],
171
207
  errors: [
172
208
  { status: 400, description: "Invalid query parameters", schema: attachmentErrorSchema },
173
- { status: 401, description: "Unauthorized", schema: attachmentErrorSchema }
209
+ { status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
210
+ { status: 403, description: "Owner policy denies access", schema: attachmentErrorSchema },
211
+ { status: 404, description: "Attachment or owner is unavailable", schema: attachmentErrorSchema },
212
+ { status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
174
213
  ]
175
214
  }
176
215
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/attachments/api/library/route.ts"],
4
- "sourcesContent": ["import { 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 { sql } from 'kysely'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '../../lib/imageUrls'\nimport { readAttachmentMetadata } from '../../lib/metadata'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../lib/assignmentDetails'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { ensureDefaultPartitions } from '../../lib/partitions'\nimport {\n attachmentsTag,\n attachmentListQuerySchema as openApiListQuerySchema,\n attachmentListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst listQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n partition: z.string().optional(),\n tags: z.string().optional(),\n sortField: z.enum(['fileName', 'fileSize', 'createdAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n}\n\nfunction buildTagFilter(raw?: string): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((tag) => tag.trim())\n .filter((tag) => tag.length > 0)\n}\n\nfunction formatDateValue(value: unknown): string {\n const toDate = (): Date => {\n if (value instanceof Date) return value\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed\n }\n const fallback = new Date(value as any)\n if (!Number.isNaN(fallback.getTime())) return fallback\n return new Date()\n }\n return toDate().toISOString()\n}\n\nexport async function GET(req: Request) {\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 url = new URL(req.url)\n const parsed = listQuerySchema.safeParse(Object.fromEntries(url.searchParams.entries()))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query' }, { status: 400 })\n }\n\n const { page, pageSize, search, partition, tags, sortField, sortDir } = parsed.data\n const tagList = buildTagFilter(tags)\n const offset = (page - 1) * pageSize\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n await ensureDefaultPartitions(em)\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const qb = em.createQueryBuilder(Attachment, 'a')\n const baseFilter: Record<string, unknown> = { tenantId: auth.tenantId }\n if (auth.orgId) {\n baseFilter.organizationId = auth.orgId\n }\n qb.where(baseFilter)\n if (search && search.trim().length > 0) {\n qb.andWhere({ fileName: { $ilike: buildIlikeTerm(search.trim()) } })\n }\n if (partition && partition.trim().length > 0) {\n qb.andWhere({ partitionCode: partition.trim() })\n }\n if (tagList.length > 0) {\n qb.andWhere(`coalesce(a.storage_metadata->'tags', '[]'::jsonb) @> ?::jsonb`, [JSON.stringify(tagList)])\n }\n const countQb = qb.clone()\n const orderMap: Record<string, string> = {\n fileName: 'a.file_name',\n fileSize: 'a.file_size',\n createdAt: 'a.created_at',\n }\n const orderColumn = orderMap[sortField ?? 'createdAt'] ?? 'a.created_at'\n qb.orderBy({ [orderColumn]: sortDir === 'asc' ? 'asc' : 'desc' })\n qb.limit(pageSize).offset(offset)\n\n const partitionsPromise = em.find(\n AttachmentPartition,\n {},\n { orderBy: { title: 'asc' }, fields: ['code', 'title', 'description'] as any },\n )\n const [records, total, partitions] = await Promise.all([qb.getResultList(), countQb.getCount('a.id', true), partitionsPromise])\n const partitionTitleByCode = partitions.reduce<Record<string, string>>((acc, entry) => {\n if (entry.code) acc[entry.code] = entry.title ?? entry.code\n return acc\n }, {})\n const items = records.map((record) => {\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const fileName = record.fileName || ''\n const isImage = typeof record.mimeType === 'string' && record.mimeType.toLowerCase().startsWith('image/')\n const thumbnailUrl = isImage\n ? buildAttachmentImageUrl(record.id, {\n width: 200,\n height: 200,\n slug: slugifyAttachmentFileName(fileName),\n })\n : undefined\n return {\n id: record.id,\n fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partitionTitleByCode[record.partitionCode] ?? null,\n url: record.url,\n createdAt: formatDateValue(record.createdAt),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n thumbnailUrl,\n content: record.content && record.content.trim() ? record.content : null,\n }\n })\n\n const allAssignments = items.flatMap((item) => item.assignments ?? [])\n const enrichments = await resolveAssignmentEnrichments(allAssignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedItems = enrichments.size\n ? items.map((item) => ({\n ...item,\n assignments: applyAssignmentEnrichments(item.assignments ?? [], enrichments),\n }))\n : items\n\n const totalPages = Math.max(1, Math.ceil(Number(total) / pageSize))\n const db = em.getKysely<any>() as any\n let tagQuery = db\n .selectFrom('attachments')\n .select(sql<string>`distinct jsonb_array_elements_text(coalesce(storage_metadata->'tags', '[]'::jsonb))`.as('tag'))\n .where('tenant_id', '=', auth.tenantId)\n if (auth.orgId) {\n tagQuery = tagQuery.where('organization_id', '=', auth.orgId)\n }\n const tagRows = await tagQuery.orderBy('tag', 'asc').execute() as Array<{ tag?: string | null }>\n const availableTags = tagRows\n .map((row) => (typeof row.tag === 'string' ? row.tag.trim() : ''))\n .filter((tag) => tag.length > 0)\n\n return NextResponse.json({\n items: enrichedItems,\n page,\n pageSize,\n total,\n totalPages,\n availableTags,\n partitions: partitions.map((entry) => ({\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n })),\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment library management',\n methods: {\n GET: {\n summary: 'List attachments',\n description: 'Returns paginated list of attachments with optional filtering by search term, partition, and tags. Includes available tags and partitions.',\n query: openApiListQuerySchema,\n responses: [\n { status: 200, description: 'Attachments list with pagination and metadata', schema: attachmentListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createAttachmentAccessContext } from '../../lib/access-runner'\nimport { requiresAttachmentAccessScan, scanAuthorizedAttachments } from '../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../lib/access-errors'\nimport { 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 { sql } from 'kysely'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '../../lib/imageUrls'\nimport { readAttachmentMetadata } from '../../lib/metadata'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../lib/assignmentDetails'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { ensureDefaultPartitions } from '../../lib/partitions'\nimport {\n attachmentsTag,\n attachmentListQuerySchema as openApiListQuerySchema,\n attachmentListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst listQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n partition: z.string().optional(),\n tags: z.string().optional(),\n sortField: z.enum(['fileName', 'fileSize', 'createdAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n}\n\nfunction buildTagFilter(raw?: string): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((tag) => tag.trim())\n .filter((tag) => tag.length > 0)\n}\n\nfunction formatDateValue(value: unknown): string {\n const toDate = (): Date => {\n if (value instanceof Date) return value\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed\n }\n const fallback = new Date(value as any)\n if (!Number.isNaN(fallback.getTime())) return fallback\n return new Date()\n }\n return toDate().toISOString()\n}\n\nasync function listAttachments(req: Request) {\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 url = new URL(req.url)\n const parsed = listQuerySchema.safeParse(Object.fromEntries(url.searchParams.entries()))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query' }, { status: 400 })\n }\n\n const { page, pageSize, search, partition, tags, sortField, sortDir } = parsed.data\n const tagList = buildTagFilter(tags)\n const offset = (page - 1) * pageSize\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n await ensureDefaultPartitions(em)\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const qb = em.createQueryBuilder(Attachment, 'a')\n const baseFilter: Record<string, unknown> = { tenantId: auth.tenantId }\n if (auth.orgId) {\n baseFilter.organizationId = auth.orgId\n }\n qb.where(baseFilter)\n if (search && search.trim().length > 0) {\n qb.andWhere({ fileName: { $ilike: buildIlikeTerm(search.trim()) } })\n }\n if (partition && partition.trim().length > 0) {\n qb.andWhere({ partitionCode: partition.trim() })\n }\n if (tagList.length > 0) {\n qb.andWhere(`coalesce(a.storage_metadata->'tags', '[]'::jsonb) @> ?::jsonb`, [JSON.stringify(tagList)])\n }\n const countQb = qb.clone()\n const orderMap: Record<string, string> = {\n fileName: 'a.file_name',\n fileSize: 'a.file_size',\n createdAt: 'a.created_at',\n }\n const orderColumn = orderMap[sortField ?? 'createdAt'] ?? 'a.created_at'\n qb.orderBy({ [orderColumn]: sortDir === 'asc' ? 'asc' : 'desc', 'a.id': 'asc' })\n qb.limit(pageSize).offset(offset)\n\n const partitions = await em.find(AttachmentPartition, {}, { orderBy: { title: 'asc' } })\n const protectedScan = requiresAttachmentAccessScan(partitions)\n const accessContext = createAttachmentAccessContext(container)\n const deadline = Date.now() + 15_000\n let records: Attachment[]\n let total: number\n let authorizedTags: string[] | null = null\n if (protectedScan) {\n const scanned = await scanAuthorizedAttachments({\n auth, context: accessContext, partitions, offset, limit: pageSize, deadline,\n loadBatch: (batchOffset, limit) => qb.clone().limit(limit).offset(batchOffset).getResultList(),\n })\n records = scanned.records\n total = scanned.total\n const facetQuery = em.createQueryBuilder(Attachment, 'a').where(baseFilter).orderBy({ 'a.id': 'asc' })\n const facets = await scanAuthorizedAttachments({\n auth, context: accessContext, partitions, limit: 0, collectTags: true, deadline,\n loadBatch: (batchOffset, limit) => facetQuery.clone().limit(limit).offset(batchOffset).getResultList(),\n })\n authorizedTags = facets.tags\n } else {\n ;[records, total] = await Promise.all([qb.getResultList(), countQb.getCount('a.id', true)])\n }\n const partitionTitleByCode = partitions.reduce<Record<string, string>>((acc, entry) => {\n if (entry.code) acc[entry.code] = entry.title ?? entry.code\n return acc\n }, {})\n const items = records.map((record) => {\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const fileName = record.fileName || ''\n const isImage = typeof record.mimeType === 'string' && record.mimeType.toLowerCase().startsWith('image/')\n const thumbnailUrl = isImage\n ? buildAttachmentImageUrl(record.id, {\n width: 200,\n height: 200,\n slug: slugifyAttachmentFileName(fileName),\n })\n : undefined\n return {\n id: record.id,\n fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partitionTitleByCode[record.partitionCode] ?? null,\n url: record.url,\n createdAt: formatDateValue(record.createdAt),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n thumbnailUrl,\n content: record.content && record.content.trim() ? record.content : null,\n }\n })\n\n const allAssignments = items.flatMap((item) => item.assignments ?? [])\n const enrichments = await resolveAssignmentEnrichments(allAssignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedItems = enrichments.size\n ? items.map((item) => ({\n ...item,\n assignments: applyAssignmentEnrichments(item.assignments ?? [], enrichments),\n }))\n : items\n\n const totalPages = Math.max(1, Math.ceil(Number(total) / pageSize))\n let availableTags = authorizedTags ?? []\n if (!protectedScan) {\n const db = em.getKysely<any>() as any\n let tagQuery = db\n .selectFrom('attachments')\n .select(sql<string>`distinct jsonb_array_elements_text(coalesce(storage_metadata->'tags', '[]'::jsonb))`.as('tag'))\n .where('tenant_id', '=', auth.tenantId)\n if (auth.orgId) {\n tagQuery = tagQuery.where('organization_id', '=', auth.orgId)\n }\n const tagRows = await tagQuery.orderBy('tag', 'asc').execute() as Array<{ tag?: string | null }>\n availableTags = tagRows\n .map((row) => (typeof row.tag === 'string' ? row.tag.trim() : ''))\n .filter((tag) => tag.length > 0)\n }\n\n return NextResponse.json({\n items: enrichedItems,\n page,\n pageSize,\n total,\n totalPages,\n availableTags,\n partitions: partitions.map((entry) => ({\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n })),\n }, { headers: protectedScan ? { 'Cache-Control': 'private, no-store' } : undefined })\n}\n\nexport const GET = withAttachmentAccessErrors(listAttachments)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment library management',\n methods: {\n GET: {\n summary: 'List attachments',\n description: 'Returns paginated list of attachments with optional filtering by search term, partition, and tags. Includes available tags and partitions.',\n query: openApiListQuerySchema,\n responses: [\n { status: 200, description: 'Attachments list with pagination and metadata', schema: attachmentListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment or owner is unavailable', 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"]
7
7
  }
@@ -6,6 +6,7 @@ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/
6
6
  import { Attachment, AttachmentPartition } from "../../data/entities.js";
7
7
  import { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from "../../lib/partitions.js";
8
8
  import { resolvePartitionEnvKey } from "../../lib/partitionEnv.js";
9
+ import { getDeclaredAttachmentAccessRequirements } from "../../lib/access-protection.js";
9
10
  import { resolveDefaultAttachmentOcrEnabled } from "../../lib/ocrConfig.js";
10
11
  import {
11
12
  attachmentsTag,
@@ -113,6 +114,7 @@ async function POST(req) {
113
114
  }
114
115
  const entry = em.create(AttachmentPartition, {
115
116
  code,
117
+ accessResolverRequirements: getDeclaredAttachmentAccessRequirements(code),
116
118
  title: parsed.data.title.trim(),
117
119
  description: parsed.data.description?.trim() ?? null,
118
120
  storageDriver: parsed.data.storageDriver ?? "local",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/attachments/api/partitions/route.ts"],
4
- "sourcesContent": ["import { 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 { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\ntype AuthLike = { sub?: string; tenantId?: string | null; orgId?: string | null; isSuperAdmin?: boolean } | null | undefined\n\nfunction isSuperAdmin(auth: AuthLike): boolean {\n return auth?.isSuperAdmin === true\n}\n\nfunction canManagePartition(auth: AuthLike, entry: AttachmentPartition): boolean {\n if (!auth?.tenantId) return false\n if (entry.tenantId && entry.tenantId === auth.tenantId) return true\n if (entry.tenantId == null && isSuperAdmin(auth)) return true\n return false\n}\n\nfunction partitionVisibilityFilter(auth: AuthLike) {\n if (isSuperAdmin(auth)) return {}\n return { $or: [{ tenantId: null }, { tenantId: auth?.tenantId ?? null }] }\n}\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n storageDriver: entry.storageDriver ?? 'local',\n configJson: entry.configJson ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await findWithDecryption<AttachmentPartition>(\n em,\n AttachmentPartition,\n partitionVisibilityFilter(auth),\n { orderBy: { createdAt: 'asc' } },\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { code },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: parsed.data.storageDriver ?? 'local',\n configJson: parsed.data.configJson ?? null,\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n if (parsed.data.storageDriver !== undefined) {\n entry.storageDriver = parsed.data.storageDriver\n }\n if (parsed.data.configJson !== undefined) {\n entry.configJson = parsed.data.configJson ?? null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usageFilter: Record<string, unknown> = { partitionCode: entry.code }\n if (entry.tenantId) usageFilter.tenantId = entry.tenantId\n const usage = await em.count(Attachment, usageFilter)\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { 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 { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport { getDeclaredAttachmentAccessRequirements } from '../../lib/access-protection'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\ntype AuthLike = { sub?: string; tenantId?: string | null; orgId?: string | null; isSuperAdmin?: boolean } | null | undefined\n\nfunction isSuperAdmin(auth: AuthLike): boolean {\n return auth?.isSuperAdmin === true\n}\n\nfunction canManagePartition(auth: AuthLike, entry: AttachmentPartition): boolean {\n if (!auth?.tenantId) return false\n if (entry.tenantId && entry.tenantId === auth.tenantId) return true\n if (entry.tenantId == null && isSuperAdmin(auth)) return true\n return false\n}\n\nfunction partitionVisibilityFilter(auth: AuthLike) {\n if (isSuperAdmin(auth)) return {}\n return { $or: [{ tenantId: null }, { tenantId: auth?.tenantId ?? null }] }\n}\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n storageDriver: entry.storageDriver ?? 'local',\n configJson: entry.configJson ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await findWithDecryption<AttachmentPartition>(\n em,\n AttachmentPartition,\n partitionVisibilityFilter(auth),\n { orderBy: { createdAt: 'asc' } },\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { code },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n accessResolverRequirements: getDeclaredAttachmentAccessRequirements(code),\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: parsed.data.storageDriver ?? 'local',\n configJson: parsed.data.configJson ?? null,\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n if (parsed.data.storageDriver !== undefined) {\n entry.storageDriver = parsed.data.storageDriver\n }\n if (parsed.data.configJson !== undefined) {\n entry.configJson = parsed.data.configJson ?? null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usageFilter: Record<string, unknown> = { partitionCode: entry.code }\n if (entry.tenantId) usageFilter.tenantId = entry.tenantId\n const usage = await em.count(Attachment, usageFilter)\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,17 @@
1
+ import { LockMode } from "@mikro-orm/core";
2
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { prepareAttachmentMutation } from "../lib/access-mutation.js";
4
+ import { throwAttachmentAccessError } from "../lib/access-errors.js";
5
+ import { createAttachmentAccessContext } from "../lib/access-runner.js";
6
+ import { assertAttachmentOwnerAccess, requiresAttachmentAccessScan, scanAuthorizedAttachments } from "../lib/access-query.js";
7
+ import { withAttachmentAccessErrors } from "../lib/access-errors.js";
1
8
  import { NextResponse } from "next/server";
2
9
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
10
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
11
  import { z } from "zod";
5
12
  import { sql } from "kysely";
6
13
  import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from "../lib/imageUrls.js";
14
+ import { ensureAttachmentPartitionProtection } from "../lib/access-protection.js";
7
15
  import { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from "../lib/partitions.js";
8
16
  import { Attachment, AttachmentPartition } from "../data/entities.js";
9
17
  import { extractAttachmentContent } from "../lib/textExtraction.js";
@@ -11,7 +19,7 @@ import { requestOcrProcessing } from "../lib/ocrQueue.js";
11
19
  import { StorageDriverFactory } from "../lib/drivers/index.js";
12
20
  import { OcrService, shouldUseLlmOcr } from "../lib/ocrService.js";
13
21
  import { clearAttachmentThumbnailCache } from "../lib/thumbnailCache.js";
14
- import { removeAttachmentRecord } from "../lib/storageReferences.js";
22
+ import { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from "../lib/storageReferences.js";
15
23
  import { assertAttachmentScopeInvariant } from "../lib/access.js";
16
24
  import { resolveAttachmentRequestScope } from "../lib/requestScope.js";
17
25
  import {
@@ -168,7 +176,7 @@ function parseFormAssignments(value) {
168
176
  return [];
169
177
  }
170
178
  }
171
- async function GET(req) {
179
+ async function listRecordAttachments(req) {
172
180
  const auth = await getAuthFromRequest(req);
173
181
  if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
174
182
  const url = new URL(req.url);
@@ -191,27 +199,33 @@ async function GET(req) {
191
199
  if (orgId) filter.organizationId = orgId;
192
200
  const orderBy = { createdAt: "DESC", id: "DESC" };
193
201
  const usePaging = typeof page === "number" && typeof pageSize === "number";
194
- const total = usePaging ? await em.count(Attachment, filter) : null;
195
202
  const currentPage = usePaging ? Math.max(1, page) : null;
196
203
  const currentPageSize = usePaging ? pageSize : null;
197
- const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize)) : null;
198
204
  const pageOffset = usePaging ? (currentPage - 1) * currentPageSize : void 0;
199
- const items = await findWithDecryption(
200
- em,
201
- Attachment,
202
- filter,
203
- {
205
+ const partitions = await em.find(AttachmentPartition, {});
206
+ const protectedScan = requiresAttachmentAccessScan(partitions);
207
+ const scope = { tenantId: auth.tenantId ?? null, organizationId: orgId ?? null };
208
+ let items;
209
+ let total;
210
+ if (protectedScan) {
211
+ const scanned = await scanAuthorizedAttachments({
212
+ auth: { ...auth, orgId },
213
+ context: createAttachmentAccessContext(container),
214
+ partitions,
215
+ offset: pageOffset,
216
+ limit: currentPageSize ?? ATTACHMENT_LIST_MAX_PAGE_SIZE,
217
+ loadBatch: (offset, limit) => findWithDecryption(em, Attachment, filter, { orderBy, offset, limit }, scope)
218
+ });
219
+ items = scanned.records;
220
+ total = usePaging ? scanned.total : null;
221
+ } else {
222
+ total = usePaging ? await em.count(Attachment, filter) : null;
223
+ items = await findWithDecryption(em, Attachment, filter, {
204
224
  orderBy,
205
- ...usePaging ? {
206
- limit: currentPageSize,
207
- offset: pageOffset
208
- } : { limit: ATTACHMENT_LIST_MAX_PAGE_SIZE }
209
- },
210
- {
211
- tenantId: auth.tenantId ?? null,
212
- organizationId: orgId ?? null
213
- }
214
- );
225
+ ...usePaging ? { limit: currentPageSize, offset: pageOffset } : { limit: ATTACHMENT_LIST_MAX_PAGE_SIZE }
226
+ }, scope);
227
+ }
228
+ const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize)) : null;
215
229
  return NextResponse.json({
216
230
  items: items.map((a) => {
217
231
  const metadata2 = readAttachmentMetadata(a.storageMetadata);
@@ -239,8 +253,9 @@ async function GET(req) {
239
253
  pageSize: currentPageSize,
240
254
  totalPages
241
255
  } : {}
242
- });
256
+ }, { headers: protectedScan ? { "Cache-Control": "private, no-store" } : void 0 });
243
257
  }
258
+ const GET = withAttachmentAccessErrors(listRecordAttachments);
244
259
  async function POST(req) {
245
260
  const { t } = await resolveTranslations();
246
261
  const auth = await getAuthFromRequest(req);
@@ -403,6 +418,7 @@ async function POST(req) {
403
418
  if (!partition) {
404
419
  return NextResponse.json({ error: "Storage partition is not configured." }, { status: 400 });
405
420
  }
421
+ await ensureAttachmentPartitionProtection(em, partition);
406
422
  const requestedPublicOverride = typeof partitionOverride === "string" && partitionOverride.length > 0 && partition.code === partitionOverride && partition.isPublic === true && partition.code !== defaultPartitionCode && partition.code !== partitionFromField;
407
423
  if (requestedPublicOverride) {
408
424
  return NextResponse.json({ error: t("attachments.errors.publicPartitionBlocked", "Public storage partitions cannot be selected explicitly for this upload.") }, { status: 403 });
@@ -621,7 +637,7 @@ async function readTenantAttachmentUsageBytes(em, tenantId) {
621
637
  throw new Error("Attachment quota accounting is unavailable.", { cause: error });
622
638
  }
623
639
  }
624
- async function DELETE(req) {
640
+ async function deleteAttachment(req) {
625
641
  const auth = await getAuthFromRequest(req);
626
642
  if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
627
643
  const url = new URL(req.url);
@@ -636,20 +652,37 @@ async function DELETE(req) {
636
652
  const orgId = deleteScope.organizationId;
637
653
  const deleteFilter = { id, tenantId: auth.tenantId };
638
654
  if (orgId) deleteFilter.organizationId = orgId;
639
- const record = await em.findOne(Attachment, deleteFilter);
640
- if (!record) return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
641
- const removal = await removeAttachmentRecord(em, record);
642
- if (!removal.removed) return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
655
+ const scopedAuth = { ...auth, orgId };
656
+ const outcome = await em.transactional(async (tx) => {
657
+ const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, void 0, {
658
+ tenantId: auth.tenantId,
659
+ organizationId: orgId
660
+ });
661
+ if (!candidate) return throwAttachmentAccessError(404);
662
+ const referenceIds = await lockAttachmentStorageReferences(tx, candidate);
663
+ const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id);
664
+ if (!removal.removed) return throwAttachmentAccessError(404);
665
+ const record2 = await findOneWithDecryption(tx, Attachment, deleteFilter, {
666
+ lockMode: LockMode.PESSIMISTIC_WRITE,
667
+ refresh: true
668
+ }, { tenantId: auth.tenantId, organizationId: orgId });
669
+ if (!record2) return throwAttachmentAccessError(404);
670
+ const guard = await prepareAttachmentMutation({ container, req, auth: scopedAuth, recordId: record2.id, operation: "delete" });
671
+ await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth: scopedAuth, attachment: record2, action: "delete" });
672
+ const driver = record2.storagePath ? await storageDriverFactory.resolveForPartition(record2.partitionCode, {
673
+ tenantId: record2.tenantId ?? auth.tenantId,
674
+ organizationId: record2.organizationId ?? orgId ?? ""
675
+ }) : null;
676
+ tx.remove(record2);
677
+ await tx.flush();
678
+ return { record: record2, driver, guard, releaseStorage: removal.releaseStorage };
679
+ });
680
+ const { record } = outcome;
643
681
  await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {
644
682
  logger.error("Failed to cleanup cached thumbnails", { err: error });
645
683
  });
646
- if (record.storagePath && removal.releaseStorage) {
647
- const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
648
- tenantId: record.tenantId ?? auth.tenantId,
649
- organizationId: record.organizationId ?? orgId ?? ""
650
- });
651
- await delDriver.delete(record.partitionCode, record.storagePath);
652
- }
684
+ if (outcome.driver && outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath);
685
+ await outcome.guard.runAfterSuccess();
653
686
  if (dataEngine) {
654
687
  await emitCrudSideEffects({
655
688
  dataEngine,
@@ -667,6 +700,7 @@ async function DELETE(req) {
667
700
  }
668
701
  return NextResponse.json({ ok: true });
669
702
  }
703
+ const DELETE = withAttachmentAccessErrors(deleteAttachment);
670
704
  const openApi = {
671
705
  summary: "Manage entity attachments",
672
706
  description: "Upload and list attachments associated with module entities and records.",
@@ -680,7 +714,10 @@ const openApi = {
680
714
  ],
681
715
  errors: [
682
716
  { status: 400, description: "Missing entity or record identifiers", schema: errorSchema },
683
- { status: 401, description: "Unauthorized", schema: errorSchema }
717
+ { status: 401, description: "Unauthorized", schema: errorSchema },
718
+ { status: 403, description: "Owner policy denies access", schema: errorSchema },
719
+ { status: 404, description: "Attachment or owner is unavailable", schema: errorSchema },
720
+ { status: 504, description: "Owner authorization timed out", schema: errorSchema }
684
721
  ]
685
722
  },
686
723
  POST: {
@@ -709,7 +746,10 @@ const openApi = {
709
746
  ],
710
747
  errors: [
711
748
  { status: 400, description: "Missing attachment identifier", schema: errorSchema },
712
- { status: 401, description: "Unauthorized", schema: errorSchema }
749
+ { status: 401, description: "Unauthorized", schema: errorSchema },
750
+ { status: 403, description: "Owner policy denies access", schema: errorSchema },
751
+ { status: 404, description: "Attachment or owner is unavailable", schema: errorSchema },
752
+ { status: 504, description: "Owner authorization timed out", schema: errorSchema }
713
753
  ]
714
754
  }
715
755
  }