@open-mercato/core 0.10.1-develop.7741.1.61a2652671 → 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 (154) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/attachment_partition/index.js +2 -0
  3. package/dist/generated/entities/attachment_partition/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
  7. package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
  8. package/dist/modules/attachments/api/file/[id]/route.js +16 -10
  9. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  10. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
  11. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  12. package/dist/modules/attachments/api/library/[id]/route.js +100 -56
  13. package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
  14. package/dist/modules/attachments/api/library/route.js +56 -17
  15. package/dist/modules/attachments/api/library/route.js.map +2 -2
  16. package/dist/modules/attachments/api/partitions/route.js +2 -0
  17. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  18. package/dist/modules/attachments/api/route.js +74 -34
  19. package/dist/modules/attachments/api/route.js.map +3 -3
  20. package/dist/modules/attachments/api/transfer/route.js +63 -37
  21. package/dist/modules/attachments/api/transfer/route.js.map +2 -2
  22. package/dist/modules/attachments/data/entities.js +3 -0
  23. package/dist/modules/attachments/data/entities.js.map +2 -2
  24. package/dist/modules/attachments/data/validators.js +41 -0
  25. package/dist/modules/attachments/data/validators.js.map +7 -0
  26. package/dist/modules/attachments/di.js +5 -2
  27. package/dist/modules/attachments/di.js.map +2 -2
  28. package/dist/modules/attachments/generators.js +29 -0
  29. package/dist/modules/attachments/generators.js.map +7 -0
  30. package/dist/modules/attachments/index.js +8 -1
  31. package/dist/modules/attachments/index.js.map +2 -2
  32. package/dist/modules/attachments/lib/access-errors.js +35 -0
  33. package/dist/modules/attachments/lib/access-errors.js.map +7 -0
  34. package/dist/modules/attachments/lib/access-mutation.js +23 -0
  35. package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
  36. package/dist/modules/attachments/lib/access-protection.js +73 -0
  37. package/dist/modules/attachments/lib/access-protection.js.map +7 -0
  38. package/dist/modules/attachments/lib/access-query.js +74 -0
  39. package/dist/modules/attachments/lib/access-query.js.map +7 -0
  40. package/dist/modules/attachments/lib/access-registry.js +87 -0
  41. package/dist/modules/attachments/lib/access-registry.js.map +7 -0
  42. package/dist/modules/attachments/lib/access-runner.js +161 -0
  43. package/dist/modules/attachments/lib/access-runner.js.map +7 -0
  44. package/dist/modules/attachments/lib/access-types.js +1 -0
  45. package/dist/modules/attachments/lib/access-types.js.map +7 -0
  46. package/dist/modules/attachments/lib/attachment-service.js +14 -6
  47. package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
  48. package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
  49. package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
  50. package/dist/modules/attachments/lib/partitions.js +3 -2
  51. package/dist/modules/attachments/lib/partitions.js.map +2 -2
  52. package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
  53. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  54. package/dist/modules/attachments/lib/target-access-service.js +12 -3
  55. package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
  56. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
  57. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
  58. package/dist/modules/attachments/setup.js +10 -0
  59. package/dist/modules/attachments/setup.js.map +2 -2
  60. package/dist/modules/notifications/api/[id]/action/route.js +8 -0
  61. package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
  62. package/dist/modules/notifications/api/route.js +17 -0
  63. package/dist/modules/notifications/api/route.js.map +2 -2
  64. package/dist/modules/notifications/data/validators.js +1 -1
  65. package/dist/modules/notifications/data/validators.js.map +2 -2
  66. package/dist/modules/notifications/lib/notificationService.js +45 -3
  67. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  68. package/dist/modules/notifications/lib/routeHelpers.js +27 -0
  69. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  70. package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
  71. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  72. package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
  73. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  74. package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
  75. package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
  76. package/dist/modules/staff/notifications.js +4 -2
  77. package/dist/modules/staff/notifications.js.map +2 -2
  78. package/dist/modules/workflows/data/entities.js.map +2 -2
  79. package/dist/modules/workflows/lib/activity-executor.js +18 -4
  80. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  81. package/dist/modules/workflows/lib/activity-types.js +2 -2
  82. package/dist/modules/workflows/lib/activity-types.js.map +2 -2
  83. package/dist/modules/workflows/lib/condition-handler.js +7 -1
  84. package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
  85. package/dist/modules/workflows/lib/duration.js +13 -0
  86. package/dist/modules/workflows/lib/duration.js.map +2 -2
  87. package/dist/modules/workflows/lib/step-handler.js +9 -2
  88. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  89. package/dist/modules/workflows/lib/task-quick-action.js +13 -10
  90. package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
  91. package/dist/modules/workflows/lib/transition-handler.js +2 -1
  92. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  93. package/dist/modules/workflows/notifications.js +4 -0
  94. package/dist/modules/workflows/notifications.js.map +2 -2
  95. package/generated/entities/attachment_partition/index.ts +1 -0
  96. package/generated/entity-fields-registry.ts +1 -0
  97. package/package.json +7 -7
  98. package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
  99. package/src/modules/attachments/api/file/[id]/route.ts +12 -9
  100. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
  101. package/src/modules/attachments/api/library/[id]/route.ts +87 -58
  102. package/src/modules/attachments/api/library/route.ts +40 -11
  103. package/src/modules/attachments/api/partitions/route.ts +2 -0
  104. package/src/modules/attachments/api/route.ts +68 -35
  105. package/src/modules/attachments/api/transfer/route.ts +65 -42
  106. package/src/modules/attachments/data/entities.ts +4 -0
  107. package/src/modules/attachments/data/validators.ts +36 -0
  108. package/src/modules/attachments/di.ts +4 -1
  109. package/src/modules/attachments/generators.ts +27 -0
  110. package/src/modules/attachments/i18n/de.json +4 -0
  111. package/src/modules/attachments/i18n/en.json +4 -0
  112. package/src/modules/attachments/i18n/es.json +4 -0
  113. package/src/modules/attachments/i18n/ko.json +4 -0
  114. package/src/modules/attachments/i18n/pl.json +4 -0
  115. package/src/modules/attachments/index.ts +18 -0
  116. package/src/modules/attachments/lib/access-errors.ts +32 -0
  117. package/src/modules/attachments/lib/access-mutation.ts +26 -0
  118. package/src/modules/attachments/lib/access-protection.ts +83 -0
  119. package/src/modules/attachments/lib/access-query.ts +86 -0
  120. package/src/modules/attachments/lib/access-registry.ts +117 -0
  121. package/src/modules/attachments/lib/access-runner.ts +183 -0
  122. package/src/modules/attachments/lib/access-types.ts +78 -0
  123. package/src/modules/attachments/lib/attachment-service.ts +8 -4
  124. package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
  125. package/src/modules/attachments/lib/partitions.ts +3 -2
  126. package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
  127. package/src/modules/attachments/lib/target-access-service.ts +10 -3
  128. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
  129. package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
  130. package/src/modules/attachments/setup.ts +10 -0
  131. package/src/modules/notifications/api/[id]/action/route.ts +8 -0
  132. package/src/modules/notifications/api/route.ts +17 -0
  133. package/src/modules/notifications/data/validators.ts +1 -1
  134. package/src/modules/notifications/i18n/de.json +2 -0
  135. package/src/modules/notifications/i18n/en.json +2 -0
  136. package/src/modules/notifications/i18n/es.json +2 -0
  137. package/src/modules/notifications/i18n/ko.json +2 -0
  138. package/src/modules/notifications/i18n/pl.json +2 -0
  139. package/src/modules/notifications/lib/notificationService.ts +67 -1
  140. package/src/modules/notifications/lib/routeHelpers.ts +38 -0
  141. package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
  142. package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
  143. package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
  144. package/src/modules/staff/notifications.ts +2 -0
  145. package/src/modules/workflows/AGENTS.md +2 -2
  146. package/src/modules/workflows/data/entities.ts +6 -0
  147. package/src/modules/workflows/lib/activity-executor.ts +36 -4
  148. package/src/modules/workflows/lib/activity-types.ts +2 -2
  149. package/src/modules/workflows/lib/condition-handler.ts +16 -1
  150. package/src/modules/workflows/lib/duration.ts +25 -0
  151. package/src/modules/workflows/lib/step-handler.ts +16 -1
  152. package/src/modules/workflows/lib/task-quick-action.ts +19 -10
  153. package/src/modules/workflows/lib/transition-handler.ts +2 -1
  154. package/src/modules/workflows/notifications.ts +4 -0
