@open-mercato/core 0.10.1-develop.7841.1.af87ac614d → 0.10.1-develop.7857.1.259928eb85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_keys/api/keys/route.js +15 -1
  3. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  4. package/dist/modules/attachments/api/file/[id]/route.js +6 -1
  5. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  6. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +6 -1
  7. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  8. package/dist/modules/attachments/api/library/[id]/route.js +16 -1
  9. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  10. package/dist/modules/attachments/api/route.js +11 -3
  11. package/dist/modules/attachments/api/route.js.map +2 -2
  12. package/dist/modules/attachments/cli.js +2 -0
  13. package/dist/modules/attachments/cli.js.map +2 -2
  14. package/dist/modules/attachments/data/validators.js +11 -0
  15. package/dist/modules/attachments/data/validators.js.map +2 -2
  16. package/dist/modules/attachments/index.js +10 -0
  17. package/dist/modules/attachments/index.js.map +2 -2
  18. package/dist/modules/attachments/lib/access-query.js +5 -1
  19. package/dist/modules/attachments/lib/access-query.js.map +2 -2
  20. package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
  21. package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
  22. package/dist/modules/attachments/lib/drivers/driverFactory.js +54 -33
  23. package/dist/modules/attachments/lib/drivers/driverFactory.js.map +2 -2
  24. package/dist/modules/attachments/lib/drivers/index.js +11 -1
  25. package/dist/modules/attachments/lib/drivers/index.js.map +2 -2
  26. package/dist/modules/attachments/lib/drivers/storageValidation.js +85 -0
  27. package/dist/modules/attachments/lib/drivers/storageValidation.js.map +7 -0
  28. package/dist/modules/attachments/lib/scoped-upload-service.js +3 -1
  29. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  30. package/dist/modules/attachments/lib/storageErrors.js +22 -0
  31. package/dist/modules/attachments/lib/storageErrors.js.map +7 -0
  32. package/dist/modules/auth/api/users/route.js +16 -1
  33. package/dist/modules/auth/api/users/route.js.map +2 -2
  34. package/dist/modules/auth/backend/users/page.js +13 -2
  35. package/dist/modules/auth/backend/users/page.js.map +2 -2
  36. package/dist/modules/catalog/commands/products.js +4 -2
  37. package/dist/modules/catalog/commands/products.js.map +2 -2
  38. package/dist/modules/customers/api/companies/[id]/route.js +4 -9
  39. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  40. package/dist/modules/customers/api/people/[id]/route.js +4 -9
  41. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  42. package/dist/modules/customers/components/detail/TaskDialog.js +2 -0
  43. package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
  44. package/dist/modules/customers/lib/todoCompatibility.js +23 -0
  45. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  46. package/dist/modules/directory/api/organization-branding/route.js +6 -9
  47. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  48. package/dist/modules/directory/backend/directory/branding/page.js +15 -3
  49. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  50. package/dist/modules/wms/api/listEnrichers.js +3 -1
  51. package/dist/modules/wms/api/listEnrichers.js.map +2 -2
  52. package/dist/modules/wms/index.js +1 -1
  53. package/dist/modules/wms/index.js.map +2 -2
  54. package/jest.config.cjs +1 -1
  55. package/package.json +8 -8
  56. package/src/modules/api_keys/api/keys/route.ts +15 -1
  57. package/src/modules/api_keys/i18n/de.json +1 -0
  58. package/src/modules/api_keys/i18n/en.json +1 -0
  59. package/src/modules/api_keys/i18n/es.json +1 -0
  60. package/src/modules/api_keys/i18n/ko.json +1 -0
  61. package/src/modules/api_keys/i18n/pl.json +1 -0
  62. package/src/modules/attachments/api/file/[id]/route.ts +7 -1
  63. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -1
  64. package/src/modules/attachments/api/library/[id]/route.ts +14 -1
  65. package/src/modules/attachments/api/route.ts +11 -3
  66. package/src/modules/attachments/cli.ts +2 -0
  67. package/src/modules/attachments/data/validators.ts +11 -0
  68. package/src/modules/attachments/i18n/de.json +1 -0
  69. package/src/modules/attachments/i18n/en.json +1 -0
  70. package/src/modules/attachments/i18n/es.json +1 -0
  71. package/src/modules/attachments/i18n/ko.json +1 -0
  72. package/src/modules/attachments/i18n/pl.json +1 -0
  73. package/src/modules/attachments/index.ts +12 -0
  74. package/src/modules/attachments/lib/access-query.ts +5 -1
  75. package/src/modules/attachments/lib/createFromBuffer.ts +2 -0
  76. package/src/modules/attachments/lib/drivers/driverFactory.ts +79 -55
  77. package/src/modules/attachments/lib/drivers/index.ts +13 -0
  78. package/src/modules/attachments/lib/drivers/storageValidation.ts +114 -0
  79. package/src/modules/attachments/lib/scoped-upload-service.ts +3 -1
  80. package/src/modules/attachments/lib/storageErrors.ts +21 -0
  81. package/src/modules/auth/api/users/route.ts +18 -1
  82. package/src/modules/auth/backend/users/page.tsx +14 -2
  83. package/src/modules/auth/i18n/de.json +1 -0
  84. package/src/modules/auth/i18n/en.json +1 -0
  85. package/src/modules/auth/i18n/es.json +1 -0
  86. package/src/modules/auth/i18n/ko.json +1 -0
  87. package/src/modules/auth/i18n/pl.json +1 -0
  88. package/src/modules/catalog/commands/products.ts +4 -1
  89. package/src/modules/customer_accounts/AGENTS.md +1 -1
  90. package/src/modules/customers/api/companies/[id]/route.ts +6 -13
  91. package/src/modules/customers/api/people/[id]/route.ts +6 -13
  92. package/src/modules/customers/components/detail/TaskDialog.tsx +2 -0
  93. package/src/modules/customers/lib/todoCompatibility.ts +28 -0
  94. package/src/modules/directory/api/organization-branding/route.ts +5 -8
  95. package/src/modules/directory/backend/directory/branding/page.tsx +15 -3
  96. package/src/modules/sales/AGENTS.md +3 -3
  97. package/src/modules/wms/api/listEnrichers.ts +5 -2
  98. package/src/modules/wms/index.ts +1 -1
