@open-mercato/core 0.6.6-develop.6543.1.49fadb4c31 → 0.6.6

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 (166) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_keys/api/keys/route.js +9 -23
  3. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  4. package/dist/modules/api_keys/cli.js +1 -2
  5. package/dist/modules/api_keys/cli.js.map +2 -2
  6. package/dist/modules/attachments/api/file/[id]/route.js +7 -9
  7. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  8. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -9
  9. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  10. package/dist/modules/attachments/api/route.js +17 -21
  11. package/dist/modules/attachments/api/route.js.map +2 -2
  12. package/dist/modules/auth/api/users/route.js +2 -6
  13. package/dist/modules/auth/api/users/route.js.map +2 -2
  14. package/dist/modules/auth/cli.js +2 -3
  15. package/dist/modules/auth/cli.js.map +2 -2
  16. package/dist/modules/configs/lib/upgrade-actions.js +0 -22
  17. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  18. package/dist/modules/customer_accounts/widgets/injection-table.js +2 -0
  19. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -7
  21. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  22. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +7 -11
  23. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  24. package/dist/modules/dashboards/api/roles/widgets/route.js +3 -22
  25. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  26. package/dist/modules/dashboards/api/users/widgets/route.js +3 -22
  27. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  28. package/dist/modules/dashboards/cli.js +3 -4
  29. package/dist/modules/dashboards/cli.js.map +2 -2
  30. package/dist/modules/dashboards/lib/widgetAssignmentScope.js +1 -9
  31. package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
  32. package/dist/modules/dictionaries/commands/entries.js +14 -7
  33. package/dist/modules/dictionaries/commands/entries.js.map +2 -2
  34. package/dist/modules/dictionaries/commands/entry-operations.js +16 -7
  35. package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
  36. package/dist/modules/dictionaries/commands/factory.js +3 -9
  37. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  38. package/dist/modules/dictionaries/fields/dictionary.js +41 -56
  39. package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
  40. package/dist/modules/directory/utils/organizationScope.js +3 -10
  41. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  42. package/dist/modules/entities/api/records.js +4 -5
  43. package/dist/modules/entities/api/records.js.map +2 -2
  44. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +0 -1
  45. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
  46. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
  47. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  48. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +0 -1
  49. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
  50. package/dist/modules/entities/cli.js +1 -2
  51. package/dist/modules/entities/cli.js.map +2 -2
  52. package/dist/modules/query_index/api/status.js +8 -2
  53. package/dist/modules/query_index/api/status.js.map +2 -2
  54. package/dist/modules/sales/commands/documents.js +0 -44
  55. package/dist/modules/sales/commands/documents.js.map +2 -2
  56. package/dist/modules/sales/commands/returns.js +27 -2
  57. package/dist/modules/sales/commands/returns.js.map +2 -2
  58. package/dist/modules/sales/components/documents/AddressesSection.js +2 -6
  59. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  60. package/dist/modules/sales/components/documents/ItemsSection.js +0 -1
  61. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  62. package/dist/modules/sales/components/documents/LineItemDialog.js +0 -9
  63. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  64. package/dist/modules/sales/components/documents/SalesDocumentForm.js +0 -44
  65. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  66. package/dist/modules/sales/components/documents/ShipmentDialog.js +38 -59
  67. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  68. package/dist/modules/sales/lib/shipments/snapshots.js +1 -27
  69. package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
  70. package/dist/modules/workflows/frontend/checkout-demo/page.js +8 -1
  71. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  72. package/dist/modules/workflows/lib/activity-executor.js +1 -1
  73. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  74. package/dist/modules/workflows/lib/find-definition.js +18 -2
  75. package/dist/modules/workflows/lib/find-definition.js.map +2 -2
  76. package/dist/modules/workflows/lib/signal-handler.js +3 -2
  77. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  78. package/dist/modules/workflows/lib/step-handler.js +4 -8
  79. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  80. package/dist/modules/workflows/lib/task-handler.js +3 -5
  81. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  82. package/dist/modules/workflows/lib/timer-handler.js +2 -1
  83. package/dist/modules/workflows/lib/timer-handler.js.map +2 -2
  84. package/dist/modules/workflows/lib/transition-handler.js +5 -13
  85. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  86. package/dist/modules/workflows/lib/workflow-executor.js +11 -9
  87. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  88. package/dist/modules/workflows/migrations/Migration20260715120000.js +68 -0
  89. package/dist/modules/workflows/migrations/Migration20260715120000.js.map +7 -0
  90. package/dist/modules/workflows/migrations/Migration20260716120000.js +24 -0
  91. package/dist/modules/workflows/migrations/Migration20260716120000.js.map +7 -0
  92. package/dist/modules/workflows/workflows.js +2 -0
  93. package/dist/modules/workflows/workflows.js.map +2 -2
  94. package/package.json +8 -9
  95. package/src/modules/api_keys/api/keys/route.ts +9 -27
  96. package/src/modules/api_keys/cli.ts +3 -2
  97. package/src/modules/attachments/api/file/[id]/route.ts +7 -11
  98. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -11
  99. package/src/modules/attachments/api/route.ts +21 -26
  100. package/src/modules/auth/api/users/route.ts +2 -11
  101. package/src/modules/auth/cli.ts +2 -3
  102. package/src/modules/configs/i18n/de.json +1 -5
  103. package/src/modules/configs/i18n/en.json +1 -5
  104. package/src/modules/configs/i18n/es.json +1 -5
  105. package/src/modules/configs/i18n/pl.json +1 -5
  106. package/src/modules/configs/lib/upgrade-actions.ts +0 -24
  107. package/src/modules/customer_accounts/AGENTS.md +6 -8
  108. package/src/modules/customer_accounts/widgets/injection-table.ts +2 -0
  109. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -11
  110. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +7 -13
  111. package/src/modules/dashboards/api/roles/widgets/route.ts +3 -22
  112. package/src/modules/dashboards/api/users/widgets/route.ts +3 -22
  113. package/src/modules/dashboards/cli.ts +9 -4
  114. package/src/modules/dashboards/lib/widgetAssignmentScope.ts +0 -18
  115. package/src/modules/dictionaries/commands/entries.ts +16 -7
  116. package/src/modules/dictionaries/commands/entry-operations.ts +18 -8
  117. package/src/modules/dictionaries/commands/factory.ts +2 -8
  118. package/src/modules/dictionaries/fields/dictionary.tsx +32 -55
  119. package/src/modules/dictionaries/i18n/de.json +0 -2
  120. package/src/modules/dictionaries/i18n/en.json +0 -2
  121. package/src/modules/dictionaries/i18n/es.json +0 -2
  122. package/src/modules/dictionaries/i18n/pl.json +0 -2
  123. package/src/modules/directory/utils/organizationScope.ts +2 -36
  124. package/src/modules/entities/api/records.ts +7 -5
  125. package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +0 -1
  126. package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +0 -1
  127. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +49 -51
  128. package/src/modules/entities/cli.ts +1 -2
  129. package/src/modules/entities/i18n/de.json +0 -34
  130. package/src/modules/entities/i18n/en.json +0 -34
  131. package/src/modules/entities/i18n/es.json +0 -34
  132. package/src/modules/entities/i18n/pl.json +0 -34
  133. package/src/modules/query_index/api/status.ts +8 -2
  134. package/src/modules/sales/commands/documents.ts +0 -65
  135. package/src/modules/sales/commands/returns.ts +32 -2
  136. package/src/modules/sales/components/documents/AddressesSection.tsx +2 -6
  137. package/src/modules/sales/components/documents/ItemsSection.tsx +0 -5
  138. package/src/modules/sales/components/documents/LineItemDialog.tsx +0 -10
  139. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +0 -46
  140. package/src/modules/sales/components/documents/ShipmentDialog.tsx +40 -65
  141. package/src/modules/sales/i18n/de.json +0 -4
  142. package/src/modules/sales/i18n/en.json +0 -4
  143. package/src/modules/sales/i18n/es.json +0 -4
  144. package/src/modules/sales/i18n/pl.json +0 -4
  145. package/src/modules/sales/lib/shipments/snapshots.ts +0 -30
  146. package/src/modules/workflows/frontend/checkout-demo/page.tsx +12 -1
  147. package/src/modules/workflows/lib/activity-executor.ts +22 -2
  148. package/src/modules/workflows/lib/find-definition.ts +53 -1
  149. package/src/modules/workflows/lib/signal-handler.ts +5 -4
  150. package/src/modules/workflows/lib/step-handler.ts +4 -8
  151. package/src/modules/workflows/lib/task-handler.ts +2 -4
  152. package/src/modules/workflows/lib/timer-handler.ts +3 -2
  153. package/src/modules/workflows/lib/transition-handler.ts +5 -13
  154. package/src/modules/workflows/lib/workflow-executor.ts +11 -8
  155. package/src/modules/workflows/migrations/Migration20260715120000.ts +114 -0
  156. package/src/modules/workflows/migrations/Migration20260716120000.ts +57 -0
  157. package/src/modules/workflows/workflows.ts +2 -0
  158. package/dist/modules/attachments/lib/reconcileOrganization.js +0 -110
  159. package/dist/modules/attachments/lib/reconcileOrganization.js.map +0 -7
  160. package/dist/modules/attachments/lib/requestScope.js +0 -10
  161. package/dist/modules/attachments/lib/requestScope.js.map +0 -7
  162. package/dist/modules/customers/components/detail/useDealsAccess.js +0 -16
  163. package/dist/modules/customers/components/detail/useDealsAccess.js.map +0 -7
  164. package/src/modules/attachments/lib/reconcileOrganization.ts +0 -188
  165. package/src/modules/attachments/lib/requestScope.ts +0 -27
  166. package/src/modules/customers/components/detail/useDealsAccess.ts +0 -19