@@ -0,0 +1,144 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { expect } from "@playwright/test";
3
+ import { apiRequest, clearAuthTokenCache, getAuthToken, withCredentialIsolatedRequest } from "./api.js";
4
+ import { createRoleFixture, createUserFixture, deleteRoleIfExists, deleteUserIfExists, setRoleAclFeatures } from "./authFixtures.js";
5
+ import { deleteAttachmentIfExists, uploadAttachmentFixture } from "./attachmentsFixtures.js";
6
+ import { expectId, getTokenContext, readJsonSafe } from "./generalFixtures.js";
7
+ const OWNER_ACCESS_FEATURES = [
8
+ "documents.view",
9
+ "documents.edit",
10
+ "documents.create",
11
+ "documents.share",
12
+ "documents.delete",
13
+ "attachments.view",
14
+ "attachments.manage"
15
+ ];
16
+ const OWNER_ACCESS_PNG = Buffer.from(
17
+ "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEUlEQVQImWMwSpn2H4QZYAwASOwIrfM45AsAAAAASUVORK5CYII=",
18
+ "base64"
19
+ );
20
+ async function ownerPolicyRequest(method, path, token, data, scheme = "Bearer") {
21
+ return withCredentialIsolatedRequest(async (isolated) => {
22
+ const response = await isolated.fetch(path, {
23
+ method,
24
+ headers: { Authorization: `${scheme} ${token}`, "Content-Type": "application/json" },
25
+ ...data === void 0 ? {} : { data }
26
+ });
27
+ return {
28
+ status: response.status(),
29
+ headers: response.headers(),
30
+ body: await readJsonSafe(response),
31
+ bytes: await response.body()
32
+ };
33
+ });
34
+ }
35
+ async function withAttachmentOwnerFixture(request, run) {
36
+ const admin = await getAuthToken(request, "admin");
37
+ const scope = getTokenContext(admin);
38
+ const prefix = `attachment-owner-${randomUUID()}`;
39
+ const users = [], documents = [], attachments = [], keys = [];
40
+ let roleId = null;
41
+ try {
42
+ roleId = await createRoleFixture(request, admin, { name: prefix, tenantId: scope.tenantId });
43
+ await setRoleAclFeatures(request, admin, { roleId, features: OWNER_ACCESS_FEATURES });
44
+ const actor = async (label) => {
45
+ const email = `${prefix}-${label}@example.com`, password = `Owner1!${randomUUID()}`;
46
+ const id = await createUserFixture(request, admin, {
47
+ email,
48
+ password,
49
+ organizationId: scope.organizationId,
50
+ roles: [roleId],
51
+ name: label
52
+ });
53
+ users.push(id);
54
+ return { id, token: await getAuthToken(request, email, password), refreshToken: async () => {
55
+ clearAuthTokenCache();
56
+ return getAuthToken(request, email, password);
57
+ } };
58
+ };
59
+ const recipient = await actor("recipient"), unshared = await actor("unshared");
60
+ const document = async (label, token = admin) => {
61
+ const response = await apiRequest(request, "POST", "/api/documents", { token, data: { title: `${prefix}-${label}`, folderId: null } });
62
+ expect(response.status()).toBe(201);
63
+ const body = await readJsonSafe(response);
64
+ const id = expectId(body?.id, "Fixture response must include id");
65
+ documents.push(id);
66
+ return id;
67
+ };
68
+ const share = async (documentId, permission, principalType = "user", principalId = recipient.id, token = admin) => {
69
+ const response = await apiRequest(request, "POST", `/api/documents/${documentId}/shares`, {
70
+ token,
71
+ data: { principalType, principalId, permission }
72
+ });
73
+ expect(response.status()).toBe(201);
74
+ const body = await readJsonSafe(response);
75
+ expect(body?.updatedAt).toBeTruthy();
76
+ return { id: expectId(body?.id, "Fixture response must include id"), updatedAt: body.updatedAt };
77
+ };
78
+ const upload = async (documentId, label, options = {}) => {
79
+ const file = await uploadAttachmentFixture(request, admin, {
80
+ entityId: options.entityId ?? "documents:document",
81
+ recordId: documentId,
82
+ fileName: `${prefix}-${label}.txt`,
83
+ mimeType: "text/plain",
84
+ buffer: Buffer.from(`${prefix}-${label}-content`),
85
+ assignments: options.assignments,
86
+ tags: options.tags
87
+ });
88
+ attachments.push(file.id);
89
+ return file.id;
90
+ };
91
+ const uploadProxy = async (documentId) => {
92
+ const route = `/api/documents/${documentId}/attachments`;
93
+ const response = await request.fetch(process.env.BASE_URL?.trim() ? `${process.env.BASE_URL.trim()}${route}` : route, {
94
+ method: "POST",
95
+ headers: { Authorization: `Bearer ${admin}` },
96
+ multipart: { file: { name: `${prefix}-proxy.png`, mimeType: "image/png", buffer: OWNER_ACCESS_PNG } }
97
+ });
98
+ expect(response.status()).toBe(201);
99
+ const body = await readJsonSafe(response);
100
+ return { attachmentId: expectId(body?.attachmentId, "Document upload must include attachmentId"), url: String(body?.url) };
101
+ };
102
+ const key = async () => {
103
+ const response = await apiRequest(request, "POST", "/api/api_keys/keys", {
104
+ token: admin,
105
+ data: { name: prefix, tenantId: scope.tenantId, organizationId: scope.organizationId, roles: [roleId] }
106
+ });
107
+ expect(response.status()).toBe(201);
108
+ const body = await readJsonSafe(response);
109
+ const id = expectId(body?.id, "Fixture response must include id");
110
+ keys.push(id);
111
+ expect(body?.secret).toMatch(/^omk_/);
112
+ return { id, secret: body.secret };
113
+ };
114
+ const uploadPublicImage = async (documentId) => {
115
+ const file = await uploadAttachmentFixture(request, admin, {
116
+ entityId: "catalog:catalog_product",
117
+ recordId: randomUUID(),
118
+ fileName: `${prefix}-public.png`,
119
+ mimeType: "image/png",
120
+ buffer: OWNER_ACCESS_PNG,
121
+ assignments: [{ type: "documents:document", id: documentId }]
122
+ });
123
+ attachments.push(file.id);
124
+ expect(file.partitionCode).toBe("productsMedia");
125
+ return file.id;
126
+ };
127
+ const visible = await document("visible"), hidden = await document("hidden"), destination = await document("destination");
128
+ await share(visible, "viewer");
129
+ await run({ admin, roleId, prefix, recipient, unshared, visible, hidden, destination, document, share, upload, uploadProxy, uploadPublicImage, key });
130
+ } finally {
131
+ for (const id of keys) await apiRequest(request, "DELETE", `/api/api_keys/keys?id=${id}`, { token: admin }).catch(() => void 0);
132
+ for (const id of attachments) await deleteAttachmentIfExists(request, admin, id);
133
+ for (const id of documents.reverse()) await apiRequest(request, "DELETE", `/api/documents/${id}`, { token: admin }).catch(() => void 0);
134
+ for (const id of users) await deleteUserIfExists(request, admin, id);
135
+ await deleteRoleIfExists(request, admin, roleId);
136
+ }
137
+ }
138
+ export {
139
+ OWNER_ACCESS_FEATURES,
140
+ OWNER_ACCESS_PNG,
141
+ ownerPolicyRequest,
142
+ withAttachmentOwnerFixture
143
+ };
144
+ //# sourceMappingURL=attachmentAccessFixtures.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/helpers/integration/attachmentAccessFixtures.ts"],
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport { expect, type APIRequestContext } from '@playwright/test'\nimport { apiRequest, clearAuthTokenCache, getAuthToken, withCredentialIsolatedRequest } from './api'\nimport { createRoleFixture, createUserFixture, deleteRoleIfExists, deleteUserIfExists, setRoleAclFeatures } from './authFixtures'\nimport { deleteAttachmentIfExists, uploadAttachmentFixture } from './attachmentsFixtures'\nimport { expectId, getTokenContext, readJsonSafe } from './generalFixtures'\n\nexport const OWNER_ACCESS_FEATURES = [\n 'documents.view', 'documents.edit', 'documents.create', 'documents.share', 'documents.delete',\n 'attachments.view', 'attachments.manage',\n]\nexport const OWNER_ACCESS_PNG = Buffer.from(\n 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEUlEQVQImWMwSpn2H4QZYAwASOwIrfM45AsAAAAASUVORK5CYII=',\n 'base64',\n)\n\nexport type OwnerAccessItem = { id: string; tags?: string[]; assignments?: Array<{ type: string; id: string; label?: string | null }> }\nexport type OwnerAccessList = { items: OwnerAccessItem[]; total?: number; availableTags?: string[] }\nexport type OwnerAccessActor = { id: string; token: string; refreshToken: () => Promise<string> }\ntype Assignment = { type: string; id: string; label?: string }\ntype Share = { id: string; updatedAt: string }\n\nexport async function ownerPolicyRequest<Body = { error?: string }>(\n method: string, path: string, token: string, data?: unknown, scheme: 'Bearer' | 'ApiKey' = 'Bearer',\n) {\n return withCredentialIsolatedRequest(async (isolated) => {\n const response = await isolated.fetch(path, {\n method, headers: { Authorization: `${scheme} ${token}`, 'Content-Type': 'application/json' },\n ...(data === undefined ? {} : { data }),\n })\n return {\n status: response.status(), headers: response.headers(),\n body: await readJsonSafe<Body>(response), bytes: await response.body(),\n }\n })\n}\n\nexport type AttachmentOwnerFixture = {\n admin: string\n roleId: string\n prefix: string\n recipient: OwnerAccessActor\n unshared: OwnerAccessActor\n visible: string\n hidden: string\n destination: string\n document: (label: string, token?: string) => Promise<string>\n share: (documentId: string, permission: 'viewer' | 'editor', principalType?: 'user' | 'role', principalId?: string, token?: string) => Promise<Share>\n upload: (documentId: string, label: string, options?: { entityId?: string; assignments?: Assignment[]; tags?: string[] }) => Promise<string>\n uploadProxy: (documentId: string) => Promise<{ attachmentId: string; url: string }>\n uploadPublicImage: (documentId: string) => Promise<string>\n key: () => Promise<{ id: string; secret: string }>\n}\n\nexport async function withAttachmentOwnerFixture(\n request: APIRequestContext, run: (fixture: AttachmentOwnerFixture) => Promise<void>,\n): Promise<void> {\n const admin = await getAuthToken(request, 'admin')\n const scope = getTokenContext(admin)\n const prefix = `attachment-owner-${randomUUID()}`\n const users: string[] = [], documents: string[] = [], attachments: string[] = [], keys: string[] = []\n let roleId: string | null = null\n try {\n roleId = await createRoleFixture(request, admin, { name: prefix, tenantId: scope.tenantId })\n await setRoleAclFeatures(request, admin, { roleId, features: OWNER_ACCESS_FEATURES })\n const actor = async (label: string): Promise<OwnerAccessActor> => {\n const email = `${prefix}-${label}@example.com`, password = `Owner1!${randomUUID()}`\n const id = await createUserFixture(request, admin, {\n email, password, organizationId: scope.organizationId, roles: [roleId!], name: label,\n })\n users.push(id)\n return { id, token: await getAuthToken(request, email, password), refreshToken: async () => {\n clearAuthTokenCache()\n return getAuthToken(request, email, password)\n } }\n }\n const recipient = await actor('recipient'), unshared = await actor('unshared')\n const document = async (label: string, token = admin): Promise<string> => {\n const response = await apiRequest(request, 'POST', '/api/documents', { token, data: { title: `${prefix}-${label}`, folderId: null } })\n expect(response.status()).toBe(201)\n const body = await readJsonSafe<{ id: string }>(response)\n const id = expectId(body?.id, 'Fixture response must include id'); documents.push(id); return id\n }\n const share: AttachmentOwnerFixture['share'] = async (documentId, permission, principalType = 'user', principalId = recipient.id, token = admin) => {\n const response = await apiRequest(request, 'POST', `/api/documents/${documentId}/shares`, {\n token, data: { principalType, principalId, permission },\n })\n expect(response.status()).toBe(201)\n const body = await readJsonSafe<Share>(response)\n expect(body?.updatedAt).toBeTruthy()\n return { id: expectId(body?.id, 'Fixture response must include id'), updatedAt: body!.updatedAt }\n }\n const upload: AttachmentOwnerFixture['upload'] = async (documentId, label, options = {}) => {\n const file = await uploadAttachmentFixture(request, admin, {\n entityId: options.entityId ?? 'documents:document', recordId: documentId,\n fileName: `${prefix}-${label}.txt`, mimeType: 'text/plain', buffer: Buffer.from(`${prefix}-${label}-content`),\n assignments: options.assignments, tags: options.tags,\n })\n attachments.push(file.id); return file.id\n }\n const uploadProxy = async (documentId: string) => {\n const route = `/api/documents/${documentId}/attachments`\n const response = await request.fetch(process.env.BASE_URL?.trim() ? `${process.env.BASE_URL.trim()}${route}` : route, {\n method: 'POST', headers: { Authorization: `Bearer ${admin}` },\n multipart: { file: { name: `${prefix}-proxy.png`, mimeType: 'image/png', buffer: OWNER_ACCESS_PNG } },\n })\n expect(response.status()).toBe(201)\n const body = await readJsonSafe<{ attachmentId: string; url: string }>(response)\n return { attachmentId: expectId(body?.attachmentId, 'Document upload must include attachmentId'), url: String(body?.url) }\n }\n const key = async () => {\n const response = await apiRequest(request, 'POST', '/api/api_keys/keys', {\n token: admin, data: { name: prefix, tenantId: scope.tenantId, organizationId: scope.organizationId, roles: [roleId] },\n })\n expect(response.status()).toBe(201)\n const body = await readJsonSafe<{ id: string; secret: string }>(response)\n const id = expectId(body?.id, 'Fixture response must include id'); keys.push(id)\n expect(body?.secret).toMatch(/^omk_/)\n return { id, secret: body!.secret }\n }\n const uploadPublicImage = async (documentId: string) => {\n const file = await uploadAttachmentFixture(request, admin, {\n entityId: 'catalog:catalog_product', recordId: randomUUID(),\n fileName: `${prefix}-public.png`, mimeType: 'image/png', buffer: OWNER_ACCESS_PNG,\n assignments: [{ type: 'documents:document', id: documentId }],\n })\n attachments.push(file.id)\n expect(file.partitionCode).toBe('productsMedia')\n return file.id\n }\n const visible = await document('visible'), hidden = await document('hidden'), destination = await document('destination')\n await share(visible, 'viewer')\n await run({ admin, roleId, prefix, recipient, unshared, visible, hidden, destination, document, share, upload, uploadProxy, uploadPublicImage, key })\n } finally {\n for (const id of keys) await apiRequest(request, 'DELETE', `/api/api_keys/keys?id=${id}`, { token: admin }).catch(() => undefined)\n for (const id of attachments) await deleteAttachmentIfExists(request, admin, id)\n for (const id of documents.reverse()) await apiRequest(request, 'DELETE', `/api/documents/${id}`, { token: admin }).catch(() => undefined)\n for (const id of users) await deleteUserIfExists(request, admin, id)\n await deleteRoleIfExists(request, admin, roleId)\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,3 +1,5 @@
1
+ import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../lib/access-runner.js";
2
+ import { attachmentAccessErrorResponse } from "../../../lib/access-errors.js";
1
3
  import { NextResponse } from "next/server";
2
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
5
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
@@ -58,11 +60,17 @@ async function GET(req, context) {
58
60
  { status: 500 }
59
61
  );
60
62
  }