@@ -1,4 +1,4 @@
1
- [build:core] found 6242 entry points
1
+ [build:core] found 6248 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
- import { Role } from "@open-mercato/core/modules/auth/data/entities";
3
+ import { Role, User } from "@open-mercato/core/modules/auth/data/entities";
4
4
  import { Organization } from "@open-mercato/core/modules/directory/data/entities";
5
5
  import { ApiKey } from "../../data/entities.js";
6
6
  import { createApiKeySchema } from "../../data/validators.js";
@@ -9,6 +9,7 @@ import { enforceTenantSelection, resolveIsSuperAdmin } from "@open-mercato/core/
9
9
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
10
10
  import { assertActorCanGrantRoles } from "@open-mercato/core/modules/auth/lib/grantChecks";
11
11
  import { isOrganizationAccessAllowed } from "@open-mercato/shared/lib/auth/organizationAccess";
12
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
12
13
  const listQuerySchema = z.object({
13
14
  page: z.string().optional(),
14
15
  pageSize: z.string().optional(),
@@ -112,6 +113,19 @@ async function prepareCreateCommandInput(input, ctx) {
112
113
  })) {
113
114
  throw json({ error: translate("api_keys.errors.organizationOutOfScope", "Organization out of scope") }, { status: 403 });
114
115
  }