@@ -1,4 +1,4 @@
1
- [build:core] found 3475 entry points
1
+ [build:core] found 3473 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 186 entry points
4
4
  [build:core:generated] built successfully
@@ -9,8 +9,6 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
9
9
  import { enforceTenantSelection, resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
10
10
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
11
11
  import { assertActorCanGrantRoles } from "@open-mercato/core/modules/auth/lib/grantChecks";
12
- import { isOrganizationAccessAllowed } from "@open-mercato/shared/lib/auth/organizationAccess";
13
- import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
14
12
  const listQuerySchema = z.object({
15
13
  page: z.string().optional(),
16
14
  pageSize: z.string().optional(),
@@ -275,30 +273,18 @@ const crud = makeCrudRoute({
275
273
  const { translate } = await resolveTranslations();
276
274
  if (!auth?.tenantId) throw json({ error: translate("api_keys.errors.tenantRequired", "Tenant context required") }, { status: 400 });
277
275
  const em = ctx.container.resolve("em");
276
+ const record = await em.findOne(ApiKey, { id, deletedAt: null });
277
+ if (!record) throw json({ error: translate("api_keys.errors.notFound", "Not found") }, { status: 404 });
278
278
  const scopedCtx = ctx;
279
279
  const isSuperAdmin = await resolveIsSuperAdmin(scopedCtx);
280
- const allowedIds = ctx.organizationScope?.allowedIds ?? null;
281
- const recordFilter = {
282
- id,
283
- tenantId: auth.tenantId,
284
- deletedAt: null
285
- };
286
- if (!isSuperAdmin && Array.isArray(allowedIds)) {
287
- recordFilter.organizationId = { $in: allowedIds };
280
+ if (!isSuperAdmin && record.tenantId && record.tenantId !== auth.tenantId) {
281
+ throw json({ error: translate("api_keys.errors.forbidden", "Forbidden") }, { status: 403 });
288
282
  }
289
- const record = await findOneWithDecryption(
290
- em,
291
- ApiKey,
292
- recordFilter,
293
- void 0,
294
- { tenantId: auth.tenantId, organizationId: null }
295
- );
296
- if (!record || record.tenantId !== auth.tenantId || !isOrganizationAccessAllowed({
297
- isSuperAdmin,
298
- allowedOrganizationIds: allowedIds,
299
- targetOrganizationId: record.organizationId ?? null
300
- })) {
301
- throw json({ error: translate("api_keys.errors.notFound", "Not found") }, { status: 404 });
283
+ const allowedIds = ctx.organizationScope?.allowedIds ?? null;
284
+ if (record.organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {
285
+ if (!allowedIds.includes(record.organizationId)) {
286
+ throw json({ error: translate("api_keys.errors.organizationOutOfScope", "Organization out of scope") }, { status: 403 });
287
+ }
302
288
  }
303
289
  scopedCtx.__apiKeyOrganizationId = record.organizationId ?? null;
304
290
  },
@@ -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 { generateApiKeySecret, hashApiKey } from '../../services/apiKeyService'\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'\n\ntype ApiKeyCrudCtx = CrudCtx & {\n __apiKeySecret?: { secret: string; prefix: string }\n __apiKeyHash?: string\n __apiKeyRoleIds?: string[]\n __apiKeyRoles?: Role[]\n __apiKeyOrganizationId?: string | null\n __apiKeyTenantId?: string | null\n}\n\ntype ApiKeyEntityWithMeta = ApiKey & {\n __apiKeySecret?: string\n __apiKeyRoles?: Role[]\n}\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\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 create: {\n schema: createApiKeySchema,\n mapToEntity: (input, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const keyHash = scopedCtx.__apiKeyHash\n if (!secretData || !keyHash) throw new Error('API key secret not prepared')\n const roleIds = Array.isArray(scopedCtx.__apiKeyRoleIds) ? scopedCtx.__apiKeyRoleIds : []\n const organizationId = scopedCtx.__apiKeyOrganizationId ?? null\n const tenantId = scopedCtx.__apiKeyTenantId ?? null\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId,\n organizationId,\n keyHash,\n keyPrefix: secretData.prefix,\n rolesJson: roleIds,\n createdBy: ctx.auth?.sub ?? null,\n expiresAt: input.expiresAt ?? null,\n }\n },\n response: (entity) => {\n const meta = entity as ApiKeyEntityWithMeta\n const secret = meta.__apiKeySecret\n const roles = meta.__apiKeyRoles\n return {\n id: String(entity.id),\n name: entity.name,\n keyPrefix: entity.keyPrefix,\n secret,\n tenantId: entity.tenantId ?? null,\n organizationId: entity.organizationId ?? null,\n roles: Array.isArray(roles)\n ? roles.map((role) => ({ id: String(role.id), name: role.name ?? null }))\n : (Array.isArray(entity.rolesJson) ? entity.rolesJson.map((id: string) => ({ id, name: null })) : []),\n }\n },\n },\n del: { idFrom: 'query' },\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 if (organizationIds && organizationIds.length > 0) {\n qb.andWhere({ organizationId: { $in: organizationIds } })\n } else if (auth.orgId) {\n qb.andWhere({ organizationId: auth.orgId })\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 beforeCreate: async (input, 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\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId') ? input.tenantId : auth.tenantId\n const scopedCtx = ctx as ApiKeyCrudCtx\n const targetTenantId = await enforceTenantSelection(scopedCtx, requestedTenant)\n scopedCtx.__apiKeyTenantId = targetTenantId\n\n const secretData = generateApiKeySecret()\n scopedCtx.__apiKeySecret = secretData\n scopedCtx.__apiKeyHash = await hashApiKey(secretData.secret)\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const roleTokens = Array.isArray(input.roles) ? input.roles.filter((value) => typeof value === 'string' && value.trim().length > 0) : []\n const roleEntities: Role[] = []\n const roleIds: string[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const rawTenantId = targetTenantId ?? auth.tenantId ?? null\n const effectiveTenantId = typeof rawTenantId === 'string' && rawTenantId.trim().length > 0 ? rawTenantId.trim() : null\n const normalizedEffectiveTenantId = effectiveTenantId ? effectiveTenantId.toLowerCase() : null\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 nameFilter: FilterQuery<Role> = { name: value, deletedAt: null }\n if (normalizedEffectiveTenantId) {\n nameFilter.$or = [\n { tenantId: effectiveTenantId },\n { tenantId: null },\n ]\n } else {\n nameFilter.tenantId = null\n }\n const candidates = await em.find(Role, nameFilter, { limit: 5 })\n if (normalizedEffectiveTenantId) {\n role =\n candidates.find((candidate) => {\n if (!candidate.tenantId) return false\n return String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n }) ??\n candidates.find((candidate) => candidate.tenantId === null) ??\n null\n } else {\n role = candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n role = candidates[0] ?? null\n }\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) : null\n const normalizedRoleTenantId = roleTenantId ? roleTenantId.toLowerCase() : null\n if (normalizedRoleTenantId && normalizedEffectiveTenantId && normalizedRoleTenantId !== 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 roleIds.push(String(role.id))\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 scopedCtx.__apiKeyRoles = roleEntities\n scopedCtx.__apiKeyRoleIds = roleIds\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = input.organizationId ?? ctx.selectedOrganizationId ?? auth.orgId ?? null\n if (organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(organizationId)) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n }\n scopedCtx.__apiKeyOrganizationId = organizationId ?? null\n\n return { ...input, organizationId }\n },\n afterCreate: async (entity, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const roles = scopedCtx.__apiKeyRoles\n if (secretData) (entity as ApiKeyEntityWithMeta).__apiKeySecret = secretData.secret\n if (roles) (entity as ApiKeyEntityWithMeta).__apiKeyRoles = roles\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${entity.id}`)\n } catch {}\n },\n beforeDelete: async (id, 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 em = (ctx.container.resolve('em') as EntityManager)\n const scopedCtx = ctx as ApiKeyCrudCtx\n const isSuperAdmin = await resolveIsSuperAdmin(scopedCtx)\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const recordFilter: FilterQuery<ApiKey> = {\n id,\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (!isSuperAdmin && Array.isArray(allowedIds)) {\n recordFilter.organizationId = { $in: allowedIds }\n }\n const record = await findOneWithDecryption(\n em,\n ApiKey,\n recordFilter,\n undefined,\n { tenantId: auth.tenantId, organizationId: null },\n )\n if (\n !record ||\n record.tenantId !== auth.tenantId ||\n !isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: record.organizationId ?? null,\n })\n ) {\n throw json({ error: translate('api_keys.errors.notFound', 'Not found') }, { status: 404 })\n }\n scopedCtx.__apiKeyOrganizationId = record.organizationId ?? null\n },\n afterDelete: async (id, ctx) => {\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${id}`)\n } catch {}\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 } 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 { generateApiKeySecret, hashApiKey } from '../../services/apiKeyService'\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'\n\ntype ApiKeyCrudCtx = CrudCtx & {\n __apiKeySecret?: { secret: string; prefix: string }\n __apiKeyHash?: string\n __apiKeyRoleIds?: string[]\n __apiKeyRoles?: Role[]\n __apiKeyOrganizationId?: string | null\n __apiKeyTenantId?: string | null\n}\n\ntype ApiKeyEntityWithMeta = ApiKey & {\n __apiKeySecret?: string\n __apiKeyRoles?: Role[]\n}\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\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 create: {\n schema: createApiKeySchema,\n mapToEntity: (input, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const keyHash = scopedCtx.__apiKeyHash\n if (!secretData || !keyHash) throw new Error('API key secret not prepared')\n const roleIds = Array.isArray(scopedCtx.__apiKeyRoleIds) ? scopedCtx.__apiKeyRoleIds : []\n const organizationId = scopedCtx.__apiKeyOrganizationId ?? null\n const tenantId = scopedCtx.__apiKeyTenantId ?? null\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId,\n organizationId,\n keyHash,\n keyPrefix: secretData.prefix,\n rolesJson: roleIds,\n createdBy: ctx.auth?.sub ?? null,\n expiresAt: input.expiresAt ?? null,\n }\n },\n response: (entity) => {\n const meta = entity as ApiKeyEntityWithMeta\n const secret = meta.__apiKeySecret\n const roles = meta.__apiKeyRoles\n return {\n id: String(entity.id),\n name: entity.name,\n keyPrefix: entity.keyPrefix,\n secret,\n tenantId: entity.tenantId ?? null,\n organizationId: entity.organizationId ?? null,\n roles: Array.isArray(roles)\n ? roles.map((role) => ({ id: String(role.id), name: role.name ?? null }))\n : (Array.isArray(entity.rolesJson) ? entity.rolesJson.map((id: string) => ({ id, name: null })) : []),\n }\n },\n },\n del: { idFrom: 'query' },\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 if (organizationIds && organizationIds.length > 0) {\n qb.andWhere({ organizationId: { $in: organizationIds } })\n } else if (auth.orgId) {\n qb.andWhere({ organizationId: auth.orgId })\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 beforeCreate: async (input, 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\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId') ? input.tenantId : auth.tenantId\n const scopedCtx = ctx as ApiKeyCrudCtx\n const targetTenantId = await enforceTenantSelection(scopedCtx, requestedTenant)\n scopedCtx.__apiKeyTenantId = targetTenantId\n\n const secretData = generateApiKeySecret()\n scopedCtx.__apiKeySecret = secretData\n scopedCtx.__apiKeyHash = await hashApiKey(secretData.secret)\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const roleTokens = Array.isArray(input.roles) ? input.roles.filter((value) => typeof value === 'string' && value.trim().length > 0) : []\n const roleEntities: Role[] = []\n const roleIds: string[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const rawTenantId = targetTenantId ?? auth.tenantId ?? null\n const effectiveTenantId = typeof rawTenantId === 'string' && rawTenantId.trim().length > 0 ? rawTenantId.trim() : null\n const normalizedEffectiveTenantId = effectiveTenantId ? effectiveTenantId.toLowerCase() : null\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 nameFilter: FilterQuery<Role> = { name: value, deletedAt: null }\n if (normalizedEffectiveTenantId) {\n nameFilter.$or = [\n { tenantId: effectiveTenantId },\n { tenantId: null },\n ]\n } else {\n nameFilter.tenantId = null\n }\n const candidates = await em.find(Role, nameFilter, { limit: 5 })\n if (normalizedEffectiveTenantId) {\n role =\n candidates.find((candidate) => {\n if (!candidate.tenantId) return false\n return String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n }) ??\n candidates.find((candidate) => candidate.tenantId === null) ??\n null\n } else {\n role = candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n role = candidates[0] ?? null\n }\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) : null\n const normalizedRoleTenantId = roleTenantId ? roleTenantId.toLowerCase() : null\n if (normalizedRoleTenantId && normalizedEffectiveTenantId && normalizedRoleTenantId !== 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 roleIds.push(String(role.id))\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 scopedCtx.__apiKeyRoles = roleEntities\n scopedCtx.__apiKeyRoleIds = roleIds\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = input.organizationId ?? ctx.selectedOrganizationId ?? auth.orgId ?? null\n if (organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(organizationId)) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n }\n scopedCtx.__apiKeyOrganizationId = organizationId ?? null\n\n return { ...input, organizationId }\n },\n afterCreate: async (entity, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const roles = scopedCtx.__apiKeyRoles\n if (secretData) (entity as ApiKeyEntityWithMeta).__apiKeySecret = secretData.secret\n if (roles) (entity as ApiKeyEntityWithMeta).__apiKeyRoles = roles\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${entity.id}`)\n } catch {}\n },\n beforeDelete: async (id, 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 em = (ctx.container.resolve('em') as EntityManager)\n const record = await em.findOne(ApiKey, { id, deletedAt: null })\n if (!record) throw json({ error: translate('api_keys.errors.notFound', 'Not found') }, { status: 404 })\n const scopedCtx = ctx as ApiKeyCrudCtx\n const isSuperAdmin = await resolveIsSuperAdmin(scopedCtx)\n if (!isSuperAdmin && record.tenantId && record.tenantId !== auth.tenantId) {\n throw json({ error: translate('api_keys.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n if (record.organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(record.organizationId)) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n }\n scopedCtx.__apiKeyOrganizationId = record.organizationId ?? null\n },\n afterDelete: async (id, ctx) => {\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${id}`)\n } catch {}\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
  }