61
- const access = checkAttachmentAccess(scopedAuth, attachment, partition);
62
- if (!access.ok) {
63
- const message = access.status === 401 ? "Unauthorized" : "Forbidden";
64
- return NextResponse.json({ error: message }, { status: access.status });
65
- }
63
+ const url = new URL(req.url);
64
+ const forceDownload = url.searchParams.get("download") === "1";
65
+ const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);
66
+ const access = await evaluateAttachmentAccess({
67
+ auth: scopedAuth,
68
+ attachment,
69
+ partition,
70
+ action: renderInline ? "render" : "read",
71
+ context: createAttachmentAccessContext(container)
72
+ });
73
+ if (!access.ok) return attachmentAccessErrorResponse(access);
66
74
  const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {
67
75
  tenantId: attachment.tenantId ?? "",
68
76
  organizationId: attachment.organizationId ?? ""
@@ -74,11 +82,8 @@ async function GET(req, context) {
74
82
  } catch {
75
83
  return NextResponse.json({ error: "File not available" }, { status: 404 });
76
84
  }
77
- const url = new URL(req.url);
78
- const forceDownload = url.searchParams.get("download") === "1";
79
- const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);
80
85
  const headers = {
81
- "Cache-Control": checkAttachmentAccess(null, attachment, partition).ok ? "public, max-age=86400" : "private, max-age=60",
86
+ "Cache-Control": access.protected ? "private, no-store" : checkAttachmentAccess(null, attachment, partition).ok ? "public, max-age=86400" : "private, max-age=60",
82
87
  "Content-Security-Policy": "default-src 'none'; sandbox",
83
88
  "Content-Type": renderInline ? attachment.mimeType || "application/octet-stream" : "application/octet-stream",
84
89
  "Content-Disposition": buildAttachmentContentDisposition(
@@ -132,7 +137,8 @@ const openApi = {
132
137
  status: 500,
133
138
  description: "Partition misconfigured",
134
139
  schema: attachmentErrorSchema
135
- }
140
+ },
141
+ { status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
136
142
  ]
137
143
  }