116
+ if (organizationId) {
117
+ const creator = await findOneWithDecryption(
118
+ em,
119
+ User,
120
+ { id: auth.sub, tenantId: targetTenantId, deletedAt: null },
121
+ { fields: ["organizationId"] },
122
+ { tenantId: targetTenantId, organizationId: null }
123
+ );
124
+ if (creator?.organizationId?.toLowerCase() !== organizationId.toLowerCase()) {
125
+ const message = translate("api_keys.errors.organizationMustMatchCreator");
126
+ throw json({ error: message, fieldErrors: { organizationId: message } }, { status: 400 });
127
+ }
128
+ }
115
129
  return {
116
130
  name: input.name,
117
131
  description: input.description ?? null,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/api_keys/api/keys/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { ApiKey } from '../../data/entities'\nimport { createApiKeySchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport type { CreateApiKeyCommandInput } from '../../commands/keys'\n\nconst listQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n})\n\nconst apiKeyRoleSchema = z.object({\n id: z.string().describe('Role identifier or alias assigned to the key'),\n name: z.string().nullable().describe('Display name of the mapped role, if available'),\n})\n\nconst apiKeyListItemSchema = z.object({\n id: z.string().describe('API key identifier'),\n name: z.string().describe('Friendly label used to identify the key'),\n description: z.string().nullable().describe('Optional free-form description'),\n keyPrefix: z.string().describe('Public prefix exposed to clients'),\n organizationId: z.string().uuid().nullable().describe('Organization scope of the key'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().describe('Creation timestamp (ISO 8601)'),\n lastUsedAt: z.string().nullable().describe('Last time the key was observed in use (ISO 8601)'),\n expiresAt: z.string().nullable().describe('When the key expires (ISO 8601)'),\n roles: z.array(apiKeyRoleSchema).describe('Effective roles applied when this key authenticates'),\n})\n\nconst apiKeyCollectionResponseSchema = z.object({\n items: z.array(apiKeyListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst apiKeyCreateResponseSchema = z.object({\n id: z.string().describe('Newly created API key identifier'),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value. Shown once for secure persistence.').optional(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(apiKeyRoleSchema),\n})\n\nconst deleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nasync function prepareCreateCommandInput(\n input: z.infer<typeof createApiKeySchema>,\n ctx: CrudCtx,\n): Promise<CreateApiKeyCommandInput> {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) {\n throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId')\n ? input.tenantId\n : auth.tenantId\n const targetTenantId = await enforceTenantSelection(ctx, requestedTenant)\n const em = ctx.container.resolve('em') as EntityManager\n const roleTokens = input.roles.filter((value) => value.trim().length > 0)\n const roleEntities: Role[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const effectiveTenantId = targetTenantId ?? auth.tenantId\n const normalizedEffectiveTenantId = effectiveTenantId.toLowerCase()\n let role: Role | null = null\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {\n role = await em.findOne(Role, { id: value, deletedAt: null })\n }\n if (!role) {\n const candidates = await em.find(Role, {\n name: value,\n deletedAt: null,\n $or: [{ tenantId: effectiveTenantId }, { tenantId: null }],\n } as FilterQuery<Role>, { limit: 5 })\n role = candidates.find((candidate) => (\n candidate.tenantId !== null\n && String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n )) ?? candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n throw json({ error: translate('api_keys.errors.roleNotFound', `Role ${value} not found`, { identifier: value }) }, { status: 400 })\n }\n const roleTenantId = role.tenantId ? String(role.tenantId).toLowerCase() : null\n if (roleTenantId && roleTenantId !== normalizedEffectiveTenantId) {\n throw json({ error: translate('api_keys.errors.roleWrongTenant', `Role ${role.name} belongs to another tenant`, { role: role.name ?? value }) }, { status: 400 })\n }\n roleEntities.push(role)\n }\n await assertActorCanGrantRoles({\n em,\n rbacService: ctx.container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n roles: roleEntities,\n })\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = Object.prototype.hasOwnProperty.call(input, 'organizationId')\n ? input.organizationId ?? null\n : ctx.selectedOrganizationId ?? auth.orgId ?? null\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n if (!isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: organizationId,\n })) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId: targetTenantId,\n organizationId,\n roleIds: roleEntities.map((role) => String(role.id)),\n expiresAt: input.expiresAt ?? null,\n }\n}\n\nconst crud = makeCrudRoute<\n z.infer<typeof createApiKeySchema>,\n never,\n z.infer<typeof listQuerySchema>\n>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['api_keys.view'] },\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n DELETE: { requireAuth: true, requireFeatures: ['api_keys.delete'] },\n },\n orm: { entity: ApiKey, orgField: null },\n list: { schema: listQuerySchema },\n del: { idFrom: 'query' },\n actions: {\n create: {\n commandId: 'api_keys.keys.create',\n schema: createApiKeySchema,\n mapInput: ({ parsed, ctx }) => prepareCreateCommandInput(parsed, ctx),\n response: ({ result }) => result,\n },\n delete: {\n commandId: 'api_keys.keys.delete',\n mapInput: ({ raw }) => ({ id: String(raw.query?.id ?? '') }),\n response: () => ({ success: true }),\n },\n },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 200)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const qb = em.createQueryBuilder(ApiKey, 'k')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n const organizationConstraint = organizationIds?.length\n ? { organizationId: { $in: organizationIds } }\n : auth.orgId ? { organizationId: auth.orgId } : null\n if (organizationConstraint) {\n const tenantWideAccess = isOrganizationAccessAllowed({\n targetOrganizationId: null,\n allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],\n isSuperAdmin: await resolveIsSuperAdmin(ctx),\n })\n qb.andWhere(tenantWideAccess\n ? { $or: [{ organizationId: null }, organizationConstraint] }\n : organizationConstraint)\n }\n if (search) {\n const pattern = `%${escapeLikePattern(search)}%`\n qb.andWhere({\n $or: [\n { name: { $ilike: pattern } },\n { keyPrefix: { $ilike: pattern } },\n ],\n })\n }\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset((page - 1) * pageSize)\n const [items, total] = await qb.getResultAndCount()\n\n if (!items.length) {\n throw json({ items: [], total, page, pageSize, totalPages: Math.ceil(total / pageSize) })\n }\n\n const roleIdSet = new Set<string>()\n const orgIdSet = new Set<string>()\n for (const item of items) {\n if (Array.isArray(item.rolesJson)) {\n for (const roleId of item.rolesJson) roleIdSet.add(String(roleId))\n }\n if (item.organizationId) orgIdSet.add(String(item.organizationId))\n }\n\n const roleIdArray = Array.from(roleIdSet)\n const organizationIdArray = Array.from(orgIdSet)\n const roleFilter: FilterQuery<Role> = { id: { $in: roleIdArray } }\n const organizationFilter: FilterQuery<Organization> = { id: { $in: organizationIdArray } }\n const [roles, organizations] = await Promise.all([\n roleIdArray.length ? em.find(Role, roleFilter) : [],\n organizationIdArray.length ? em.find(Organization, organizationFilter) : [],\n ])\n const roleMap = new Map(roles.map((role) => [String(role.id), role.name ?? null]))\n const orgMap = new Map(organizations.map((org) => [String(org.id), org.name ?? null]))\n\n const payload = {\n items: items.map((item) => ({\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n keyPrefix: item.keyPrefix,\n organizationId: item.organizationId ?? null,\n organizationName: item.organizationId ? orgMap.get(String(item.organizationId)) ?? null : null,\n createdAt: item.createdAt,\n lastUsedAt: item.lastUsedAt ?? null,\n expiresAt: item.expiresAt ?? null,\n roles: Array.isArray(item.rolesJson)\n ? item.rolesJson.map((id) => ({ id, name: roleMap.get(String(id)) ?? null }))\n : [],\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n }\n\n throw json(payload)\n },\n },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage API keys',\n description:\n 'Provides list, creation, and deletion capabilities for API keys scoped to the authenticated tenant and organization.',\n methods: {\n GET: {\n summary: 'List API keys',\n description:\n 'Returns paginated API keys visible to the current user, including per-key role assignments and organization context.',\n query: listQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Collection of API keys',\n schema: apiKeyCollectionResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Tenant context missing', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden by organization scope', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Create API key',\n description:\n 'Creates a new API key, returning the one-time secret value together with the generated key prefix and scope details.',\n requestBody: {\n contentType: 'application/json',\n schema: createApiKeySchema,\n description: 'API key definition including optional scope and role assignments.',\n },\n responses: [\n {\n status: 201,\n description: 'API key created successfully',\n schema: apiKeyCreateResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing tenant context', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete API key',\n description:\n 'Removes an API key by identifier. The key must belong to the current tenant and fall within the requester organization scope.',\n query: z.object({\n id: z.string().uuid().describe('API key identifier to delete'),\n }),\n responses: [\n { status: 200, description: 'Key deleted successfully', schema: deleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing or invalid identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n { status: 404, description: 'Key not found within scope', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Role, User } from '@open-mercato/core/modules/auth/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { ApiKey } from '../../data/entities'\nimport { createApiKeySchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { CreateApiKeyCommandInput } from '../../commands/keys'\n\nconst listQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n})\n\nconst apiKeyRoleSchema = z.object({\n id: z.string().describe('Role identifier or alias assigned to the key'),\n name: z.string().nullable().describe('Display name of the mapped role, if available'),\n})\n\nconst apiKeyListItemSchema = z.object({\n id: z.string().describe('API key identifier'),\n name: z.string().describe('Friendly label used to identify the key'),\n description: z.string().nullable().describe('Optional free-form description'),\n keyPrefix: z.string().describe('Public prefix exposed to clients'),\n organizationId: z.string().uuid().nullable().describe('Organization scope of the key'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().describe('Creation timestamp (ISO 8601)'),\n lastUsedAt: z.string().nullable().describe('Last time the key was observed in use (ISO 8601)'),\n expiresAt: z.string().nullable().describe('When the key expires (ISO 8601)'),\n roles: z.array(apiKeyRoleSchema).describe('Effective roles applied when this key authenticates'),\n})\n\nconst apiKeyCollectionResponseSchema = z.object({\n items: z.array(apiKeyListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst apiKeyCreateResponseSchema = z.object({\n id: z.string().describe('Newly created API key identifier'),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value. Shown once for secure persistence.').optional(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(apiKeyRoleSchema),\n})\n\nconst deleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nasync function prepareCreateCommandInput(\n input: z.infer<typeof createApiKeySchema>,\n ctx: CrudCtx,\n): Promise<CreateApiKeyCommandInput> {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) {\n throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId')\n ? input.tenantId\n : auth.tenantId\n const targetTenantId = await enforceTenantSelection(ctx, requestedTenant)\n const em = ctx.container.resolve('em') as EntityManager\n const roleTokens = input.roles.filter((value) => value.trim().length > 0)\n const roleEntities: Role[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const effectiveTenantId = targetTenantId ?? auth.tenantId\n const normalizedEffectiveTenantId = effectiveTenantId.toLowerCase()\n let role: Role | null = null\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {\n role = await em.findOne(Role, { id: value, deletedAt: null })\n }\n if (!role) {\n const candidates = await em.find(Role, {\n name: value,\n deletedAt: null,\n $or: [{ tenantId: effectiveTenantId }, { tenantId: null }],\n } as FilterQuery<Role>, { limit: 5 })\n role = candidates.find((candidate) => (\n candidate.tenantId !== null\n && String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n )) ?? candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n throw json({ error: translate('api_keys.errors.roleNotFound', `Role ${value} not found`, { identifier: value }) }, { status: 400 })\n }\n const roleTenantId = role.tenantId ? String(role.tenantId).toLowerCase() : null\n if (roleTenantId && roleTenantId !== normalizedEffectiveTenantId) {\n throw json({ error: translate('api_keys.errors.roleWrongTenant', `Role ${role.name} belongs to another tenant`, { role: role.name ?? value }) }, { status: 400 })\n }\n roleEntities.push(role)\n }\n await assertActorCanGrantRoles({\n em,\n rbacService: ctx.container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n roles: roleEntities,\n })\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = Object.prototype.hasOwnProperty.call(input, 'organizationId')\n ? input.organizationId ?? null\n : ctx.selectedOrganizationId ?? auth.orgId ?? null\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n if (!isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: organizationId,\n })) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n if (organizationId) {\n const creator = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, tenantId: targetTenantId, deletedAt: null },\n { fields: ['organizationId'] },\n { tenantId: targetTenantId, organizationId: null },\n )\n if (creator?.organizationId?.toLowerCase() !== organizationId.toLowerCase()) {\n const message = translate('api_keys.errors.organizationMustMatchCreator')\n throw json({ error: message, fieldErrors: { organizationId: message } }, { status: 400 })\n }\n }\n\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId: targetTenantId,\n organizationId,\n roleIds: roleEntities.map((role) => String(role.id)),\n expiresAt: input.expiresAt ?? null,\n }\n}\n\nconst crud = makeCrudRoute<\n z.infer<typeof createApiKeySchema>,\n never,\n z.infer<typeof listQuerySchema>\n>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['api_keys.view'] },\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n DELETE: { requireAuth: true, requireFeatures: ['api_keys.delete'] },\n },\n orm: { entity: ApiKey, orgField: null },\n list: { schema: listQuerySchema },\n del: { idFrom: 'query' },\n actions: {\n create: {\n commandId: 'api_keys.keys.create',\n schema: createApiKeySchema,\n mapInput: ({ parsed, ctx }) => prepareCreateCommandInput(parsed, ctx),\n response: ({ result }) => result,\n },\n delete: {\n commandId: 'api_keys.keys.delete',\n mapInput: ({ raw }) => ({ id: String(raw.query?.id ?? '') }),\n response: () => ({ success: true }),\n },\n },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 200)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const qb = em.createQueryBuilder(ApiKey, 'k')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n const organizationConstraint = organizationIds?.length\n ? { organizationId: { $in: organizationIds } }\n : auth.orgId ? { organizationId: auth.orgId } : null\n if (organizationConstraint) {\n const tenantWideAccess = isOrganizationAccessAllowed({\n targetOrganizationId: null,\n allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],\n isSuperAdmin: await resolveIsSuperAdmin(ctx),\n })\n qb.andWhere(tenantWideAccess\n ? { $or: [{ organizationId: null }, organizationConstraint] }\n : organizationConstraint)\n }\n if (search) {\n const pattern = `%${escapeLikePattern(search)}%`\n qb.andWhere({\n $or: [\n { name: { $ilike: pattern } },\n { keyPrefix: { $ilike: pattern } },\n ],\n })\n }\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset((page - 1) * pageSize)\n const [items, total] = await qb.getResultAndCount()\n\n if (!items.length) {\n throw json({ items: [], total, page, pageSize, totalPages: Math.ceil(total / pageSize) })\n }\n\n const roleIdSet = new Set<string>()\n const orgIdSet = new Set<string>()\n for (const item of items) {\n if (Array.isArray(item.rolesJson)) {\n for (const roleId of item.rolesJson) roleIdSet.add(String(roleId))\n }\n if (item.organizationId) orgIdSet.add(String(item.organizationId))\n }\n\n const roleIdArray = Array.from(roleIdSet)\n const organizationIdArray = Array.from(orgIdSet)\n const roleFilter: FilterQuery<Role> = { id: { $in: roleIdArray } }\n const organizationFilter: FilterQuery<Organization> = { id: { $in: organizationIdArray } }\n const [roles, organizations] = await Promise.all([\n roleIdArray.length ? em.find(Role, roleFilter) : [],\n organizationIdArray.length ? em.find(Organization, organizationFilter) : [],\n ])\n const roleMap = new Map(roles.map((role) => [String(role.id), role.name ?? null]))\n const orgMap = new Map(organizations.map((org) => [String(org.id), org.name ?? null]))\n\n const payload = {\n items: items.map((item) => ({\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n keyPrefix: item.keyPrefix,\n organizationId: item.organizationId ?? null,\n organizationName: item.organizationId ? orgMap.get(String(item.organizationId)) ?? null : null,\n createdAt: item.createdAt,\n lastUsedAt: item.lastUsedAt ?? null,\n expiresAt: item.expiresAt ?? null,\n roles: Array.isArray(item.rolesJson)\n ? item.rolesJson.map((id) => ({ id, name: roleMap.get(String(id)) ?? null }))\n : [],\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n }\n\n throw json(payload)\n },\n },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage API keys',\n description:\n 'Provides list, creation, and deletion capabilities for API keys scoped to the authenticated tenant and organization.',\n methods: {\n GET: {\n summary: 'List API keys',\n description:\n 'Returns paginated API keys visible to the current user, including per-key role assignments and organization context.',\n query: listQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Collection of API keys',\n schema: apiKeyCollectionResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Tenant context missing', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden by organization scope', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Create API key',\n description:\n 'Creates a new API key, returning the one-time secret value together with the generated key prefix and scope details.',\n requestBody: {\n contentType: 'application/json',\n schema: createApiKeySchema,\n description: 'API key definition including optional scope and role assignments.',\n },\n responses: [\n {\n status: 201,\n description: 'API key created successfully',\n schema: apiKeyCreateResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing tenant context', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete API key',\n description:\n 'Removes an API key by identifier. The key must belong to the current tenant and fall within the requester organization scope.',\n query: z.object({\n id: z.string().uuid().describe('API key identifier to delete'),\n }),\n responses: [\n { status: 200, description: 'Key deleted successfully', schema: deleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing or invalid identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n { status: 404, description: 'Key not found within scope', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,5 @@
1
+ import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
2
+ import { assertAttachmentStoragePartitionExists } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
1
3
  import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../lib/access-runner.js";
2
4
  import { attachmentAccessErrorResponse } from "../../../lib/access-errors.js";
3
5
  import { NextResponse } from "next/server";
@@ -19,7 +21,7 @@ import { resolveAttachmentRequestScope } from "@open-mercato/core/modules/attach
19
21
  const metadata = {
20
22
  GET: { requireAuth: false }
21
23
  };
22
- async function GET(req, context) {
24
+ async function readAttachmentFile(req, context) {
23
25
  const { id } = await context.params;
24
26
  if (!id) {
25
27
  return NextResponse.json(
@@ -54,6 +56,7 @@ async function GET(req, context) {
54
56
  const partition = await em.findOne(AttachmentPartition, {
55
57
  code: attachment.partitionCode
56
58
  });
59
+ assertAttachmentStoragePartitionExists(Boolean(partition));
57
60
  if (!partition) {
58
61
  return NextResponse.json(
59
62
  { error: "Partition misconfigured" },
@@ -98,6 +101,7 @@ async function GET(req, context) {
98
101
  const responseBody = new Uint8Array(buffer);
99
102
  return new NextResponse(responseBody, { status: 200, headers });
100
103
  }
104
+ const GET = withAttachmentStorageErrors(readAttachmentFile);
101
105
  const openApi = {
102
106
  tag: attachmentsTag,
103
107
  summary: "Download attachment file",
@@ -113,6 +117,7 @@ const openApi = {
113
117
  }
114
118
  ],
115
119
  errors: [
120
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
116
121
  {
117
122
  status: 400,
118
123
  description: "Missing attachment ID",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/attachments/api/file/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from \"next/server\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope';\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string }> },\n) {\n const { id } = await context.params;\n if (!id) {\n return NextResponse.json(\n { error: \"Attachment id is required\" },\n { status: 400 },\n );\n }\n const auth = await getAuthFromRequest(req);\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const storageDriverFactory =\n (container.resolve(\"storageDriverFactory\") as StorageDriverFactory | null) ??\n new StorageDriverFactory(em);\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req);\n if (requestScope.denied) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth;\n const findFilter: Record<string, unknown> = { id };\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: renderInline ? 'render' : 'read',\n context: createAttachmentAccessContext(container),\n });\n if (!access.ok) return attachmentAccessErrorResponse(access);\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n });\n let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const headers: Record<string, string> = {\n \"Cache-Control\": access.protected ? \"private, no-store\" : checkAttachmentAccess(null, attachment, partition).ok\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'\nimport { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from \"next/server\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope';\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nasync function readAttachmentFile(\n req: NextRequest,\n context: { params: Promise<{ id: string }> },\n) {\n const { id } = await context.params;\n if (!id) {\n return NextResponse.json(\n { error: \"Attachment id is required\" },\n { status: 400 },\n );\n }\n const auth = await getAuthFromRequest(req);\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const storageDriverFactory =\n (container.resolve(\"storageDriverFactory\") as StorageDriverFactory | null) ??\n new StorageDriverFactory(em);\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req);\n if (requestScope.denied) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth;\n const findFilter: Record<string, unknown> = { id };\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: renderInline ? 'render' : 'read',\n context: createAttachmentAccessContext(container),\n });\n if (!access.ok) return attachmentAccessErrorResponse(access);\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n });\n let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const headers: Record<string, string> = {\n \"Cache-Control\": access.protected ? \"private, no-store\" : checkAttachmentAccess(null, attachment, partition).ok\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const GET = withAttachmentStorageErrors(readAttachmentFile)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,5 @@
1
+ import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
2
+ import { assertAttachmentStoragePartitionExists } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
1
3
  import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../../lib/access-runner.js";
2
4
  import { attachmentAccessErrorResponse } from "../../../../lib/access-errors.js";
3
5
  import { NextResponse } from "next/server";
@@ -31,7 +33,7 @@ const querySchema = z.object({
31
33
  const metadata = {
32
34
  GET: { requireAuth: false }
33
35
  };
34
- async function GET(req, context) {
36
+ async function readAttachmentImage(req, context) {
35
37
  const auth = await getAuthFromRequest(req);
36
38
  const { id } = await context.params;
37
39
  if (!id) {
@@ -62,6 +64,7 @@ async function GET(req, context) {
62
64
  return NextResponse.json({ error: "Not found" }, { status: 404 });
63
65
  }
64
66
  const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode });
67
+ assertAttachmentStoragePartitionExists(Boolean(partition));
65
68
  if (!partition) {
66
69
  return NextResponse.json({ error: "Partition misconfigured" }, { status: 500 });
67
70
  }
@@ -134,6 +137,7 @@ async function GET(req, context) {
134
137
  return NextResponse.json({ error: "Failed to render image" }, { status: 500 });
135
138
  }
136
139
  }
140
+ const GET = withAttachmentStorageErrors(readAttachmentImage);
137
141
  const openApi = {
138
142
  tag: attachmentsTag,
139
143
  summary: "Serve resized images",
@@ -149,6 +153,7 @@ const openApi = {
149
153
  }
150
154
  ],
151
155
  errors: [
156
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
152
157
  { status: 400, description: "Invalid parameters, missing ID, or non-image attachment", schema: attachmentErrorSchema },
153
158
  { status: 401, description: "Unauthorized - authentication required for private partitions", schema: attachmentErrorSchema },
154
159
  { status: 403, description: "Forbidden - insufficient permissions", schema: attachmentErrorSchema },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/attachments/api/image/%5Bid%5D/%5B%5B...slug%5D%5D/route.ts"],
4
- "sourcesContent": ["import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req)\n if (requestScope.denied) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: 'render', context: createAttachmentAccessContext(container),\n })\n if (!access.ok) return attachmentAccessErrorResponse(access)\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: ResizeOptions = {\n width: width || undefined,\n height: height || undefined,\n fit: cropType === 'contain' ? 'contain' : 'cover',\n }\n if (cropType === 'contain') {\n resizeOptions.background = { r: 0, g: 0, b: 0, alpha: 0 }\n }\n transformer = transformer.resize(resizeOptions)\n }\n buffer = await transformer.toBuffer()\n if (cacheKey) {\n void writeThumbnailCache(attachment.partitionCode, attachment.id, cacheKey, buffer).catch((cacheError) => {\n logger.error('Thumbnail cache write failed', { err: cacheError })\n })\n }\n }\n if (!buffer) {\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n const responseBody = new Uint8Array(buffer)\n\n return new NextResponse(responseBody, {\n headers: {\n 'Content-Type': attachment.mimeType || 'image/jpeg',\n 'Cache-Control': access.protected ? 'private, no-store' : checkAttachmentAccess(null, attachment, partition).ok ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'\nimport { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nasync function readAttachmentImage(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req)\n if (requestScope.denied) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: 'render', context: createAttachmentAccessContext(container),\n })\n if (!access.ok) return attachmentAccessErrorResponse(access)\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: ResizeOptions = {\n width: width || undefined,\n height: height || undefined,\n fit: cropType === 'contain' ? 'contain' : 'cover',\n }\n if (cropType === 'contain') {\n resizeOptions.background = { r: 0, g: 0, b: 0, alpha: 0 }\n }\n transformer = transformer.resize(resizeOptions)\n }\n buffer = await transformer.toBuffer()\n if (cacheKey) {\n void writeThumbnailCache(attachment.partitionCode, attachment.id, cacheKey, buffer).catch((cacheError) => {\n logger.error('Thumbnail cache write failed', { err: cacheError })\n })\n }\n }\n if (!buffer) {\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n const responseBody = new Uint8Array(buffer)\n\n return new NextResponse(responseBody, {\n headers: {\n 'Content-Type': attachment.mimeType || 'image/jpeg',\n 'Cache-Control': access.protected ? 'private, no-store' : checkAttachmentAccess(null, attachment, partition).ok ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const GET = withAttachmentStorageErrors(readAttachmentImage)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,5 @@
1
+ import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
2
+ import { getAttachmentStoragePolicy } from "../../../lib/drivers/storageValidation.js";
1
3
  import { LockMode } from "@mikro-orm/core";
2
4
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
5
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
@@ -244,6 +246,18 @@ async function deleteAttachment(req, ctx) {
244
246
  tenantId: auth.tenantId,
245
247
  organizationId: auth.orgId
246
248
  };
249
+ if (getAttachmentStoragePolicy() === "strict") {
250
+ const preflightRecord = await findOneWithDecryption(em, Attachment, deleteFilter, void 0, {
251
+ tenantId: auth.tenantId,
252
+ organizationId: auth.orgId
253
+ });
254
+ if (!preflightRecord) return throwAttachmentAccessError(404);
255
+ await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: preflightRecord, action: "delete" });
256
+ await storageDriverFactory.resolveForPartition(preflightRecord.partitionCode, {
257
+ tenantId: preflightRecord.tenantId ?? auth.tenantId,
258
+ organizationId: preflightRecord.organizationId ?? auth.orgId
259
+ });
260
+ }
247
261
  const outcome = await em.transactional(async (tx) => {
248
262
  const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, void 0, {
249
263
  tenantId: auth.tenantId,
@@ -289,7 +303,7 @@ async function deleteAttachment(req, ctx) {
289
303
  return NextResponse.json({ ok: true });
290
304
  }
291
305
  const PATCH = withAttachmentAccessErrors(updateAttachment);
292
- const DELETE = withAttachmentAccessErrors(deleteAttachment);
306
+ const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment));
293
307
  const openApi = {
294
308
  tag: attachmentsTag,
295
309
  summary: "Attachment detail management",
@@ -334,6 +348,7 @@ const openApi = {
334
348
  { status: 200, description: "Attachment deleted successfully", schema: z.object({ ok: z.literal(true) }) }
335
349
  ],
336
350
  errors: [
351
+ { status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
337
352
  { status: 400, description: "Invalid attachment ID", schema: attachmentErrorSchema },
338
353
  { status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
339
354
  { status: 404, description: "Attachment not found", schema: attachmentErrorSchema },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/attachments/api/library/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { prepareAttachmentMutation } from '../../../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../../../lib/access-errors'\nimport { createAttachmentAccessContext } from '../../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'metadata' })\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n }, { headers: { 'Cache-Control': 'private, no-store' } })\n}\n\nexport const GET = withAttachmentAccessErrors(getAttachment)\n\nasync function updateAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await readJsonSafe(req, null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const record = await findOneWithDecryption(tx, Attachment, patchFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update',\n payload: { ...parsed.data, customFields: custom },\n })\n const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom })\n const effectiveParsed = updateSchema.safeParse(effective.base)\n if (!effectiveParsed.success) return throwAttachmentAccessError(403)\n const patch: Record<string, unknown> = {}\n if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags)\n if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments)\n const nextMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'reassign' })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'metadata' })\n record.storageMetadata = nextMetadata\n await tx.flush()\n if (dataEngine && Object.keys(effective.custom).length) {\n await setCustomFieldsIfAny({ dataEngine, entityId: E.attachments.attachment, recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId, organizationId: record.organizationId ?? auth.orgId,\n values: effective.custom,\n })\n }\n return { record, custom: effective.custom, guard }\n })\n const { record } = outcome\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(outcome.custom ?? null),\n },\n })\n}\n\nasync function deleteAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!candidate) return throwAttachmentAccessError(404)\n const referenceIds = await lockAttachmentStorageReferences(tx, candidate)\n const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)\n if (!removal.removed) return throwAttachmentAccessError(404)\n const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'delete' })\n const driver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? auth.orgId!,\n })\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const PATCH = withAttachmentAccessErrors(updateAttachment)\nexport const DELETE = withAttachmentAccessErrors(deleteAttachment)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { getAttachmentStoragePolicy } from '../../../lib/drivers/storageValidation'\nimport { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { prepareAttachmentMutation } from '../../../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../../../lib/access-errors'\nimport { createAttachmentAccessContext } from '../../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'metadata' })\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n }, { headers: { 'Cache-Control': 'private, no-store' } })\n}\n\nexport const GET = withAttachmentAccessErrors(getAttachment)\n\nasync function updateAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await readJsonSafe(req, null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const record = await findOneWithDecryption(tx, Attachment, patchFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update',\n payload: { ...parsed.data, customFields: custom },\n })\n const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom })\n const effectiveParsed = updateSchema.safeParse(effective.base)\n if (!effectiveParsed.success) return throwAttachmentAccessError(403)\n const patch: Record<string, unknown> = {}\n if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags)\n if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments)\n const nextMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'reassign' })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'metadata' })\n record.storageMetadata = nextMetadata\n await tx.flush()\n if (dataEngine && Object.keys(effective.custom).length) {\n await setCustomFieldsIfAny({ dataEngine, entityId: E.attachments.attachment, recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId, organizationId: record.organizationId ?? auth.orgId,\n values: effective.custom,\n })\n }\n return { record, custom: effective.custom, guard }\n })\n const { record } = outcome\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(outcome.custom ?? null),\n },\n })\n}\n\nasync function deleteAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n if (getAttachmentStoragePolicy() === 'strict') {\n const preflightRecord = await findOneWithDecryption(em, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!preflightRecord) return throwAttachmentAccessError(404)\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: preflightRecord, action: 'delete' })\n await storageDriverFactory.resolveForPartition(preflightRecord.partitionCode, {\n tenantId: preflightRecord.tenantId ?? auth.tenantId, organizationId: preflightRecord.organizationId ?? auth.orgId,\n })\n }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!candidate) return throwAttachmentAccessError(404)\n const referenceIds = await lockAttachmentStorageReferences(tx, candidate)\n const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)\n if (!removal.removed) return throwAttachmentAccessError(404)\n const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'delete' })\n const driver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? auth.orgId!,\n })\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const PATCH = withAttachmentAccessErrors(updateAttachment)\nexport const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment))\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["metadata", "record"]
7
7
  }