@@ -2,7 +2,6 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
2
2
  import { createApiKey } from "./services/apiKeyService.js";
3
3
  import { Organization } from "@open-mercato/core/modules/directory/data/entities";
4
4
  import { Role } from "@open-mercato/core/modules/auth/data/entities";
5
- import { parseCommaSeparatedList } from "@open-mercato/shared/lib/string";
6
5
  function parseArgs(rest) {
7
6
  const args = {};
8
7
  for (let i = 0; i < rest.length; i += 2) {
@@ -40,7 +39,7 @@ const addApiKey = {
40
39
  console.error("Tenant context is required. Provide --tenantId or select an organization belonging to a tenant.");
41
40
  return;
42
41
  }
43
- const roleInputs = parseCommaSeparatedList(rolesCsv);
42
+ const roleInputs = rolesCsv ? rolesCsv.split(",").map((value) => value.trim()).filter(Boolean) : [];
44
43
  const roleIds = [];
45
44
  if (roleInputs.length) {
46
45
  for (const token of roleInputs) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/api_keys/cli.ts"],
4
- "sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createApiKey } from './services/apiKeyService'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\n\nfunction parseArgs(rest: string[]): Record<string, string> {\n const args: Record<string, string> = {}\n for (let i = 0; i < rest.length; i += 2) {\n const key = rest[i]?.replace(/^--/, '')\n const value = rest[i + 1]\n if (key) args[key] = value ?? ''\n }\n return args\n}\n\nconst addApiKey: ModuleCli = {\n command: 'add',\n async run(rest) {\n const args = parseArgs(rest)\n const name = args.name || args.label\n const organizationId = args.organizationId || args.orgId || args.org\n let tenantId: string | null = args.tenantId || args.tenant || null\n const rolesCsv = args.roles || ''\n\n if (!name) {\n console.error('Usage: mercato api_keys add --name <name> [--organizationId <uuid>] [--tenantId <uuid>] [--roles roleA,roleB]')\n return\n }\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n\n let orgRecord: any = null\n if (organizationId) {\n orgRecord = await em.findOne(Organization, { id: organizationId })\n if (!orgRecord) {\n console.error('Organization not found:', organizationId)\n return\n }\n tenantId = tenantId ?? (orgRecord.tenant ? String(orgRecord.tenant.id) : null)\n }\n\n if (!tenantId) {\n console.error('Tenant context is required. Provide --tenantId or select an organization belonging to a tenant.')\n return\n }\n\n const roleInputs = parseCommaSeparatedList(rolesCsv)\n const roleIds: string[] = []\n if (roleInputs.length) {\n for (const token of roleInputs) {\n let role = await em.findOne(Role, { id: token })\n if (!role) {\n role = await em.findOne(Role, { name: token })\n }\n if (!role) {\n console.error(`Role not found: ${token}`)\n return\n }\n roleIds.push(String(role.id))\n }\n }\n\n const { record, secret } = await createApiKey(em, {\n name,\n description: args.description || null,\n organizationId: organizationId || null,\n tenantId,\n roles: roleIds,\n createdBy: null,\n expiresAt: args.expiresAt ? new Date(args.expiresAt) : null,\n }, { rbac })\n\n console.log('API key created:')\n console.log(' id:', record.id)\n console.log(' name:', record.name)\n console.log(' tenantId:', record.tenantId)\n console.log(' organizationId:', record.organizationId ?? 'null')\n if (roleIds.length) console.log(' roles:', roleInputs.join(', '))\n console.log(' keyPrefix:', record.keyPrefix)\n console.log('')\n console.log('Secret (store immediately):')\n console.log(secret)\n },\n}\n\nexport default [addApiKey]\n"],
5
- "mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AACrB,SAAS,+BAA+B;AAExC,SAAS,UAAU,MAAwC;AACzD,QAAM,OAA+B,CAAC;AACtC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;AACvC,UAAM,MAAM,KAAK,CAAC,GAAG,QAAQ,OAAO,EAAE;AACtC,UAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,QAAI,IAAK,MAAK,GAAG,IAAI,SAAS;AAAA,EAChC;AACA,SAAO;AACT;AAEA,MAAM,YAAuB;AAAA,EAC3B,SAAS;AAAA,EACT,MAAM,IAAI,MAAM;AACd,UAAM,OAAO,UAAU,IAAI;AAC3B,UAAM,OAAO,KAAK,QAAQ,KAAK;AAC/B,UAAM,iBAAiB,KAAK,kBAAkB,KAAK,SAAS,KAAK;AACjE,QAAI,WAA0B,KAAK,YAAY,KAAK,UAAU;AAC9D,UAAM,WAAW,KAAK,SAAS;AAE/B,QAAI,CAAC,MAAM;AACT,cAAQ,MAAM,+GAA+G;AAC7H;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,UAAM,KAAK,QAAQ,IAAI;AACvB,UAAM,OAAO,QAAQ,aAAa;AAElC,QAAI,YAAiB;AACrB,QAAI,gBAAgB;AAClB,kBAAY,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,eAAe,CAAC;AACjE,UAAI,CAAC,WAAW;AACd,gBAAQ,MAAM,2BAA2B,cAAc;AACvD;AAAA,MACF;AACA,iBAAW,aAAa,UAAU,SAAS,OAAO,UAAU,OAAO,EAAE,IAAI;AAAA,IAC3E;AAEA,QAAI,CAAC,UAAU;AACb,cAAQ,MAAM,iGAAiG;AAC/G;AAAA,IACF;AAEA,UAAM,aAAa,wBAAwB,QAAQ;AACnD,UAAM,UAAoB,CAAC;AAC3B,QAAI,WAAW,QAAQ;AACrB,iBAAW,SAAS,YAAY;AAC9B,YAAI,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,MAAM,CAAC;AAC/C,YAAI,CAAC,MAAM;AACT,iBAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,MAAM,MAAM,CAAC;AAAA,QAC/C;AACA,YAAI,CAAC,MAAM;AACT,kBAAQ,MAAM,mBAAmB,KAAK,EAAE;AACxC;AAAA,QACF;AACA,gBAAQ,KAAK,OAAO,KAAK,EAAE,CAAC;AAAA,MAC9B;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,OAAO,IAAI,MAAM,aAAa,IAAI;AAAA,MAChD;AAAA,MACA,aAAa,KAAK,eAAe;AAAA,MACjC,gBAAgB,kBAAkB;AAAA,MAClC;AAAA,MACA,OAAO;AAAA,MACP,WAAW;AAAA,MACX,WAAW,KAAK,YAAY,IAAI,KAAK,KAAK,SAAS,IAAI;AAAA,IACzD,GAAG,EAAE,KAAK,CAAC;AAEX,YAAQ,IAAI,kBAAkB;AAC9B,YAAQ,IAAI,SAAS,OAAO,EAAE;AAC9B,YAAQ,IAAI,WAAW,OAAO,IAAI;AAClC,YAAQ,IAAI,eAAe,OAAO,QAAQ;AAC1C,YAAQ,IAAI,qBAAqB,OAAO,kBAAkB,MAAM;AAChE,QAAI,QAAQ,OAAQ,SAAQ,IAAI,YAAY,WAAW,KAAK,IAAI,CAAC;AACjE,YAAQ,IAAI,gBAAgB,OAAO,SAAS;AAC5C,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,6BAA6B;AACzC,YAAQ,IAAI,MAAM;AAAA,EACpB;AACF;AAEA,IAAO,cAAQ,CAAC,SAAS;",
4
+ "sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createApiKey } from './services/apiKeyService'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\n\nfunction parseArgs(rest: string[]): Record<string, string> {\n const args: Record<string, string> = {}\n for (let i = 0; i < rest.length; i += 2) {\n const key = rest[i]?.replace(/^--/, '')\n const value = rest[i + 1]\n if (key) args[key] = value ?? ''\n }\n return args\n}\n\nconst addApiKey: ModuleCli = {\n command: 'add',\n async run(rest) {\n const args = parseArgs(rest)\n const name = args.name || args.label\n const organizationId = args.organizationId || args.orgId || args.org\n let tenantId: string | null = args.tenantId || args.tenant || null\n const rolesCsv = args.roles || ''\n\n if (!name) {\n console.error('Usage: mercato api_keys add --name <name> [--organizationId <uuid>] [--tenantId <uuid>] [--roles roleA,roleB]')\n return\n }\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n\n let orgRecord: any = null\n if (organizationId) {\n orgRecord = await em.findOne(Organization, { id: organizationId })\n if (!orgRecord) {\n console.error('Organization not found:', organizationId)\n return\n }\n tenantId = tenantId ?? (orgRecord.tenant ? String(orgRecord.tenant.id) : null)\n }\n\n if (!tenantId) {\n console.error('Tenant context is required. Provide --tenantId or select an organization belonging to a tenant.')\n return\n }\n\n const roleInputs = rolesCsv\n ? rolesCsv.split(',').map((value) => value.trim()).filter(Boolean)\n : []\n const roleIds: string[] = []\n if (roleInputs.length) {\n for (const token of roleInputs) {\n let role = await em.findOne(Role, { id: token })\n if (!role) {\n role = await em.findOne(Role, { name: token })\n }\n if (!role) {\n console.error(`Role not found: ${token}`)\n return\n }\n roleIds.push(String(role.id))\n }\n }\n\n const { record, secret } = await createApiKey(em, {\n name,\n description: args.description || null,\n organizationId: organizationId || null,\n tenantId,\n roles: roleIds,\n createdBy: null,\n expiresAt: args.expiresAt ? new Date(args.expiresAt) : null,\n }, { rbac })\n\n console.log('API key created:')\n console.log(' id:', record.id)\n console.log(' name:', record.name)\n console.log(' tenantId:', record.tenantId)\n console.log(' organizationId:', record.organizationId ?? 'null')\n if (roleIds.length) console.log(' roles:', roleInputs.join(', '))\n console.log(' keyPrefix:', record.keyPrefix)\n console.log('')\n console.log('Secret (store immediately):')\n console.log(secret)\n },\n}\n\nexport default [addApiKey]\n"],
5
+ "mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AAErB,SAAS,UAAU,MAAwC;AACzD,QAAM,OAA+B,CAAC;AACtC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;AACvC,UAAM,MAAM,KAAK,CAAC,GAAG,QAAQ,OAAO,EAAE;AACtC,UAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,QAAI,IAAK,MAAK,GAAG,IAAI,SAAS;AAAA,EAChC;AACA,SAAO;AACT;AAEA,MAAM,YAAuB;AAAA,EAC3B,SAAS;AAAA,EACT,MAAM,IAAI,MAAM;AACd,UAAM,OAAO,UAAU,IAAI;AAC3B,UAAM,OAAO,KAAK,QAAQ,KAAK;AAC/B,UAAM,iBAAiB,KAAK,kBAAkB,KAAK,SAAS,KAAK;AACjE,QAAI,WAA0B,KAAK,YAAY,KAAK,UAAU;AAC9D,UAAM,WAAW,KAAK,SAAS;AAE/B,QAAI,CAAC,MAAM;AACT,cAAQ,MAAM,+GAA+G;AAC7H;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,UAAM,KAAK,QAAQ,IAAI;AACvB,UAAM,OAAO,QAAQ,aAAa;AAElC,QAAI,YAAiB;AACrB,QAAI,gBAAgB;AAClB,kBAAY,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,eAAe,CAAC;AACjE,UAAI,CAAC,WAAW;AACd,gBAAQ,MAAM,2BAA2B,cAAc;AACvD;AAAA,MACF;AACA,iBAAW,aAAa,UAAU,SAAS,OAAO,UAAU,OAAO,EAAE,IAAI;AAAA,IAC3E;AAEA,QAAI,CAAC,UAAU;AACb,cAAQ,MAAM,iGAAiG;AAC/G;AAAA,IACF;AAEA,UAAM,aAAa,WACf,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO,IAC/D,CAAC;AACL,UAAM,UAAoB,CAAC;AAC3B,QAAI,WAAW,QAAQ;AACrB,iBAAW,SAAS,YAAY;AAC9B,YAAI,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,MAAM,CAAC;AAC/C,YAAI,CAAC,MAAM;AACT,iBAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,MAAM,MAAM,CAAC;AAAA,QAC/C;AACA,YAAI,CAAC,MAAM;AACT,kBAAQ,MAAM,mBAAmB,KAAK,EAAE;AACxC;AAAA,QACF;AACA,gBAAQ,KAAK,OAAO,KAAK,EAAE,CAAC;AAAA,MAC9B;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,OAAO,IAAI,MAAM,aAAa,IAAI;AAAA,MAChD;AAAA,MACA,aAAa,KAAK,eAAe;AAAA,MACjC,gBAAgB,kBAAkB;AAAA,MAClC;AAAA,MACA,OAAO;AAAA,MACP,WAAW;AAAA,MACX,WAAW,KAAK,YAAY,IAAI,KAAK,KAAK,SAAS,IAAI;AAAA,IACzD,GAAG,EAAE,KAAK,CAAC;AAEX,YAAQ,IAAI,kBAAkB;AAC9B,YAAQ,IAAI,SAAS,OAAO,EAAE;AAC9B,YAAQ,IAAI,WAAW,OAAO,IAAI;AAClC,YAAQ,IAAI,eAAe,OAAO,QAAQ;AAC1C,YAAQ,IAAI,qBAAqB,OAAO,kBAAkB,MAAM;AAChE,QAAI,QAAQ,OAAQ,SAAQ,IAAI,YAAY,WAAW,KAAK,IAAI,CAAC;AACjE,YAAQ,IAAI,gBAAgB,OAAO,SAAS;AAC5C,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,6BAA6B;AACzC,YAAQ,IAAI,MAAM;AAAA,EACpB;AACF;AAEA,IAAO,cAAQ,CAAC,SAAS;",
6
6
  "names": []
7
7
  }