138
144
  }
@@ -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 { 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 access = checkAttachmentAccess(scopedAuth, attachment, partition);\n if (!access.ok) {\n const message = access.status === 401 ? \"Unauthorized\" : \"Forbidden\";\n return NextResponse.json({ error: message }, { status: access.status });\n }\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 url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const headers: Record<string, string> = {\n \"Cache-Control\": 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 ],\n },\n },\n};\n"],
5
- "mappings": "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",
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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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,5 @@
1
+ import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../../lib/access-runner.js";
2
+ import { attachmentAccessErrorResponse } from "../../../../lib/access-errors.js";
1
3
  import { NextResponse } from "next/server";
2
4
  import sharp from "sharp";
3
5
  import { z } from "zod";
@@ -59,17 +61,20 @@ async function GET(req, context) {
59
61
  if (!attachment) {
60
62
  return NextResponse.json({ error: "Not found" }, { status: 404 });
61
63
  }
62
- if (!canRenderInlineAttachment(attachment.mimeType)) {
63
- return NextResponse.json({ error: "Unsupported media type" }, { status: 400 });
64
- }
65
64
  const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode });
66
65
  if (!partition) {
67
66
  return NextResponse.json({ error: "Partition misconfigured" }, { status: 500 });
68
67
  }
69
- const access = checkAttachmentAccess(scopedAuth, attachment, partition);
70
- if (!access.ok) {
71
- const message = access.status === 401 ? "Unauthorized" : "Forbidden";
72
- return NextResponse.json({ error: message }, { status: access.status });
68
+ const access = await evaluateAttachmentAccess({
69
+ auth: scopedAuth,
70
+ attachment,
71
+ partition,
72
+ action: "render",
73
+ context: createAttachmentAccessContext(container)
74
+ });
75
+ if (!access.ok) return attachmentAccessErrorResponse(access);
76
+ if (!canRenderInlineAttachment(attachment.mimeType)) {
77
+ return NextResponse.json({ error: "Unsupported media type" }, { status: 400 });
73
78
  }
74
79
  const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {
75
80
  tenantId: attachment.tenantId ?? "",
@@ -120,7 +125,7 @@ async function GET(req, context) {
120
125
  return new NextResponse(responseBody, {
121
126
  headers: {
122
127
  "Content-Type": attachment.mimeType || "image/jpeg",
123
- "Cache-Control": checkAttachmentAccess(null, attachment, partition).ok ? "public, max-age=3600" : "private, max-age=60",
128
+ "Cache-Control": access.protected ? "private, no-store" : checkAttachmentAccess(null, attachment, partition).ok ? "public, max-age=3600" : "private, max-age=60",
124
129
  "X-Content-Type-Options": "nosniff"
125
130
  }
126
131
  });
@@ -148,7 +153,8 @@ const openApi = {
148
153
  { status: 401, description: "Unauthorized - authentication required for private partitions", schema: attachmentErrorSchema },
149
154
  { status: 403, description: "Forbidden - insufficient permissions", schema: attachmentErrorSchema },
150
155
  { status: 404, description: "Image not found", schema: attachmentErrorSchema },
151
- { status: 500, description: "Partition misconfigured or image rendering failed", schema: attachmentErrorSchema }
156
+ { status: 500, description: "Partition misconfigured or image rendering failed", schema: attachmentErrorSchema },
157
+ { status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
152
158
  ]
153
159
  }
154
160
  }
@@ -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 { 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 if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\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 = checkAttachmentAccess(scopedAuth, attachment, partition)\n if (!access.ok) {\n const message = access.status === 401 ? 'Unauthorized' : 'Forbidden'\n return NextResponse.json({ error: message }, { status: access.status })\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': 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 ],\n },\n },\n}\n"],
5
- "mappings": "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",
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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",
6
6
  "names": []
7
7
  }