@open-mercato/core 0.10.1-develop.7787.1.96c7cfc66f → 0.10.1-develop.7792.1.b0ce6289cf

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 6212 entry points
1
+ [build:core] found 6214 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -3,7 +3,8 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
3
  import { resolveSingleOrganizationIdOrDeny } from "@open-mercato/shared/lib/auth/organizationScope";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
- import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
+ import { isCrudHttpError, translateCrudErrorBody } from "@open-mercato/shared/lib/crud/errors";
7
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
7
8
  import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
8
9
  import { z } from "zod";
9
10
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -150,7 +151,8 @@ async function POST(req) {
150
151
  } catch (err) {
151
152
  if (isCrudHttpError(err)) {
152
153
  logger.warn("Redo rejected", { err });
153
- return NextResponse.json(err.body, { status: err.status });
154
+ const { translate } = await resolveTranslations();
155
+ return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status });
154
156
  }
155
157
  const interceptorRejection = getCommandInterceptorHttpRejection(err);
156
158
  if (interceptorRejection) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/audit_logs/api/audit-logs/actions/redo/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : singleOrganizationId\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n const logActorSubject = resolveActionLogActorSubject(log)\n const isLegacyApiKeyOwner = log.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && log.contextJson?.actorSubject === undefined\n if (log.actorUserId && !logActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && !actionLogBelongsToAuth(log, auth) && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant\n ? (logActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.redo_self',\n tenantFeature: 'audit_logs.redo_tenant',\n unavailableMessage: 'Redo target not available',\n })\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry, replaySourceFinalized } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n if (replaySourceFinalized !== true) await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n // Same contract as undo: a domain refusal carries an already-localized body the\n // operator can act on, so surface it instead of a generic message.\n if (isCrudHttpError(err)) {\n logger.warn('Redo rejected', { err })\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Redo failed', { err })\n return NextResponse.json({ error: 'Redo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA4C;AACrD,SAAS,yCAAyC;AAClD,SAAS,8BAA8B;AACvC,SAAS,4BAA4B,0CAA0C;AAG/E,SAAS,uBAAuB;AAGhC,SAAS,kCAAkC;AAG3C,SAAS,SAAS;AAElB,SAAS,oBAAoB;AAC7B,SAAS,0CAA0C;AACnD,SAAS,6CAA6C;AACtD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,SAAS,aAAa,YAAY,EAAE,MAAM,EAAE,WAAW,OAAO,CAAC;AAE9D,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACvE;AAMA,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACjC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,gDAAgD;AACpF,CAAC;AAED,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,oDAAoD;AAAA,EAC1F,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,kDAAkD;AAC9F,CAAC;AAED,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9E,QAAM,cAAc,4BAA4B,IAAI;AACpD,MAAI,CAAC,YAAa,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAErF,QAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAC/C,QAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,KAAK,MAAM,KAAK,IAAI;AACpE,MAAI,CAAC,MAAO,QAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAEjF,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,aAAc,UAAU,QAAQ,YAAY;AAClD,QAAM,OAAQ,UAAU,QAAQ,kBAAkB;AAClD,MAAI,OAA2B;AAC/B,MAAI;AACF,WAAQ,UAAU,QAAQ,aAAa;AAAA,EACzC,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,QAAM,EAAE,gBAAgB,MAAM,IAAI,MAAM,2BAA2B,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACpG,MAAI;AACJ,MAAI;AACF,2BAAuB,kBAAkB,kCAAkC,OAAO,IAAI;AAAA,EACxF,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,EAAG,QAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AACnF,UAAM;AAAA,EACR;AAEA,QAAM,gBAAgB,OAClB,MAAM,KAAK,mBAAmB,KAAK,KAAK,CAAC,wBAAwB,GAAG;AAAA,IAClE,UAAU,KAAK,YAAY;AAAA,IAC3B;AAAA,EACF,CAAC,IACD;AAEJ,QAAM,cAAc,gBAAgB,kBAAkB,OAAO;AAC7D,QAAM,MAAM,MAAM,KAAK,SAAS,KAAK;AAErC,MAAI,CAAC,OAAO,IAAI,mBAAmB,UAAU;AAC3C,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AACA,QAAM,kBAAkB,6BAA6B,GAAG;AACxD,QAAM,sBAAsB,IAAI,gBAAgB,YAAY,aACvD,YAAY,SAAS,aACrB,IAAI,aAAa,iBAAiB;AACvC,MAAI,IAAI,eAAe,CAAC,mBAAmB,CAAC,qBAAqB;AAC/D,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AACA,MAAI,IAAI,eAAe,CAAC,uBAAuB,KAAK,IAAI,KAAK,CAAC,eAAe;AAC3E,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AAMA,MAAI,IAAI,YAAY,IAAI,cAAc,KAAK,YAAY,OAAO;AAC5D,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AAKA,QAAM,mBAAmB,gBACrB,QAAQ,IAAI,kBAAkB,eAAe,IAAI,mBAAmB,WAAW,IAC/E,QAAQ,IAAI,kBAAkB,IAAI,mBAAmB,WAAW;AACpE,MAAI,kBAAkB;AACpB,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AAEA,QAAM,gBAAgB,gBACjB,oBAAoB,sBAAsB,YAAY,UAAU,KAAK,OACtE,YAAY;AAChB,QAAM,6BAA6B,IAAI,kBAAkB;AACzD,QAAM,eAAe,MAAM,KAAK,qBAAqB,eAAe;AAAA,IAClE,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB;AAAA,EAClB,CAAC;AACD,MAAI,CAAC,gBAAgB,aAAa,OAAO,IAAI,IAAI;AAC/C,WAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AACA,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC3F,QAAM,aAAa;AAEnB,MAAI;AACF,UAAM,MAAM,MAAM,qBAAqB,WAAW,MAAM,GAAG;AAC3D,QAAI,yBAAyB,CAAC,EAAE,UAAAA,WAAU,gBAAgB,MACxD,sCAAsC,iBAAiB,YAAYA,WAAU;AAAA,MAC3E;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb,eAAe;AAAA,MACf,oBAAoB;AAAA,IACtB,CAAC;AACH,UAAM,gBAAgB,IAAI,eAAe,OAAO,IAAI,gBAAgB,WAAY,IAAI,cAA0C;AAC9H,UAAM,kBAAkB,MAAM,QAAQ,eAAe,YAAyB,IACzE,cAAe,eAChB,CAAC;AACL,UAAM,eAAe,gBAClB,OAAO,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,CAAC,EACvF,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC;AAC9B,UAAM,kBAA2C;AAAA,MAC/C,eAAe;AAAA,MACf,aAAa,IAAI;AAAA,MACjB,iBAAiB,IAAI;AAAA,IACvB;AACA,QAAI,aAAa,OAAQ,iBAAgB,eAAe;AACxD,UAAMC,YAA+B;AAAA,MACnC,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,aAAa,KAAK;AAAA,MAClB,aAAa,IAAI;AAAA,MACjB,cAAc,IAAI;AAAA,MAClB,YAAY,IAAI;AAAA,MAChB,SAAS;AAAA,IACX;AACA,UAAM,gBAAgB,iBAAiB,IAAI,gBAAgB,GAAG;AAC9D,QAAI,CAAC,eAAe;AAClB,aAAO,aAAa,KAAK,EAAE,OAAO,wCAAwC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9F;AACA,UAAM,eAAe;AACrB,UAAM,EAAE,UAAU,sBAAsB,IAAI,MAAM,WAAW,QAAQ,IAAI,WAAW;AAAA,MAClF,OAAO;AAAA,MACP;AAAA,MACA,UAAAA;AAAA,MACA,cAAc;AAAA,IAChB,CAAC;AACD,QAAI,0BAA0B,KAAM,OAAM,KAAK,WAAW,IAAI,EAAE;AAChE,UAAM,YAAY,YAAY,QAAQ;AACtC,UAAM,WAAW,aAAa,KAAK;AAAA,MACjC,IAAI;AAAA,MACJ,OAAO,WAAW,MAAM;AAAA,MACxB,WAAW,WAAW,aAAa;AAAA,IACrC,CAAC;AACD,QAAI,WAAW,aAAa,UAAU,IAAI;AACxC,YAAM,YAAY,UAAU,qBAAqB,OAC7C,UAAU,UAAU,YAAY,IAC/B,OAAO,UAAU,cAAc,WAAW,UAAU,aAAY,oBAAI,KAAK,GAAE,YAAY;AAC5F,eAAS,QAAQ,IAAI,kBAAkB,2BAA2B;AAAA,QAChE,IAAI,UAAU;AAAA,QACd,WAAW,UAAU;AAAA,QACrB,WAAW,UAAU,aAAa,IAAI;AAAA,QACtC,aAAa,UAAU,eAAe,IAAI,eAAe;AAAA,QACzD,cAAc,OAAO,UAAU,iBAAiB,WAAW,UAAU,eAAe,IAAI,gBAAgB;AAAA,QACxG,YAAY,OAAO,UAAU,eAAe,WAAW,UAAU,aAAa,IAAI,cAAc;AAAA,QAChG,YAAY;AAAA,MACd,CAAC,CAAC;AAAA,IACJ;AACA,WAAO;AAAA,EACT,SAAS,KAAK;AAGZ,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,KAAK,iBAAiB,EAAE,IAAI,CAAC;AACpC,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,uBAAuB,mCAAmC,GAAG;AACnE,QAAI,sBAAsB;AACxB,aAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AAAA,IAC7F;AACA,WAAO,MAAM,eAAe,EAAE,IAAI,CAAC;AACnC,WAAO,aAAa,KAAK,EAAE,OAAO,cAAc,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpE;AACF;AAEA,eAAe,qBAAqB,WAA4B,MAAmB,SAAkD;AACnI,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,QAAQ,CAAC;AACnF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB,MAAM;AAAA,IAC9B,iBAAiB,MAAM;AAAA,IACvB;AAAA,EACF;AACF;AAEA,SAAS,YAAY,OAAkC;AACrD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,MAAI,OAAQ,MAA2B,OAAO,SAAU,QAAO;AAC/D,SAAO;AACT;AAEA,SAAS,iBAAiB,SAAkB,KAAgC;AAC1E,MAAI,WAAW,OAAO,YAAY,YAAY,CAAC,MAAM,QAAQ,OAAO,KAAK,iBAAiB,SAAS;AACjG,UAAM,WAAW;AACjB,WAAO,SAAS,eAAe,CAAC;AAAA,EAClC;AACA,QAAM,iBAAiB,kBAAkB,GAAG;AAC5C,MAAI,eAAgB,QAAO;AAC3B,SAAO;AACT;AAEA,SAAS,kBAAkB,KAAgD;AACzE,MAAI,CAAC,IAAI,UAAU,SAAS,SAAS,EAAG,QAAO;AAC/C,MAAI,CAAC,IAAI,WAAY,QAAO;AAC5B,QAAM,UAAU,IAAI;AACpB,MAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,EAAG,QAAO,EAAE,IAAI,IAAI,WAAW;AACnG,QAAM,UAAmC,EAAE,IAAI,IAAI,WAAW;AAC9D,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,QAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,KAAK,QAAQ,OAAO;AAChF,cAAQ,GAAG,IAAK,MAAkC;AAAA,IACpD,OAAO;AACL,cAAQ,GAAG,IAAI;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aACE;AAAA,MACF,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,mBAAmB;AAAA,MACvF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,6BAA6B,QAAQ,YAAY;AAAA,QAC7E,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,YAAY;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,YAAY;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : singleOrganizationId\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n const logActorSubject = resolveActionLogActorSubject(log)\n const isLegacyApiKeyOwner = log.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && log.contextJson?.actorSubject === undefined\n if (log.actorUserId && !logActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && !actionLogBelongsToAuth(log, auth) && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant\n ? (logActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.redo_self',\n tenantFeature: 'audit_logs.redo_tenant',\n unavailableMessage: 'Redo target not available',\n })\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry, replaySourceFinalized } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n if (replaySourceFinalized !== true) await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n logger.warn('Redo rejected', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Redo failed', { err })\n return NextResponse.json({ error: 'Redo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["logEntry", "metadata"]
7
7
  }
