@open-mercato/core 0.10.1-develop.7804.1.347c813134 → 0.10.1-develop.7807.1.9b2b8953e9

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 6218 entry points
1
+ [build:core] found 6220 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -159,10 +159,14 @@ const crud = makeCrudRoute({
159
159
  const qb = em.createQueryBuilder(ApiKey, "k");
160
160
  qb.where({ deletedAt: null });
161
161
  qb.andWhere({ tenantId: auth.tenantId });
162
- if (organizationIds && organizationIds.length > 0) {
163
- qb.andWhere({ organizationId: { $in: organizationIds } });
164
- } else if (auth.orgId) {
165
- qb.andWhere({ organizationId: auth.orgId });
162
+ const organizationConstraint = organizationIds?.length ? { organizationId: { $in: organizationIds } } : auth.orgId ? { organizationId: auth.orgId } : null;
163
+ if (organizationConstraint) {
164
+ const tenantWideAccess = isOrganizationAccessAllowed({
165
+ targetOrganizationId: null,
166
+ allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],
167
+ isSuperAdmin: await resolveIsSuperAdmin(ctx)
168
+ });
169
+ qb.andWhere(tenantWideAccess ? { $or: [{ organizationId: null }, organizationConstraint] } : organizationConstraint);
166
170
  }
167
171
  if (search) {
168
172
  const pattern = `%${escapeLikePattern(search)}%`;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/api_keys/api/keys/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { ApiKey } from '../../data/entities'\nimport { createApiKeySchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport type { CreateApiKeyCommandInput } from '../../commands/keys'\n\nconst listQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n})\n\nconst apiKeyRoleSchema = z.object({\n id: z.string().describe('Role identifier or alias assigned to the key'),\n name: z.string().nullable().describe('Display name of the mapped role, if available'),\n})\n\nconst apiKeyListItemSchema = z.object({\n id: z.string().describe('API key identifier'),\n name: z.string().describe('Friendly label used to identify the key'),\n description: z.string().nullable().describe('Optional free-form description'),\n keyPrefix: z.string().describe('Public prefix exposed to clients'),\n organizationId: z.string().uuid().nullable().describe('Organization scope of the key'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().describe('Creation timestamp (ISO 8601)'),\n lastUsedAt: z.string().nullable().describe('Last time the key was observed in use (ISO 8601)'),\n expiresAt: z.string().nullable().describe('When the key expires (ISO 8601)'),\n roles: z.array(apiKeyRoleSchema).describe('Effective roles applied when this key authenticates'),\n})\n\nconst apiKeyCollectionResponseSchema = z.object({\n items: z.array(apiKeyListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst apiKeyCreateResponseSchema = z.object({\n id: z.string().describe('Newly created API key identifier'),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value. Shown once for secure persistence.').optional(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(apiKeyRoleSchema),\n})\n\nconst deleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nasync function prepareCreateCommandInput(\n input: z.infer<typeof createApiKeySchema>,\n ctx: CrudCtx,\n): Promise<CreateApiKeyCommandInput> {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) {\n throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId')\n ? input.tenantId\n : auth.tenantId\n const targetTenantId = await enforceTenantSelection(ctx, requestedTenant)\n const em = ctx.container.resolve('em') as EntityManager\n const roleTokens = input.roles.filter((value) => value.trim().length > 0)\n const roleEntities: Role[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const effectiveTenantId = targetTenantId ?? auth.tenantId\n const normalizedEffectiveTenantId = effectiveTenantId.toLowerCase()\n let role: Role | null = null\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {\n role = await em.findOne(Role, { id: value, deletedAt: null })\n }\n if (!role) {\n const candidates = await em.find(Role, {\n name: value,\n deletedAt: null,\n $or: [{ tenantId: effectiveTenantId }, { tenantId: null }],\n } as FilterQuery<Role>, { limit: 5 })\n role = candidates.find((candidate) => (\n candidate.tenantId !== null\n && String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n )) ?? candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n throw json({ error: translate('api_keys.errors.roleNotFound', `Role ${value} not found`, { identifier: value }) }, { status: 400 })\n }\n const roleTenantId = role.tenantId ? String(role.tenantId).toLowerCase() : null\n if (roleTenantId && roleTenantId !== normalizedEffectiveTenantId) {\n throw json({ error: translate('api_keys.errors.roleWrongTenant', `Role ${role.name} belongs to another tenant`, { role: role.name ?? value }) }, { status: 400 })\n }\n roleEntities.push(role)\n }\n await assertActorCanGrantRoles({\n em,\n rbacService: ctx.container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n roles: roleEntities,\n })\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = Object.prototype.hasOwnProperty.call(input, 'organizationId')\n ? input.organizationId ?? null\n : ctx.selectedOrganizationId ?? auth.orgId ?? null\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n if (!isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: organizationId,\n })) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId: targetTenantId,\n organizationId,\n roleIds: roleEntities.map((role) => String(role.id)),\n expiresAt: input.expiresAt ?? null,\n }\n}\n\nconst crud = makeCrudRoute<\n z.infer<typeof createApiKeySchema>,\n never,\n z.infer<typeof listQuerySchema>\n>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['api_keys.view'] },\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n DELETE: { requireAuth: true, requireFeatures: ['api_keys.delete'] },\n },\n orm: { entity: ApiKey, orgField: null },\n list: { schema: listQuerySchema },\n del: { idFrom: 'query' },\n actions: {\n create: {\n commandId: 'api_keys.keys.create',\n schema: createApiKeySchema,\n mapInput: ({ parsed, ctx }) => prepareCreateCommandInput(parsed, ctx),\n response: ({ result }) => result,\n },\n delete: {\n commandId: 'api_keys.keys.delete',\n mapInput: ({ raw }) => ({ id: String(raw.query?.id ?? '') }),\n response: () => ({ success: true }),\n },\n },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 200)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const qb = em.createQueryBuilder(ApiKey, 'k')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n 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 },\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 { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport type { CreateApiKeyCommandInput } from '../../commands/keys'\n\nconst listQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n})\n\nconst apiKeyRoleSchema = z.object({\n id: z.string().describe('Role identifier or alias assigned to the key'),\n name: z.string().nullable().describe('Display name of the mapped role, if available'),\n})\n\nconst apiKeyListItemSchema = z.object({\n id: z.string().describe('API key identifier'),\n name: z.string().describe('Friendly label used to identify the key'),\n description: z.string().nullable().describe('Optional free-form description'),\n keyPrefix: z.string().describe('Public prefix exposed to clients'),\n organizationId: z.string().uuid().nullable().describe('Organization scope of the key'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().describe('Creation timestamp (ISO 8601)'),\n lastUsedAt: z.string().nullable().describe('Last time the key was observed in use (ISO 8601)'),\n expiresAt: z.string().nullable().describe('When the key expires (ISO 8601)'),\n roles: z.array(apiKeyRoleSchema).describe('Effective roles applied when this key authenticates'),\n})\n\nconst apiKeyCollectionResponseSchema = z.object({\n items: z.array(apiKeyListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst apiKeyCreateResponseSchema = z.object({\n id: z.string().describe('Newly created API key identifier'),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value. Shown once for secure persistence.').optional(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(apiKeyRoleSchema),\n})\n\nconst deleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nasync function prepareCreateCommandInput(\n input: z.infer<typeof createApiKeySchema>,\n ctx: CrudCtx,\n): Promise<CreateApiKeyCommandInput> {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) {\n throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId')\n ? input.tenantId\n : auth.tenantId\n const targetTenantId = await enforceTenantSelection(ctx, requestedTenant)\n const em = ctx.container.resolve('em') as EntityManager\n const roleTokens = input.roles.filter((value) => value.trim().length > 0)\n const roleEntities: Role[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const effectiveTenantId = targetTenantId ?? auth.tenantId\n const normalizedEffectiveTenantId = effectiveTenantId.toLowerCase()\n let role: Role | null = null\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {\n role = await em.findOne(Role, { id: value, deletedAt: null })\n }\n if (!role) {\n const candidates = await em.find(Role, {\n name: value,\n deletedAt: null,\n $or: [{ tenantId: effectiveTenantId }, { tenantId: null }],\n } as FilterQuery<Role>, { limit: 5 })\n role = candidates.find((candidate) => (\n candidate.tenantId !== null\n && String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n )) ?? candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n throw json({ error: translate('api_keys.errors.roleNotFound', `Role ${value} not found`, { identifier: value }) }, { status: 400 })\n }\n const roleTenantId = role.tenantId ? String(role.tenantId).toLowerCase() : null\n if (roleTenantId && roleTenantId !== normalizedEffectiveTenantId) {\n throw json({ error: translate('api_keys.errors.roleWrongTenant', `Role ${role.name} belongs to another tenant`, { role: role.name ?? value }) }, { status: 400 })\n }\n roleEntities.push(role)\n }\n await assertActorCanGrantRoles({\n em,\n rbacService: ctx.container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n roles: roleEntities,\n })\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = Object.prototype.hasOwnProperty.call(input, 'organizationId')\n ? input.organizationId ?? null\n : ctx.selectedOrganizationId ?? auth.orgId ?? null\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n if (!isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: organizationId,\n })) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId: targetTenantId,\n organizationId,\n roleIds: roleEntities.map((role) => String(role.id)),\n expiresAt: input.expiresAt ?? null,\n }\n}\n\nconst crud = makeCrudRoute<\n z.infer<typeof createApiKeySchema>,\n never,\n z.infer<typeof listQuerySchema>\n>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['api_keys.view'] },\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n DELETE: { requireAuth: true, requireFeatures: ['api_keys.delete'] },\n },\n orm: { entity: ApiKey, orgField: null },\n list: { schema: listQuerySchema },\n del: { idFrom: 'query' },\n actions: {\n create: {\n commandId: 'api_keys.keys.create',\n schema: createApiKeySchema,\n mapInput: ({ parsed, ctx }) => prepareCreateCommandInput(parsed, ctx),\n response: ({ result }) => result,\n },\n delete: {\n commandId: 'api_keys.keys.delete',\n mapInput: ({ raw }) => ({ id: String(raw.query?.id ?? '') }),\n response: () => ({ success: true }),\n },\n },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 200)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const qb = em.createQueryBuilder(ApiKey, 'k')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n const organizationConstraint = organizationIds?.length\n ? { organizationId: { $in: organizationIds } }\n : auth.orgId ? { organizationId: auth.orgId } : null\n if (organizationConstraint) {\n const tenantWideAccess = isOrganizationAccessAllowed({\n targetOrganizationId: null,\n allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],\n isSuperAdmin: await resolveIsSuperAdmin(ctx),\n })\n qb.andWhere(tenantWideAccess\n ? { $or: [{ organizationId: null }, organizationConstraint] }\n : organizationConstraint)\n }\n if (search) {\n const pattern = `%${escapeLikePattern(search)}%`\n qb.andWhere({\n $or: [\n { name: { $ilike: pattern } },\n { keyPrefix: { $ilike: pattern } },\n ],\n })\n }\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset((page - 1) * pageSize)\n const [items, total] = await qb.getResultAndCount()\n\n if (!items.length) {\n throw json({ items: [], total, page, pageSize, totalPages: Math.ceil(total / pageSize) })\n }\n\n const roleIdSet = new Set<string>()\n const orgIdSet = new Set<string>()\n for (const item of items) {\n if (Array.isArray(item.rolesJson)) {\n for (const roleId of item.rolesJson) roleIdSet.add(String(roleId))\n }\n if (item.organizationId) orgIdSet.add(String(item.organizationId))\n }\n\n const roleIdArray = Array.from(roleIdSet)\n const organizationIdArray = Array.from(orgIdSet)\n const roleFilter: FilterQuery<Role> = { id: { $in: roleIdArray } }\n const organizationFilter: FilterQuery<Organization> = { id: { $in: organizationIdArray } }\n const [roles, organizations] = await Promise.all([\n roleIdArray.length ? em.find(Role, roleFilter) : [],\n organizationIdArray.length ? em.find(Organization, organizationFilter) : [],\n ])\n const roleMap = new Map(roles.map((role) => [String(role.id), role.name ?? null]))\n const orgMap = new Map(organizations.map((org) => [String(org.id), org.name ?? null]))\n\n const payload = {\n items: items.map((item) => ({\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n keyPrefix: item.keyPrefix,\n organizationId: item.organizationId ?? null,\n organizationName: item.organizationId ? orgMap.get(String(item.organizationId)) ?? null : null,\n createdAt: item.createdAt,\n lastUsedAt: item.lastUsedAt ?? null,\n expiresAt: item.expiresAt ?? null,\n roles: Array.isArray(item.rolesJson)\n ? item.rolesJson.map((id) => ({ id, name: roleMap.get(String(id)) ?? null }))\n : [],\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n }\n\n throw json(payload)\n },\n },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage API keys',\n description:\n 'Provides list, creation, and deletion capabilities for API keys scoped to the authenticated tenant and organization.',\n methods: {\n GET: {\n summary: 'List API keys',\n description:\n 'Returns paginated API keys visible to the current user, including per-key role assignments and organization context.',\n query: listQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Collection of API keys',\n schema: apiKeyCollectionResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Tenant context missing', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden by organization scope', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Create API key',\n description:\n 'Creates a new API key, returning the one-time secret value together with the generated key prefix and scope details.',\n requestBody: {\n contentType: 'application/json',\n schema: createApiKeySchema,\n description: 'API key definition including optional scope and role assignments.',\n },\n responses: [\n {\n status: 201,\n description: 'API key created successfully',\n schema: apiKeyCreateResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing tenant context', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete API key',\n description:\n 'Removes an API key by identifier. The key must belong to the current tenant and fall within the requester organization scope.',\n query: z.object({\n id: z.string().uuid().describe('API key identifier to delete'),\n }),\n responses: [\n { status: 200, description: 'Key deleted successfully', schema: deleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing or invalid identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n { status: 404, description: 'Key not found within scope', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,17 @@ const routeMetadata = {
13
13
  DELETE: { requireAuth: true, requireFeatures: ["progress.cancel"] }
14
14
  };