@@ -13,7 +13,6 @@ import {
13
13
  canRenderInlineAttachment
14
14
  } from "@open-mercato/core/modules/attachments/lib/security";
15
15
  import { StorageDriverFactory } from "../../../lib/drivers/index.js";
16
- import { resolveAttachmentOrganizationId } from "@open-mercato/core/modules/attachments/lib/requestScope";
17
16
  const metadata = {
18
17
  GET: { requireAuth: false }
19
18
  };
@@ -26,14 +25,13 @@ async function GET(req, context) {
26
25
  );
27
26
  }
28
27
  const auth = await getAuthFromRequest(req);
29
- const container = await createRequestContainer();
30
- const em = container.resolve("em");
31
- const storageDriverFactory = container.resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
32
- const scopedAuth = auth ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) } : auth;
28
+ const { resolve } = await createRequestContainer();
29
+ const em = resolve("em");
30
+ const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
33
31
  const findFilter = { id };
34
- if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {
35
- if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;
36
- if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;
32
+ if (auth && !isSuperAdminAuth(auth)) {
33
+ if (auth.tenantId) findFilter.tenantId = auth.tenantId;
34
+ if (auth.orgId) findFilter.organizationId = auth.orgId;
37
35
  }
38
36
  const attachment = await em.findOne(Attachment, findFilter);
39
37
  if (!attachment) {
@@ -51,7 +49,7 @@ async function GET(req, context) {
51
49
  { status: 500 }
52
50
  );
53
51
  }
54
- const access = checkAttachmentAccess(scopedAuth, attachment, partition);
52
+ const access = checkAttachmentAccess(auth, attachment, partition);
55
53
  if (!access.ok) {
56
54
  const message = access.status === 401 ? "Unauthorized" : "Forbidden";
57
55
  return NextResponse.json({ error: message }, { status: access.status });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/attachments/api/file/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { NextRequest, NextResponse } from \"next/server\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\nimport { resolveAttachmentOrganizationId } 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 scopedAuth = auth\n ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) }\n : auth;\n const findFilter: Record<string, unknown> = { id };\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const access = checkAttachmentAccess(scopedAuth, attachment, partition);\n if (!access.ok) {\n const message = access.status === 401 ? \"Unauthorized\" : \"Forbidden\";\n return NextResponse.json({ error: message }, { status: access.status });\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n });\n let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const headers: Record<string, string> = {\n \"Cache-Control\": partition.isPublic\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n ],\n },\n },\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { 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';\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 { resolve } = await createRequestContainer();\n const em = resolve(\"em\") as EntityManager;\n const storageDriverFactory =\n (resolve(\"storageDriverFactory\") as StorageDriverFactory | null) ??\n new StorageDriverFactory(em);\n\n const findFilter: Record<string, unknown> = { id };\n if (auth && !isSuperAdminAuth(auth)) {\n if (auth.tenantId) findFilter.tenantId = auth.tenantId;\n if (auth.orgId) findFilter.organizationId = auth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const access = checkAttachmentAccess(auth, attachment, partition);\n if (!access.ok) {\n const message = access.status === 401 ? \"Unauthorized\" : \"Forbidden\";\n return NextResponse.json({ error: message }, { status: access.status });\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n });\n let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const headers: Record<string, string> = {\n \"Cache-Control\": partition.isPublic\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n ],\n },\n },\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -18,7 +18,6 @@ import {
18
18
  validateImageMagicBytes
19
19
  } from "@open-mercato/core/modules/attachments/lib/imageSafety";
20
20
  import { StorageDriverFactory } from "../../../../lib/drivers/index.js";
21
- import { resolveAttachmentOrganizationId } from "@open-mercato/core/modules/attachments/lib/requestScope";
22
21
  import { createLogger } from "@open-mercato/shared/lib/logger";
23
22
  const logger = createLogger("attachments").child({ component: "image" });
24
23
  const querySchema = z.object({
@@ -42,14 +41,13 @@ async function GET(req, context) {
42
41
  return NextResponse.json({ error: "Invalid size parameters" }, { status: 400 });
43
42
  }
44
43
  const { width, height, cropType } = parsedQuery.data;
45
- const container = await createRequestContainer();
46
- const em = container.resolve("em");
47
- const storageDriverFactory = container.resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
48
- const scopedAuth = auth ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) } : auth;
44
+ const { resolve } = await createRequestContainer();
45
+ const em = resolve("em");
46
+ const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
49
47
  const findFilter = { id };
50
- if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {
51
- if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;
52
- if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;
48
+ if (auth && !isSuperAdminAuth(auth)) {
49
+ if (auth.tenantId) findFilter.tenantId = auth.tenantId;
50
+ if (auth.orgId) findFilter.organizationId = auth.orgId;
53
51
  }
54
52
  const attachment = await em.findOne(Attachment, findFilter);
55
53
  if (!attachment) {
@@ -62,7 +60,7 @@ async function GET(req, context) {
62
60
  if (!partition) {
63
61
  return NextResponse.json({ error: "Partition misconfigured" }, { status: 500 });
64
62
  }
65
- const access = checkAttachmentAccess(scopedAuth, attachment, partition);
63
+ const access = checkAttachmentAccess(auth, attachment, partition);
66
64
  if (!access.ok) {
67
65
  const message = access.status === 401 ? "Unauthorized" : "Forbidden";
68
66
  return NextResponse.json({ error: message }, { status: access.status });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/attachments/api/image/%5Bid%5D/%5B%5B...slug%5D%5D/route.ts"],
4
- "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport sharp 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 { resolveAttachmentOrganizationId } 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 scopedAuth = auth\n ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) }\n : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = checkAttachmentAccess(scopedAuth, attachment, partition)\n if (!access.ok) {\n const message = access.status === 401 ? 'Unauthorized' : 'Forbidden'\n return NextResponse.json({ error: message }, { status: access.status })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: sharp.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': partition.isPublic ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport sharp 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 { 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 { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const storageDriverFactory =\n (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const findFilter: Record<string, unknown> = { id }\n if (auth && !isSuperAdminAuth(auth)) {\n if (auth.tenantId) findFilter.tenantId = auth.tenantId\n if (auth.orgId) findFilter.organizationId = auth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = checkAttachmentAccess(auth, attachment, partition)\n if (!access.ok) {\n const message = access.status === 401 ? 'Unauthorized' : 'Forbidden'\n return NextResponse.json({ error: message }, { status: access.status })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: sharp.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': partition.isPublic ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -12,7 +12,6 @@ import { StorageDriverFactory } from "../lib/drivers/index.js";
12
12
  import { OcrService, shouldUseLlmOcr } from "../lib/ocrService.js";
13
13
  import { clearAttachmentThumbnailCache } from "../lib/thumbnailCache.js";
14
14
  import { assertAttachmentScopeInvariant } from "../lib/access.js";
15
- import { resolveAttachmentOrganizationId } from "../lib/requestScope.js";
16
15
  import {
17
16
  mergeAttachmentMetadata,
18
17
  normalizeAttachmentAssignments,
@@ -176,11 +175,10 @@ async function GET(req) {
176
175
  return NextResponse.json({ error: "entityId and recordId are required" }, { status: 400 });
177
176
  }
178
177
  const { entityId, recordId, page, pageSize } = parsedQuery.data;
179
- const container = await createRequestContainer();
180
- const em = container.resolve("em");
181
- const orgId = await resolveAttachmentOrganizationId(container, auth, req);
178
+ const { resolve } = await createRequestContainer();
179
+ const em = resolve("em");
182
180
  const filter = { entityId, recordId, tenantId: auth.tenantId };
183
- if (orgId) filter.organizationId = orgId;
181
+ if (auth.orgId) filter.organizationId = auth.orgId;
184
182
  const orderBy = { createdAt: "DESC" };
185
183
  const usePaging = typeof page === "number" && typeof pageSize === "number";
186
184
  const total = usePaging ? await em.count(Attachment, filter) : null;
@@ -201,7 +199,7 @@ async function GET(req) {
201
199
  },
202
200
  {
203
201
  tenantId: auth.tenantId ?? null,
204
- organizationId: orgId ?? null
202
+ organizationId: auth.orgId ?? null
205
203
  }
206
204
  );
207
205
  return NextResponse.json({
@@ -243,6 +241,7 @@ async function POST(req) {
243
241
  }, { status: 400 });
244
242
  }
245
243
  const tenantId = auth.tenantId;
244
+ const orgId = auth.orgId;
246
245
  const contentType = req.headers.get("content-type") || "";
247
246
  if (!contentType.toLowerCase().includes("multipart/form-data")) {
248
247
  return NextResponse.json({ error: "Expected multipart/form-data" }, { status: 400 });
@@ -262,12 +261,10 @@ async function POST(req) {
262
261
  const partitionOverride = typeof partitionOverrideRaw === "string" && partitionOverrideRaw.trim().length > 0 ? sanitizePartitionCode(partitionOverrideRaw) : null;
263
262
  const tags = parseFormTags(form.get("tags"));
264
263
  const assignmentsFromForm = parseFormAssignments(form.get("assignments"));
265
- const container = await createRequestContainer();
266
- const em = container.resolve("em");
267
- const dataEngine = container.resolve("dataEngine");
268
- const storageDriverFactory = container.resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
269
- const orgId = await resolveAttachmentOrganizationId(container, auth, req);
270
- if (!orgId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
264
+ const { resolve } = await createRequestContainer();
265
+ const em = resolve("em");
266
+ const dataEngine = resolve("dataEngine");
267
+ const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
271
268
  await ensureDefaultPartitions(em);
272
269
  let partitionFromField = null;
273
270
  let fieldMaxAttachmentSizeMb = null;
@@ -387,12 +384,12 @@ async function POST(req) {
387
384
  }
388
385
  const metadata2 = mergeAttachmentMetadata(null, { assignments, tags });
389
386
  const attachmentId = randomUUID();
390
- assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: orgId });
387
+ assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: auth.orgId });
391
388
  const att = em.create(Attachment, {
392
389
  id: attachmentId,
393
390
  entityId,
394
391
  recordId,
395
- organizationId: orgId,
392
+ organizationId: auth.orgId,
396
393
  tenantId: auth.tenantId,
397
394
  fileName: safeName,
398
395
  mimeType: fileMimeType,
@@ -485,13 +482,12 @@ async function DELETE(req) {
485
482
  const url = new URL(req.url);
486
483
  const id = url.searchParams.get("id") || "";
487
484
  if (!id) return NextResponse.json({ error: "Attachment id is required" }, { status: 400 });
488
- const container = await createRequestContainer();
489
- const em = container.resolve("em");
490
- const dataEngine = container.resolve("dataEngine");
491
- const storageDriverFactory = container.resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
492
- const orgId = await resolveAttachmentOrganizationId(container, auth, req);
485
+ const { resolve } = await createRequestContainer();
486
+ const em = resolve("em");
487
+ const dataEngine = resolve("dataEngine");
488
+ const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
493
489
  const deleteFilter = { id, tenantId: auth.tenantId };
494
- if (orgId) deleteFilter.organizationId = orgId;
490
+ if (auth.orgId) deleteFilter.organizationId = auth.orgId;
495
491
  const record = await em.findOne(Attachment, deleteFilter);
496
492
  if (!record) return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
497
493
  await em.remove(record).flush();
@@ -501,7 +497,7 @@ async function DELETE(req) {
501
497
  if (record.storagePath) {
502
498
  const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
503
499
  tenantId: record.tenantId ?? auth.tenantId,
504
- organizationId: record.organizationId ?? orgId ?? ""
500
+ organizationId: record.organizationId ?? auth.orgId ?? ""
505
501
  });
506
502
  await delDriver.delete(record.partitionCode, record.storagePath);
507
503
  }