@@ -3,7 +3,7 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
3
  import { resolveSingleOrganizationIdOrDeny } from "@open-mercato/shared/lib/auth/organizationScope";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
- import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
+ import { isCrudHttpError, translateCrudErrorBody } from "@open-mercato/shared/lib/crud/errors";
7
7
  import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
8
8
  import { z } from "zod";
9
9
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -114,7 +114,8 @@ async function POST(req) {
114
114
  } catch (err) {
115
115
  if (isCrudHttpError(err)) {
116
116
  logger.warn("Undo rejected", { err, logId: target.id });
117
- return NextResponse.json(err.body, { status: err.status });
117
+ const { translate: translate2 } = await resolveTranslations();
118
+ return NextResponse.json(translateCrudErrorBody(err.body, translate2), { status: err.status });
118
119
  }
119
120
  const interceptorRejection = getCommandInterceptorHttpRejection(err);
120
121
  if (interceptorRejection) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/audit_logs/api/audit-logs/actions/undo/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { AwilixContainer } from 'awilix'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'undo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.undo_self'] },\n}\n\ntype UndoRequestBody = {\n undoToken?: string\n}\n\nconst undoRequestSchema = z.object({\n undoToken: z.string().min(1).describe('Undo token issued by the action log entry'),\n})\n\nconst undoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().describe('Identifier of the action log that was undone'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as UndoRequestBody | null\n const undoToken = body?.undoToken?.trim()\n if (!undoToken) return NextResponse.json({ error: 'Invalid undo token' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canUndoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.undo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const target = await logs.findByUndoToken(undoToken)\n if (!target || target.executionState !== 'done') {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n const targetActorSubject = resolveActionLogActorSubject(target)\n const isLegacyApiKeyOwner = target.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && target.contextJson?.actorSubject === undefined\n if (target.actorUserId && !targetActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n if (target.actorUserId && !actionLogBelongsToAuth(target, auth) && !canUndoTenant) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.undo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never undo a tenant-scoped row (issue #2685).\n if (target.tenantId && target.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n const scopedOrgId = canUndoTenant ? organizationId ?? null : singleOrganizationId\n // Tenant-level undoers may undo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685).\n const orgScopeMismatch = canUndoTenant\n ? Boolean(target.organizationId && scopedOrgId && target.organizationId !== scopedOrgId)\n : Boolean(target.organizationId && target.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n\n const lookupActorId = canUndoTenant\n ? (targetActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n // Scope the latest-undoable re-lookup to the target row's own organization, not\n // the caller's currently-resolved org. The actor/tenant/org guards above already\n // authorized the caller for this row; reusing the caller's scope here breaks undo\n // for tenant-level rows (organization create/update/delete/reparent log with a\n // null organization_id) whenever the caller resolves to a concrete home org, so\n // the lookup never matches and returns \"Undo token not available\" (issue #2398).\n const lookupOrgId = target.organizationId ?? null\n let latest = null\n if (target.resourceKind || target.resourceId) {\n latest = await logs.latestUndoableForResource({\n actorUserId: lookupActorId,\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n resourceKind: target.resourceKind ?? undefined,\n resourceId: target.resourceId ?? undefined,\n })\n }\n if (!latest) {\n latest = await logs.latestUndoableForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n })\n }\n if (!latest || latest.id !== target.id) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.undo_self',\n tenantFeature: 'audit_logs.undo_tenant',\n unavailableMessage: 'Undo token not available',\n })\n await commandBus.undo(undoToken, ctx)\n return NextResponse.json({ ok: true, logId: target.id })\n } catch (err) {\n // A command can refuse an undo for a domain reason the operator can act on \u2014 e.g.\n // reverting a role grant would drop a tenant below a protected role's active-holder\n // floor. Those arrive as CrudHttpError with an already-localized body, so pass them\n // through verbatim instead of flattening every failure to a generic message.\n if (isCrudHttpError(err)) {\n logger.warn('Undo rejected', { err, logId: target.id })\n return NextResponse.json(err.body, { status: err.status })\n }\n // A beforeUndo interceptor that blocked with an explicit status is a deliberate business\n // rejection, not an undo failure \u2014 surface its status and message (issue #5045).\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Undo failed', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('audit_logs.error.undo_failed', 'Undo failed') }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Undo a recent action',\n description: 'Executes the undo operation for the most recent undoable action belonging to the caller.',\n methods: {\n POST: {\n summary: 'Undo action by token',\n description:\n 'Replays the undo handler registered for a command. The provided undo token must match the latest undoable log entry accessible to the caller.',\n requestBody: {\n contentType: 'application/json',\n schema: undoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Undo applied successfully', schema: undoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid or unavailable undo token', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Undo blocked by organization or tenant scope', schema: errorSchema },\n {\n status: 422,\n description:\n 'Undo deliberately blocked by a beforeUndo command interceptor. The interceptor chooses the status (any 4xx/5xx) and may replace the body.',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { AwilixContainer } from 'awilix'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'undo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.undo_self'] },\n}\n\ntype UndoRequestBody = {\n undoToken?: string\n}\n\nconst undoRequestSchema = z.object({\n undoToken: z.string().min(1).describe('Undo token issued by the action log entry'),\n})\n\nconst undoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().describe('Identifier of the action log that was undone'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as UndoRequestBody | null\n const undoToken = body?.undoToken?.trim()\n if (!undoToken) return NextResponse.json({ error: 'Invalid undo token' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canUndoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.undo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const target = await logs.findByUndoToken(undoToken)\n if (!target || target.executionState !== 'done') {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n const targetActorSubject = resolveActionLogActorSubject(target)\n const isLegacyApiKeyOwner = target.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && target.contextJson?.actorSubject === undefined\n if (target.actorUserId && !targetActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n if (target.actorUserId && !actionLogBelongsToAuth(target, auth) && !canUndoTenant) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.undo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never undo a tenant-scoped row (issue #2685).\n if (target.tenantId && target.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n const scopedOrgId = canUndoTenant ? organizationId ?? null : singleOrganizationId\n // Tenant-level undoers may undo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685).\n const orgScopeMismatch = canUndoTenant\n ? Boolean(target.organizationId && scopedOrgId && target.organizationId !== scopedOrgId)\n : Boolean(target.organizationId && target.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n\n const lookupActorId = canUndoTenant\n ? (targetActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n // Scope the latest-undoable re-lookup to the target row's own organization, not\n // the caller's currently-resolved org. The actor/tenant/org guards above already\n // authorized the caller for this row; reusing the caller's scope here breaks undo\n // for tenant-level rows (organization create/update/delete/reparent log with a\n // null organization_id) whenever the caller resolves to a concrete home org, so\n // the lookup never matches and returns \"Undo token not available\" (issue #2398).\n const lookupOrgId = target.organizationId ?? null\n let latest = null\n if (target.resourceKind || target.resourceId) {\n latest = await logs.latestUndoableForResource({\n actorUserId: lookupActorId,\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n resourceKind: target.resourceKind ?? undefined,\n resourceId: target.resourceId ?? undefined,\n })\n }\n if (!latest) {\n latest = await logs.latestUndoableForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n })\n }\n if (!latest || latest.id !== target.id) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.undo_self',\n tenantFeature: 'audit_logs.undo_tenant',\n unavailableMessage: 'Undo token not available',\n })\n await commandBus.undo(undoToken, ctx)\n return NextResponse.json({ ok: true, logId: target.id })\n } catch (err) {\n if (isCrudHttpError(err)) {\n logger.warn('Undo rejected', { err, logId: target.id })\n const { translate } = await resolveTranslations()\n return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n }\n // A beforeUndo interceptor that blocked with an explicit status is a deliberate business\n // rejection, not an undo failure \u2014 surface its status and message (issue #5045).\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Undo failed', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('audit_logs.error.undo_failed', 'Undo failed') }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Undo a recent action',\n description: 'Executes the undo operation for the most recent undoable action belonging to the caller.',\n methods: {\n POST: {\n summary: 'Undo action by token',\n description:\n 'Replays the undo handler registered for a command. The provided undo token must match the latest undoable log entry accessible to the caller.',\n requestBody: {\n contentType: 'application/json',\n schema: undoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Undo applied successfully', schema: undoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid or unavailable undo token', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Undo blocked by organization or tenant scope', schema: errorSchema },\n {\n status: 422,\n description:\n 'Undo deliberately blocked by a beforeUndo command interceptor. The interceptor chooses the status (any 4xx/5xx) and may replace the body.',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["translate"]
7
7
  }
@@ -289,6 +289,10 @@ const seedEncryptionMaps = {
289
289
  function normalizeKeyInput(value) {
290
290
  return value.trim().replace(/(?:^['"]|['"]$)/g, "");
291
291
  }
292
+ const OLD_ENCRYPTION_KEY_ENV = "TENANT_DATA_ENCRYPTION_OLD_KEY";
293
+ function failCli(message) {
294
+ throw new Error(message);
295
+ }
292
296
  class DerivedKeyKmsService {
293
297
  constructor(secret) {
294
298
  this.root = crypto.createHash("sha256").update(normalizeKeyInput(secret)).digest();
@@ -310,10 +314,6 @@ class DerivedKeyKmsService {
310
314
  return this.getTenantDek(tenantId);
311
315
  }
312
316
  }
313
- function fingerprintDek(dek) {
314
- if (!dek?.key) return null;
315
- return crypto.createHash("sha256").update(dek.key).digest("hex").slice(0, 12);
316
- }
317
317
  function decryptWithOldKey(payload, dek) {
318
318
  if (!dek?.key) return null;
319
319
  return decryptWithAesGcm(payload, dek.key);
@@ -342,19 +342,61 @@ function buildEntityMetaRegistry(em) {
342
342
  }
343
343
  return metaByEntityId;
344
344
  }
345
- function resolveMapMeta(map, metaByEntityId, warn = () => {
346
- }) {
345
+ function classifyEncryptionMap(map, metaByEntityId) {
347
346
  const entityId = String(map.entityId);
347
+ const fields = Array.isArray(map.fieldsJson) ? map.fieldsJson : [];
348
+ if (!fields.length) {
349
+ return { status: "nothing-to-process", reason: "the map declares no encrypted fields" };
350
+ }
351
+ const tenantId = map.tenantId ? String(map.tenantId) : null;
352
+ if (!tenantId) {
353
+ return {
354
+ status: "nothing-to-process",
355
+ reason: 'the map has no tenant_id \u2014 use "mercato entities backfill-system-encryption" for system-scoped records'
356
+ };
357
+ }
348
358
  const meta = metaByEntityId.get(entityId);
349
359
  if (!meta) {
350
- warn(`Skipping ${entityId}: metadata not found.`);
351
- return null;
360
+ return {
361
+ status: "unresolvable",
362
+ reason: "entity metadata is not registered in this app \u2014 is the module that owns it still enabled?"
363
+ };
352
364
  }
353
- const fields = Array.isArray(map.fieldsJson) ? map.fieldsJson : [];
354
- if (!fields.length) return null;
355
- const tenantId = map.tenantId ? String(map.tenantId) : null;
356
- if (!tenantId) return null;
357
- return { entityId, meta, fields, tenantId };
365
+ if (!meta.tableName) {
366
+ return { status: "unresolvable", reason: "table metadata is missing" };
367
+ }
368
+ for (const rule of fields) {
369
+ if (!resolveProperty(meta, rule.field).columnName) {
370
+ return { status: "unresolvable", reason: `mapped field "${rule.field}" was not found` };
371
+ }
372
+ if (rule.hashField && !resolveProperty(meta, rule.hashField).columnName) {
373
+ return { status: "unresolvable", reason: `hash field "${rule.hashField}" was not found` };
374
+ }
375
+ }
376
+ return { status: "ready", mapMeta: { entityId, meta, fields, tenantId } };
377
+ }
378
+ function resolveMapsOrFail(maps, metaByEntityId) {
379
+ const resolved = [];
380
+ const unresolvable = [];
381
+ for (const map of maps) {
382
+ const resolution = classifyEncryptionMap(map, metaByEntityId);
383
+ if (resolution.status === "ready") {
384
+ resolved.push({ map, mapMeta: resolution.mapMeta });
385
+ continue;
386
+ }
387
+ if (resolution.status === "nothing-to-process") {
388
+ console.warn(`Skipping ${String(map.entityId)}: ${resolution.reason}.`);
389
+ continue;
390
+ }
391
+ unresolvable.push(` ${String(map.entityId)} (encryption_maps.id=${String(map.id)}): ${resolution.reason}`);
392
+ }
393
+ if (unresolvable.length) {
394
+ failCli(
395
+ `${unresolvable.length} encryption map(s) in the selected scope cannot be processed, so this command would have left their ciphertext untouched while reporting success. No rows were changed.
396
+ ` + unresolvable.join("\n") + "\nResolve each one before re-running: re-enable the module that owns the entity, or \u2014 once the entity's data is confirmed gone \u2014 soft-delete the stale map row (UPDATE encryption_maps SET deleted_at = now() WHERE id = '\u2026')."
397
+ );
398
+ }
399
+ return resolved;
358
400
  }
359
401
  function formatValueForColumn(prop, value) {
360
402
  if (value === null || value === void 0) return value;
@@ -370,31 +412,38 @@ const rotateEncryptionKey = {
370
412
  const args = parseArgs(rest);
371
413
  const tenantIdArg = args.tenant || args.tenantId || null;
372
414
  const organizationIdArg = args.org || args.organization || args.organizationId || null;
373
- const oldKey = args["old-key"] || args.oldKey || null;
415
+ const oldKeyArgument = args["old-key"] || args.oldKey || null;
416
+ const oldKeyEnvironment = process.env[OLD_ENCRYPTION_KEY_ENV] ? normalizeKeyInput(process.env[OLD_ENCRYPTION_KEY_ENV]) : null;
417
+ if (oldKeyArgument && oldKeyEnvironment && normalizeKeyInput(oldKeyArgument) !== oldKeyEnvironment) {
418
+ failCli(`--old-key and ${OLD_ENCRYPTION_KEY_ENV} contain different values. Remove --old-key and retry.`);
419
+ }
420
+ if (oldKeyArgument) {
421
+ console.warn(
422
+ `Passing key material through --old-key can expose it in shell history and process listings. Set ${OLD_ENCRYPTION_KEY_ENV} through your secret manager instead.`
423
+ );
424
+ }
425
+ const oldKey = oldKeyEnvironment || oldKeyArgument;
374
426
  const dryRun = Boolean(args["dry-run"] || args.dry);
375
427
  const debug = Boolean(args.debug);
376
428
  const rotate = Boolean(oldKey);
377
429
  if (rotate && !tenantIdArg) {
378
430
  console.warn(
379
- "\u26A0\uFE0F Rotating with --old-key across all tenants. A single old key should normally target one tenant; consider --tenant."
431
+ "\u26A0\uFE0F Rotating with one previous key across all tenants. A single previous key should normally target one tenant; consider --tenant."
380
432
  );
381
433
  }
382
434
  if (!isTenantDataEncryptionEnabled()) {
383
- console.error("TENANT_DATA_ENCRYPTION is disabled; aborting.");
384
- return;
435
+ failCli("TENANT_DATA_ENCRYPTION is disabled; aborting.");
385
436
  }
386
437
  const { resolve } = await createRequestContainer();
387
438
  const em = resolve("em");
388
439
  const conn = em?.getConnection?.();
389
440
  if (!conn || typeof conn.execute !== "function") {
390
- console.error("Unable to access raw connection; aborting.");
391
- return;
441
+ failCli("Unable to access raw connection; aborting.");
392
442
  }
393
443
  const encryptionService = new TenantDataEncryptionService(em, { kms: createKmsService() });
394
444
  const oldKms = rotate && oldKey ? new DerivedKeyKmsService(oldKey) : null;
395
445
  if (!encryptionService.isEnabled()) {
396
- console.error("Encryption service is not enabled (KMS unhealthy or no DEK). Aborting.");
397
- return;
446
+ failCli("Encryption service is not enabled (KMS unhealthy or no DEK). Aborting.");
398
447
  }
399
448
  if (debug) {
400
449
  console.log("[rotate-encryption-key]", {
@@ -403,18 +452,6 @@ const rotateEncryptionKey = {
403
452
  tenantId: tenantIdArg ?? null,
404
453
  organizationId: organizationIdArg ?? null
405
454
  });
406
- if (tenantIdArg) {
407
- const [oldDek, newDek] = await Promise.all([
408
- oldKms?.getTenantDek(tenantIdArg) ?? Promise.resolve(null),
409
- encryptionService.getDek(tenantIdArg)
410
- ]);
411
- console.log("[rotate-encryption-key] dek fingerprints", {
412
- oldKey: fingerprintDek(oldDek),
413
- currentKey: fingerprintDek(newDek)
414
- });
415
- } else {
416
- console.log("[rotate-encryption-key] dek fingerprints skipped (no tenantId)");
417
- }
418
455
  }
419
456
  const metaByEntityId = buildEntityMetaRegistry(em);
420
457
  const where = { deletedAt: null };
@@ -429,6 +466,7 @@ const rotateEncryptionKey = {
429
466
  );
430
467
  return false;
431
468
  });
469
+ const resolvedMaps = resolveMapsOrFail(maps, metaByEntityId);
432
470
  const customDocuments = await rotateCustomEntityDocuments(em, {
433
471
  encryptionService,
434
472
  oldKms,
@@ -532,7 +570,7 @@ const rotateEncryptionKey = {
532
570
  if (rule.hashField) delete payload[rule.hashField];
533
571
  unrecoverableRows.push({ entityId, field: rule.field, rowId: row[pk] });
534
572
  console.warn(
535
- `Skipping ${entityId}.${rule.field} for row ${row[pk]}: its ciphertext opens under neither --old-key nor the current tenant key. Re-run with the key that sealed it.`
573
+ `Skipping ${entityId}.${rule.field} for row ${row[pk]}: its ciphertext opens under neither the previous nor the current tenant key. Re-run with the key that sealed it.`
536
574
  );
537
575
  }
538
576
  }
@@ -586,9 +624,7 @@ const rotateEncryptionKey = {
586
624
  return updated;
587
625
  };
588
626
  let total = 0;
589
- for (const map of maps) {
590
- const mapMeta = resolveMapMeta(map, metaByEntityId, console.warn);
591
- if (!mapMeta) continue;
627
+ for (const { map, mapMeta } of resolvedMaps) {
592
628
  const { entityId, meta, fields, tenantId } = mapMeta;
593
629
  const scopes = await resolveScopes(tenantId, map.organizationId ? String(map.organizationId) : null);
594
630
  for (const scope of scopes) {
@@ -610,7 +646,7 @@ const rotateEncryptionKey = {
610
646
  const maxListed = 20;
611
647
  console.log(
612
648
  `
613
- \u26A0\uFE0F ${unrecoverableRows.length} field(s) could not be rotated \u2014 their ciphertext opens under neither --old-key nor the current tenant key, so they were left untouched. See "Key management" in apps/docs/docs/architecture/data-encryption.mdx for how to resolve these manually.`
649
+ \u26A0\uFE0F ${unrecoverableRows.length} field(s) could not be rotated \u2014 their ciphertext opens under neither the previous nor the current tenant key, so they were left untouched. See "Key management" in apps/docs/docs/architecture/data-encryption.mdx for how to resolve these manually.`
614
650
  );
615
651
  for (const row of unrecoverableRows.slice(0, maxListed)) {
616
652
  console.log(` - ${row.entityId}.${row.field} row=${String(row.rowId)}`);
@@ -618,6 +654,9 @@ const rotateEncryptionKey = {
618
654
  if (unrecoverableRows.length > maxListed) {
619
655
  console.log(` ... and ${unrecoverableRows.length - maxListed} more`);
620
656
  }
657
+ failCli(
658
+ `${unrecoverableRows.length} encrypted field(s) could not be rotated. The command left them untouched and did not complete successfully.`
659
+ );
621
660
  }
622
661
  }
623
662
  };
@@ -636,29 +675,24 @@ const decryptDatabase = {
636
675
  const sleepMs = Math.max(0, parseInt(String(args["sleep-ms"] || args.sleepMs || "0"), 10) || 0);
637
676
  const debug = Boolean(args.debug);
638
677
  if (!tenantIdArg) {
639
- console.error("--tenant <uuid> is required.");
640
- return;
678
+ failCli("--tenant <uuid> is required.");
641
679
  }
642
680
  if (!checkMode) {
643
681
  if (!confirm) {
644
- console.error("--confirm <tenantUuid> is required (safety gate). Pass the exact tenant UUID to confirm the operation.");
645
- return;
682
+ failCli("--confirm <tenantUuid> is required (safety gate). Pass the exact tenant UUID to confirm the operation.");
646
683
  }
647
684
  if (confirm !== tenantIdArg) {
648
- console.error(`--confirm value "${confirm}" does not match --tenant "${tenantIdArg}". Aborting.`);
649
- return;
685
+ failCli(`--confirm value "${confirm}" does not match --tenant "${tenantIdArg}". Aborting.`);
650
686
  }
651
687
  }
652
688
  const { resolve } = await createRequestContainer();
653
689
  const em = resolve("em");
654
690
  const conn = em?.getConnection?.();
655
691
  if (!conn || typeof conn.execute !== "function") {
656
- console.error("Unable to access raw database connection; aborting.");
657
- return;
692
+ failCli("Unable to access raw database connection; aborting.");
658
693
  }
659
694
  if (!checkMode && !isTenantDataEncryptionEnabled()) {
660
- console.error("TENANT_DATA_ENCRYPTION is disabled; aborting. Data may already be decrypted.");
661
- return;
695
+ failCli("TENANT_DATA_ENCRYPTION is disabled; aborting. Data may already be decrypted.");
662
696
  }
663
697
  const metaByEntityId = buildEntityMetaRegistry(em);
664
698
  const resolveDecryptScopes = async (tenantId, organizationId) => {
@@ -690,6 +724,7 @@ const decryptDatabase = {
690
724
  console.log("No active encryption maps found for the selected scope.");
691
725
  return;
692
726
  }
727
+ const resolvedMaps = resolveMapsOrFail(maps, metaByEntityId);
693
728
  const kms = createKmsService();
694
729
  const dekCache = /* @__PURE__ */ new Map();
695
730
  const getDek = async (tenantId) => {
@@ -704,16 +739,19 @@ const decryptDatabase = {
704
739
  console.log(`Active EncryptionMap records for scope: ${maps.length}`);
705
740
  let encryptedCandidatesSampled = 0;
706
741
  let malformedPayloadCountSampled = 0;
707
- for (const map of maps) {
708
- const mapMeta = resolveMapMeta(map, metaByEntityId);
709
- if (!mapMeta) continue;
742
+ const uninspectableEntityIds = [];
743
+ const failedSampleScopes = [];
744
+ for (const { map, mapMeta } of resolvedMaps) {
710
745
  const { entityId, meta, fields, tenantId } = mapMeta;
711
746
  const dek = await getDek(tenantId).catch(() => null);
712
- if (!dek) continue;
747
+ if (!dek) {
748
+ console.warn(`\u26A0 No DEK available for tenant ${tenantId}; cannot inspect ${entityId}.`);
749
+ uninspectableEntityIds.push(entityId);
750
+ continue;
751
+ }
713
752
  const scopes = await resolveDecryptScopes(tenantId, map.organizationId ? String(map.organizationId) : null);
714
753
  const pk = Array.isArray(meta?.primaryKeys) && meta.primaryKeys.length ? meta.primaryKeys[0] : "id";
715
- const tableName = meta?.tableName;
716
- if (!tableName) continue;
754
+ const tableName = meta.tableName;
717
755
  const schema = meta?.schema;
718
756
  const qualifiedTable = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
719
757
  const fieldCols = fields.flatMap((f) => {
@@ -722,10 +760,19 @@ const decryptDatabase = {
722
760
  });
723
761
  const colList = Array.from(/* @__PURE__ */ new Set([pk, ...fieldCols]));
724
762
  for (const scope of scopes) {
725
- const sampleRows = await conn.execute(
726
- `SELECT ${colList.map((c) => `"${c}"`).join(", ")} FROM ${qualifiedTable} WHERE tenant_id = ? AND organization_id IS NOT DISTINCT FROM ? LIMIT 100`,
727
- [scope.tenantId, scope.organizationId]
728
- ).catch(() => []);
763
+ let sampleRows;
764
+ try {
765
+ sampleRows = await conn.execute(
766
+ `SELECT ${colList.map((c) => `"${c}"`).join(", ")} FROM ${qualifiedTable} WHERE tenant_id = ? AND organization_id IS NOT DISTINCT FROM ? LIMIT 100`,
767
+ [scope.tenantId, scope.organizationId]
768
+ );
769
+ } catch (error) {
770
+ const scopeLabel = scope.organizationId ?? "global";
771
+ const errorMessage = error instanceof Error ? error.message : String(error);
772
+ console.warn(`\u26A0 Could not sample ${entityId} for organization ${scopeLabel}: ${errorMessage}`);
773
+ failedSampleScopes.push(`${entityId} (organization_id=${scopeLabel})`);
774
+ continue;
775
+ }
729
776
  for (const row of Array.isArray(sampleRows) ? sampleRows : []) {
730
777
  let rowHasEncrypted = false;
731
778
  for (const fieldRule of fields) {
@@ -754,6 +801,13 @@ const decryptDatabase = {
754
801
  console.log(`malformed payloads (sampled): ${malformedPayloadCountSampled}`);
755
802
  }
756
803
  console.log("not a proof of absence \u2014 run full command + rerun --check to confirm");
804
+ if (uninspectableEntityIds.length || failedSampleScopes.length) {
805
+ const unavailableDekSummary = uninspectableEntityIds.length ? ` No DEK was available for ${uninspectableEntityIds.join(", ")}.` : "";
806
+ const failedSampleSummary = failedSampleScopes.length ? ` Sample queries failed for ${failedSampleScopes.join(", ")}.` : "";
807
+ failCli(
808
+ `Encryption check incomplete.${unavailableDekSummary}${failedSampleSummary} The counts above cover only successfully inspected scopes.`
809
+ );
810
+ }
757
811
  return;
758
812
  }
759
813
  let totalRowsFetched = 0;
@@ -763,21 +817,20 @@ const decryptDatabase = {
763
817
  let totalMalformedPayloadCount = 0;
764
818
  const malformedByLocation = /* @__PURE__ */ new Map();
765
819
  let totalEntitiesProcessed = 0;
766
- for (const map of maps) {
767
- const mapMeta = resolveMapMeta(map, metaByEntityId, console.warn);
768
- if (!mapMeta) continue;
769
- const { entityId, meta, fields, tenantId } = mapMeta;
770
- const dek = await getDek(tenantId);
820
+ const deksByTenantId = /* @__PURE__ */ new Map();
821
+ for (const { mapMeta } of resolvedMaps) {
822
+ if (deksByTenantId.has(mapMeta.tenantId)) continue;
823
+ const dek = await getDek(mapMeta.tenantId);
771
824
  if (!dek) {
772
- console.warn(`No DEK available for tenant ${tenantId}; skipping ${entityId}.`);
773
- continue;
825
+ failCli(`No DEK available for tenant ${mapMeta.tenantId}. No rows were changed.`);
774
826
  }
827
+ deksByTenantId.set(mapMeta.tenantId, dek);
828
+ }
829
+ for (const { map, mapMeta } of resolvedMaps) {
830
+ const { entityId, meta, fields, tenantId } = mapMeta;
831
+ const dek = deksByTenantId.get(tenantId);
775
832
  const pk = Array.isArray(meta?.primaryKeys) && meta.primaryKeys.length ? meta.primaryKeys[0] : "id";
776
- const tableName = meta?.tableName;
777
- if (!tableName) {
778
- console.warn(`Skipping ${entityId}: table name not found.`);
779
- continue;
780
- }
833
+ const tableName = meta.tableName;
781
834
  const schema = meta?.schema;
782
835
  const qualifiedTable = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
783
836
  const fieldCols = fields.flatMap((f) => {
@@ -900,26 +953,6 @@ const decryptDatabase = {
900
953
  totalMalformedPayloadCount += scopeMalformedCount;
901
954
  }
902
955
  }
903
- if (deactivateMaps && !dryRun) {
904
- let deactivateSql = `UPDATE encryption_maps SET is_active = false, deleted_at = now() WHERE tenant_id = ? AND deleted_at IS NULL`;
905
- const deactivateParams = [tenantIdArg];
906
- if (organizationIdArg) {
907
- deactivateSql += ` AND (organization_id = ? OR organization_id IS NULL)`;
908
- deactivateParams.push(organizationIdArg);
909
- }
910
- if (entityIdArg) {
911
- deactivateSql += ` AND entity_id = ?`;
912
- deactivateParams.push(entityIdArg);
913
- }
914
- await conn.execute(deactivateSql, deactivateParams);
915
- console.warn("\u26A0 Restart all application replicas \u2014 in-process map caches may still be active.");
916
- if (isTenantDataEncryptionEnabled()) {
917
- console.warn("\u26A0 Env TENANT_DATA_ENCRYPTION is still true \u2014 new writes will be re-encrypted until env is updated and replicas restarted.");
918
- }
919
- }
920
- if (deactivateMaps && dryRun) {
921
- console.log(`[dry-run] Would deactivate ${maps.length} EncryptionMap record(s).`);
922
- }
923
956
  const prefix = dryRun ? "[dry-run] " : "";
924
957
  console.log(`
925
958
  ${prefix}Decryption summary:`);
@@ -941,6 +974,29 @@ ${prefix}Decryption summary:`);
941
974
  console.log(` ${loc}: ${count}`);
942
975
  }
943
976
  }
977
+ failCli(
978
+ `${totalMalformedPayloadCount} field value(s) could not be decrypted because their ciphertext was malformed. Encryption maps were left active.`
979
+ );
980
+ }
981
+ if (deactivateMaps && !dryRun) {
982
+ let deactivateSql = `UPDATE encryption_maps SET is_active = false, deleted_at = now() WHERE tenant_id = ? AND deleted_at IS NULL`;
983
+ const deactivateParams = [tenantIdArg];
984
+ if (organizationIdArg) {
985
+ deactivateSql += ` AND (organization_id = ? OR organization_id IS NULL)`;
986
+ deactivateParams.push(organizationIdArg);
987
+ }
988
+ if (entityIdArg) {
989
+ deactivateSql += ` AND entity_id = ?`;
990
+ deactivateParams.push(entityIdArg);
991
+ }
992
+ await conn.execute(deactivateSql, deactivateParams);
993
+ console.warn("\u26A0 Restart all application replicas \u2014 in-process map caches may still be active.");
994
+ if (isTenantDataEncryptionEnabled()) {
995
+ console.warn("\u26A0 Env TENANT_DATA_ENCRYPTION is still true \u2014 new writes will be re-encrypted until env is updated and replicas restarted.");
996
+ }
997
+ }
998
+ if (deactivateMaps && dryRun) {
999
+ console.log(`[dry-run] Would deactivate ${maps.length} EncryptionMap record(s).`);
944
1000
  }
945
1001
  if (!dryRun) {
946
1002
  console.log(`