15
15
  const metadata = routeMetadata;
16
+ async function canMutateProgressJob(container, auth, job) {
17
+ if (job.startedByUserId === auth.sub) return true;
18
+ const rbacService = container.resolve("rbacService");
19
+ return rbacService.userHasAllFeatures(auth.sub, ["progress.manage"], {
20
+ tenantId: auth.tenantId ?? null,
21
+ organizationId: job.organizationId ?? null
22
+ });
23
+ }
24
+ function cannotCancelJobResponse() {
25
+ return NextResponse.json({ error: "Cannot cancel this job" }, { status: 400 });
26
+ }
16
27
  async function GET(req, { params }) {
17
28
  const auth = await getAuthFromRequest(req);
18
29
  if (!auth || !auth.tenantId) {
@@ -83,7 +94,7 @@ async function PUT(req, { params }) {
83
94
  userId: auth.sub
84
95
  };
85
96
  const existing = await progressService.getJob(params.id, progressContext);
86
- if (!existing) {
97
+ if (!existing || !await canMutateProgressJob(container, auth, existing)) {
87
98
  return NextResponse.json({ error: "Not found" }, { status: 404 });
88
99
  }
89
100
  const job = await progressService.updateProgress(params.id, parsed.data, progressContext);
@@ -107,10 +118,14 @@ async function DELETE(req, { params }) {
107
118
  userId: auth.sub
108
119
  };
109
120
  try {
121
+ const existing = await progressService.getJob(params.id, progressContext);
122
+ if (!existing || !await canMutateProgressJob(container, auth, existing)) {
123
+ return cannotCancelJobResponse();
124
+ }
110
125
  await progressService.cancelJob(params.id, progressContext);
111
126
  return NextResponse.json({ ok: true });
112
127
  } catch {
113
- return NextResponse.json({ error: "Cannot cancel this job" }, { status: 400 });
128
+ return cannotCancelJobResponse();
114
129
  }
115
130
  }
116
131
  const openApi = {
@@ -125,7 +140,7 @@ const openApi = {
125
140
  },
126
141
  PUT: {
127
142
  summary: "Update progress job",
128
- description: "Updates progress metrics and heartbeat for a running job.",
143
+ description: "Updates progress metrics and heartbeat for a running job. Callers can update jobs they started; updating another user's job requires progress.manage.",
129
144
  tags: ["Progress"],
130
145
  responses: {
131
146
  200: { description: "Progress updated" },
@@ -135,7 +150,7 @@ const openApi = {
135
150
  },
136
151
  DELETE: {
137
152
  summary: "Cancel progress job",
138
- description: "Requests cancellation of a running or pending job.",
153
+ description: "Requests cancellation of a running or pending job. Callers can cancel jobs they started; cancelling another user's job requires progress.manage.",
139
154
  tags: ["Progress"],
140
155
  responses: {
141
156
  200: { description: "Cancellation requested" },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/progress/api/jobs/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { updateProgressSchema } from '../../../data/validators'\nimport type { ProgressService } from '../../../lib/progressService'\nimport {\n hasReadableProgressScope,\n hasWritableProgressScope,\n resolveProgressRequestScope,\n} from '../../requestScope'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['progress.view'] },\n PUT: { requireAuth: true, requireFeatures: ['progress.update'] },\n DELETE: { requireAuth: true, requireFeatures: ['progress.cancel'] },\n}\n\nexport const metadata = routeMetadata\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasReadableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const job = await progressService.getJob(params.id, {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n })\n\n if (!job) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n id: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount,\n etaSeconds: job.etaSeconds,\n cancellable: job.cancellable,\n meta: job.meta,\n resultSummary: job.resultSummary,\n errorMessage: job.errorMessage,\n startedByUserId: job.startedByUserId,\n startedAt: job.startedAt?.toISOString() ?? null,\n heartbeatAt: job.heartbeatAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n parentJobId: job.parentJobId,\n partitionIndex: job.partitionIndex,\n partitionCount: job.partitionCount,\n createdAt: job.createdAt.toISOString(),\n updatedAt: job.updatedAt.toISOString(),\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json()\n const parsed = updateProgressSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n const existing = await progressService.getJob(params.id, progressContext)\n if (!existing) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const job = await progressService.updateProgress(params.id, parsed.data, progressContext)\n\n return NextResponse.json({ ok: true, progressPercent: job.progressPercent })\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const progressService = container.resolve('progressService') as ProgressService\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n // No existence pre-check here on purpose. `cancelJob` already loads the job\n // through the SAME scoped filter and throws when it resolves to nothing, so a\n // job outside the caller's organization scope is rejected exactly like one that\n // does not exist \u2014 and the route keeps its documented answer for an unknown id\n // (400 \"Cannot cancel this job\", TC-PROG-009) instead of a 404.\n try {\n await progressService.cancelJob(params.id, progressContext)\n return NextResponse.json({ ok: true })\n } catch {\n return NextResponse.json({ error: 'Cannot cancel this job' }, { status: 400 })\n }\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get progress job details',\n description: 'Returns full details of a specific progress job by ID.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Progress job details' },\n 404: { description: 'Job not found' },\n },\n },\n PUT: {\n summary: 'Update progress job',\n description: 'Updates progress metrics and heartbeat for a running job.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Progress updated' },\n 400: { description: 'Invalid input' },\n 404: { description: 'Job not found' },\n },\n },\n DELETE: {\n summary: 'Cancel progress job',\n description: 'Requests cancellation of a running or pending job.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Cancellation requested' },\n 400: { description: 'Cannot cancel this job' },\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { AwilixContainer } from 'awilix'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { updateProgressSchema } from '../../../data/validators'\nimport type { ProgressJob } from '../../../data/entities'\nimport type { ProgressService } from '../../../lib/progressService'\nimport {\n hasReadableProgressScope,\n hasWritableProgressScope,\n resolveProgressRequestScope,\n} from '../../requestScope'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['progress.view'] },\n PUT: { requireAuth: true, requireFeatures: ['progress.update'] },\n DELETE: { requireAuth: true, requireFeatures: ['progress.cancel'] },\n}\n\nexport const metadata = routeMetadata\n\nasync function canMutateProgressJob(\n container: AwilixContainer,\n auth: NonNullable<AuthContext>,\n job: Pick<ProgressJob, 'startedByUserId' | 'organizationId'>,\n): Promise<boolean> {\n if (job.startedByUserId === auth.sub) return true\n const rbacService = container.resolve('rbacService') as RbacService\n return rbacService.userHasAllFeatures(auth.sub, ['progress.manage'], {\n tenantId: auth.tenantId ?? null,\n organizationId: job.organizationId ?? null,\n })\n}\n\nfunction cannotCancelJobResponse() {\n return NextResponse.json({ error: 'Cannot cancel this job' }, { status: 400 })\n}\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasReadableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const job = await progressService.getJob(params.id, {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n })\n\n if (!job) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n id: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount,\n etaSeconds: job.etaSeconds,\n cancellable: job.cancellable,\n meta: job.meta,\n resultSummary: job.resultSummary,\n errorMessage: job.errorMessage,\n startedByUserId: job.startedByUserId,\n startedAt: job.startedAt?.toISOString() ?? null,\n heartbeatAt: job.heartbeatAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n parentJobId: job.parentJobId,\n partitionIndex: job.partitionIndex,\n partitionCount: job.partitionCount,\n createdAt: job.createdAt.toISOString(),\n updatedAt: job.updatedAt.toISOString(),\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json()\n const parsed = updateProgressSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n const existing = await progressService.getJob(params.id, progressContext)\n if (!existing || !(await canMutateProgressJob(container, auth, existing))) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const job = await progressService.updateProgress(params.id, parsed.data, progressContext)\n\n return NextResponse.json({ ok: true, progressPercent: job.progressPercent })\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const progressService = container.resolve('progressService') as ProgressService\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n try {\n const existing = await progressService.getJob(params.id, progressContext)\n if (!existing || !(await canMutateProgressJob(container, auth, existing))) {\n return cannotCancelJobResponse()\n }\n await progressService.cancelJob(params.id, progressContext)\n return NextResponse.json({ ok: true })\n } catch {\n return cannotCancelJobResponse()\n }\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get progress job details',\n description: 'Returns full details of a specific progress job by ID.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Progress job details' },\n 404: { description: 'Job not found' },\n },\n },\n PUT: {\n summary: 'Update progress job',\n description: 'Updates progress metrics and heartbeat for a running job. Callers can update jobs they started; updating another user\\'s job requires progress.manage.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Progress updated' },\n 400: { description: 'Invalid input' },\n 404: { description: 'Job not found' },\n },\n },\n DELETE: {\n summary: 'Cancel progress job',\n description: 'Requests cancellation of a running or pending job. Callers can cancel jobs they started; cancelling another user\\'s job requires progress.manage.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Cancellation requested' },\n 400: { description: 'Cannot cancel this job' },\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7804.1.347c813134",
3
+ "version": "0.10.1-develop.7807.1.9b2b8953e9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.4.3"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7804.1.347c813134",
270
- "@open-mercato/shared": "0.10.1-develop.7804.1.347c813134",
271
- "@open-mercato/ui": "0.10.1-develop.7804.1.347c813134",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7807.1.9b2b8953e9",
270
+ "@open-mercato/shared": "0.10.1-develop.7807.1.9b2b8953e9",
271
+ "@open-mercato/ui": "0.10.1-develop.7807.1.9b2b8953e9",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7804.1.347c813134",
277
- "@open-mercato/shared": "0.10.1-develop.7804.1.347c813134",
278
- "@open-mercato/ui": "0.10.1-develop.7804.1.347c813134",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7807.1.9b2b8953e9",
277
+ "@open-mercato/shared": "0.10.1-develop.7807.1.9b2b8953e9",
278
+ "@open-mercato/ui": "0.10.1-develop.7807.1.9b2b8953e9",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -193,10 +193,18 @@ const crud = makeCrudRoute<
193
193
  const qb = em.createQueryBuilder(ApiKey, 'k')
194
194
  qb.where({ deletedAt: null })
195
195
  qb.andWhere({ tenantId: auth.tenantId })
196
- if (organizationIds && organizationIds.length > 0) {
197
- qb.andWhere({ organizationId: { $in: organizationIds } })
198
- } else if (auth.orgId) {
199
- qb.andWhere({ organizationId: auth.orgId })
196
+ const organizationConstraint = organizationIds?.length
197
+ ? { organizationId: { $in: organizationIds } }
198
+ : auth.orgId ? { organizationId: auth.orgId } : null
199
+ if (organizationConstraint) {
200
+ const tenantWideAccess = isOrganizationAccessAllowed({
201
+ targetOrganizationId: null,
202
+ allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],
203
+ isSuperAdmin: await resolveIsSuperAdmin(ctx),
204
+ })
205
+ qb.andWhere(tenantWideAccess
206
+ ? { $or: [{ organizationId: null }, organizationConstraint] }
207
+ : organizationConstraint)
200
208
  }
201
209
  if (search) {
202
210
  const pattern = `%${escapeLikePattern(search)}%`
@@ -238,9 +238,9 @@ and both belong to the adapter to state.
238
238
  Unless the caller passes `createProgressJob: false`, every run started through
239
239
  `startDataSyncRun` (`lib/start-run.ts`) — the run route, Retry, the scheduled
240
240
  worker and provider-owned routes — creates a `ProgressJob` that `ProgressTopBar`
241
- shows to every user with `progress.view`, with Cancel for anyone holding
242
- `progress.cancel`. An adapter may describe that
243
- job per entity type:
241
+ shows to every user with `progress.view`; its Cancel works for the user who
242
+ started the run (`progress.cancel`) or anyone holding `progress.manage`. An
243
+ adapter may describe that job per entity type:
244
244
 
245
245
  ```typescript
246
246
  describeProgressJob: ({ entityType }) =>
@@ -1,7 +1,10 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
2
+ import { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'
3
3
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import type { AwilixContainer } from 'awilix'
5
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
4
6
  import { updateProgressSchema } from '../../../data/validators'
7
+ import type { ProgressJob } from '../../../data/entities'
5
8
  import type { ProgressService } from '../../../lib/progressService'
6
9
  import {
7
10
  hasReadableProgressScope,
@@ -17,6 +20,23 @@ const routeMetadata = {
17
20
 
18
21
  export const metadata = routeMetadata
19
22
 
23
+ async function canMutateProgressJob(
24
+ container: AwilixContainer,
25
+ auth: NonNullable<AuthContext>,
26
+ job: Pick<ProgressJob, 'startedByUserId' | 'organizationId'>,
27
+ ): Promise<boolean> {
28
+ if (job.startedByUserId === auth.sub) return true
29
+ const rbacService = container.resolve('rbacService') as RbacService
30
+ return rbacService.userHasAllFeatures(auth.sub, ['progress.manage'], {
31
+ tenantId: auth.tenantId ?? null,
32
+ organizationId: job.organizationId ?? null,
33
+ })
34
+ }
35
+
36
+ function cannotCancelJobResponse() {
37
+ return NextResponse.json({ error: 'Cannot cancel this job' }, { status: 400 })
38
+ }
39
+
20
40
  export async function GET(req: Request, { params }: { params: { id: string } }) {
21
41
  const auth = await getAuthFromRequest(req)
22
42
  if (!auth || !auth.tenantId) {
@@ -93,7 +113,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
93
113
  userId: auth.sub,
94
114
  }
95
115
  const existing = await progressService.getJob(params.id, progressContext)
96
- if (!existing) {
116
+ if (!existing || !(await canMutateProgressJob(container, auth, existing))) {
97
117
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
98
118
  }
99
119
 
@@ -120,16 +140,15 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
120
140
  organizationIds: scope.filterIds,
121
141
  userId: auth.sub,
122
142
  }
123
- // No existence pre-check here on purpose. `cancelJob` already loads the job
124
- // through the SAME scoped filter and throws when it resolves to nothing, so a
125
- // job outside the caller's organization scope is rejected exactly like one that
126
- // does not exist — and the route keeps its documented answer for an unknown id
127
- // (400 "Cannot cancel this job", TC-PROG-009) instead of a 404.
128
143
  try {
144
+ const existing = await progressService.getJob(params.id, progressContext)
145
+ if (!existing || !(await canMutateProgressJob(container, auth, existing))) {
146
+ return cannotCancelJobResponse()
147
+ }
129
148
  await progressService.cancelJob(params.id, progressContext)
130
149
  return NextResponse.json({ ok: true })
131
150
  } catch {
132
- return NextResponse.json({ error: 'Cannot cancel this job' }, { status: 400 })
151
+ return cannotCancelJobResponse()
133
152
  }
134
153
  }
135
154
 
@@ -145,7 +164,7 @@ export const openApi = {
145
164
  },
146
165
  PUT: {
147
166
  summary: 'Update progress job',
148
- description: 'Updates progress metrics and heartbeat for a running job.',
167
+ description: 'Updates progress metrics and heartbeat for a running job. Callers can update jobs they started; updating another user\'s job requires progress.manage.',
149
168
  tags: ['Progress'],
150
169
  responses: {
151
170
  200: { description: 'Progress updated' },
@@ -155,7 +174,7 @@ export const openApi = {
155
174
  },
156
175
  DELETE: {
157
176
  summary: 'Cancel progress job',
158
- description: 'Requests cancellation of a running or pending job.',
177
+ description: 'Requests cancellation of a running or pending job. Callers can cancel jobs they started; cancelling another user\'s job requires progress.manage.',
159
178
  tags: ['Progress'],
160
179
  responses: {
161
180
  200: { description: 'Cancellation requested' },