services/api/src/scripts/resumePendingRequirementDeletions.js
Overview
Este script es una CLI de recuperación manual para reanudar eliminaciones de Requirement que ya estaban solicitadas y quedaron en estado pending. Su flujo está pensado para operar sobre identificadores explícitos, validar el destino local autorizado y ejecutar la reanudación con confirmación HMAC de dos pasos. La propia documentación del archivo aclara que esta recuperación no crea una nueva marca de borrado; solo reanuda solicitudes ya pendientes.
En el sistema, este archivo se conecta con el servicio interno services/api/src/modules/reconciliation/pending-requirement-recovery.service.js, que inspecciona el estado persistido del Requirement y aplica la reanudación con resumePendingRequirementDeletion. La separación entre CLI y servicio está reforzada por guardas estáticas; la ruta HTTP no importa este módulo de recuperación.
[!IMPORTANT] La ejecución real exige un destino local autorizado, una clave de confirmación y, para el modo
--execute, una autorización explícita para borrar archivos de ImageKit. El script rechaza otros escenarios antes de conectar.
How it works
flowchart TD
CLI[resumePendingRequirementDeletions]
Parse[parseRecoveryArgs]
Env[ENV values]
LocalTarget[assertLocalMongoTarget]
ImageKitGuard[RECOVERY_IMAGEKIT_DELETES allow]
Connect[mongoose connect]
ServerGuard[assertConnectedServerIsLocal]
Inspect[inspectPendingRequirement]
DryRun{execute}
Token[issueConfirmToken]
Verify[verifyConfirmToken]
Context[createRecoveryContext]
Resume[resumePendingRequirementDeletion]
Audit[JSONL audit output]
Exit[process exit code]
CLI --> Parse
Env --> Parse
Env --> ImageKitGuard
Env --> LocalTarget
LocalTarget --> Connect
ImageKitGuard --> Connect
Connect --> ServerGuard
ServerGuard --> Inspect
Inspect --> DryRun
DryRun -->|no| Token
DryRun -->|yes| Verify
Verify --> Context
Context --> Resume
Resume --> Audit
Token --> Exit
Audit --> Exit
El script primero interpreta argumentos y valida variables de entorno. Después comprueba que el destino MongoDB sea local y autorizado, desactiva la creación automática de índices y colecciones, y carga el servicio de recuperación. Solo entonces conecta con mongoose, vuelve a verificar el servidor y lee el estado observado de cada Requirement.
Si no se pasa --execute, emite un informe dry_run por cada Requirement y genera un token de confirmación. Si sí se pasa --execute, verifica ese token, crea un contexto interno con operatorRef, reason y runId, y llama a resumePendingRequirementDeletion para cada id. Al final escribe auditoría JSONL y retorna un código de salida según el resultado global.
Key components
{
"title": "EXIT",
"content": "Constante inmutable con los c\u00f3digos de salida del CLI: OK, CONNECTION, NOT_COMPLETED, CONFIRMATION, USAGE y TARGET_REFUSED.",
"type": "enum",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
{
"title": "UsageError",
"content": "Clase de error usada para rechazar argumentos inv\u00e1lidos o combinaciones no permitidas en la CLI.",
"type": "class",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
{
"title": "parseRecoveryArgs",
"content": "Parsea los argumentos de la CLI, valida su forma y devuelve la selecci\u00f3n de `Requirement` junto con el modo de ejecuci\u00f3n.",
"type": "function",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
{
"title": "confirmState",
"content": "Construye el estado observable que queda ligado al token de confirmaci\u00f3n. Cualquier cambio en ese estado invalida el token.",
"type": "function",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
{
"title": "issueConfirmToken",
"content": "Genera un token HMAC con payload base64url, ids ordenados, estados observados, destino y expiraci\u00f3n.",
"type": "function",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
{
"title": "verifyConfirmToken",
"content": "Comprueba firma, versi\u00f3n, expiraci\u00f3n, ids, destino y estados antes de permitir cualquier escritura.",
"type": "function",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
{
"title": "main",
"content": "Orquesta el flujo completo de recuperaci\u00f3n: validaci\u00f3n, conexi\u00f3n, inspecci\u00f3n, confirmaci\u00f3n, reanudaci\u00f3n y auditor\u00eda.",
"type": "function",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
{
"title": "pseudonym",
"content": "Deriva un identificador seudonimizado para operador y due\u00f1o, usando HMAC-SHA256 y un prefijo corto.",
"type": "function",
"filePath": "services/api/src/scripts/resumePendingRequirementDeletions.js",
"badges": [
"public"
]
}
Data & fields
[
{
"name": "argv",
"type": "string[]",
"required": true,
"description": "Argumentos de entrada de la CLI. El script solo admite opciones explícitas y rechaza cualquier otra forma."
},
{
"name": "env",
"type": "object",
"required": true,
"description": "Variables de entorno usadas por la recuperación.",
"children": [
{
"name": "RECOVERY_MONGODB_URI",
"type": "string",
"required": true,
"description": "URI de MongoDB usada por la recuperación. El script la toma de este entorno, no del backend general."
},
{
"name": "RECOVERY_ALLOWED_DB",
"type": "string",
"required": true,
"description": "Base de datos permitida para el destino local."
},
{
"name": "RECOVERY_CONFIRM_KEY",
"type": "string",
"required": true,
"description": "Clave HMAC para emitir y verificar el token de confirmación. Debe tener al menos 32 caracteres."
},
{
"name": "RECOVERY_IMAGEKIT_DELETES",
"type": "string",
"description": "Debe valer `allow` para ejecutar la recuperación real, salvo en `NODE_ENV=test`."
},
{
"name": "NODE_ENV",
"type": "string",
"description": "Si vale `test`, la guarda de ImageKit no exige el permiso de borrado real."
}
]
},
{
"name": "options",
"type": "object",
"required": true,
"description": "Resultado de `parseRecoveryArgs`.",
"children": [
{
"name": "requirementIds",
"type": "string[]",
"required": true,
"description": "Lista única de ids seleccionados por `--requirement`."
},
{
"name": "execute",
"type": "boolean",
"required": true,
"description": "Indica si la CLI solo evalúa o si ejecuta la recuperación."
},
{
"name": "confirm",
"type": "string | null",
"required": true,
"description": "Token HMAC exigido únicamente en modo `--execute`."
},
{
"name": "operator",
"type": "string | null",
"required": true,
"description": "Referencia seudonimizable del operador, exigida solo en ejecución."
},
{
"name": "reason",
"type": "string | null",
"required": true,
"description": "Motivo de ejecución. El único valor admitido es `D1_RECOVERY`."
}
]
}
]
[
{
"name": "EXIT.OK",
"type": "number",
"required": true,
"description": "Ejecución correcta."
},
{
"name": "EXIT.CONNECTION",
"type": "number",
"required": true,
"description": "Fallo de conexión o error interno tratado como problema de conexión."
},
{
"name": "EXIT.NOT_COMPLETED",
"type": "number",
"required": true,
"description": "Alguna reanudación no terminó en estado completado."
},
{
"name": "EXIT.CONFIRMATION",
"type": "number",
"required": true,
"description": "La confirmación fue rechazada."
},
{
"name": "EXIT.USAGE",
"type": "number",
"required": true,
"description": "Uso inválido de la CLI."
},
{
"name": "EXIT.TARGET_REFUSED",
"type": "number",
"required": true,
"description": "El destino fue rechazado por las guardas de seguridad."
},
{
"name": "MAX_REQUIREMENTS",
"type": "number",
"required": true,
"description": "Máximo de `Requirement` admitidos por ejecución. El valor es 10."
},
{
"name": "CONFIRM_TTL_SECONDS",
"type": "number",
"required": true,
"description": "Tiempo de vida del token de confirmación. El valor es 15 minutos."
}
]
Behaviors
parseRecoveryArgs(argv)— procesa--requirement,--execute,--confirm,--operatory--reason, y devuelve un objeto validado.confirmState(observed)— reduce el estado observado a los campos que deben quedar ligados al token.issueConfirmToken({ key, ids, states, target, now, ttlSeconds })— emite un token HMAC con expiración y estado firmado.verifyConfirmToken({ token, key, ids, states, target, now })— valida formato, firma, versión, expiración, ids, destino y estado.main(argv, env, io)— ejecuta el flujo completo y devuelve un código de salida.pseudonym(key, prefix, value)— genera una etiqueta seudonimizada corta para logging.
Workflow
1. Selección explícita | El operador indica uno o más `--requirement=<id>` y la CLI valida que no haya opciones extrañas.
2. Guardas previas | El script exige `RECOVERY_CONFIRM_KEY`, valida el destino local y bloquea el modo real si ImageKit no fue autorizado.
3. Conexión controlada | Se conecta con `mongoose` usando `directConnection: true`, sin autoIndex ni autoCreate.
4. Inspección del estado | Se lee cada `Requirement` con `inspectPendingRequirement` y se calcula su elegibilidad.
5. Dry-run o ejecución | Sin `--execute`, se emite un token de confirmación. Con `--execute`, se verifica el token y se llama al servicio de recuperación.
6. Auditoría y salida | Se escriben eventos JSONL de inicio y resultado, y la CLI termina con el código apropiado.
Detailed behavior
Selección de argumentos
La CLI acepta una selección explícita de Requirement mediante --requirement=<id>. El valor debe ser un ObjectId hexadecimal de 24 caracteres. La lista se deduplica y se limita a MAX_REQUIREMENTS, que vale 10. Cualquier opción no reconocida provoca UsageError.
Confirmación de dos pasos
El token de confirmación se genera con issueConfirmToken y se verifica con verifyConfirmToken. El payload incluye la versión, los ids ordenados, el estado observado, el destino y una expiración. La verificación rechaza tokens mal formados, con firma incorrecta, expirados, con ids distintos, con destino distinto o con cambios de estado.
Guardas de destino
La recuperación solo opera contra un destino Mongo local autorizado. El script usa assertLocalMongoTarget antes de conectar y vuelve a comprobar el servidor con assertConnectedServerIsLocal después de la conexión. La validación estática del repositorio confirma que no usa la URI general del backend y que la comprobación del servidor se hace con hello: 1.
Modo dry-run
En modo seco, el script no escribe nada. Recolecta el estado observado de cada Requirement, imprime un bloque dry_run por id y luego emite un bloque confirmation con el token y el tiempo de vida. Ese flujo está documentado en el propio script y verificado en las pruebas del repositorio.
Modo de ejecución
En modo real, la CLI exige --confirm, --operator y --reason=D1_RECOVERY. Antes de actuar crea un runId, construye un contexto interno con createRecoveryContext, y reitera la comprobación del estado justo antes de reanudar. Si una reanudación falla por error de infraestructura, el script la convierte en pending_again para no dejar el id sin resultado de auditoría.
Gotchas & edge cases
[!WARNING]
--executeno funciona sinRECOVERY_IMAGEKIT_DELETES=allow, salvo enNODE_ENV=test. La guarda existe antes de conectar y bloquea la ejecución real.
[!WARNING] El script no acepta un modo global para “todos”. Solo admite ids concretos con
--requirement, y el máximo por ejecución es 10.
[!CAUTION] Un token de confirmación deja de ser válido si cambia el conjunto de ids, el destino, la expiración o el estado observado. La verificación se repite antes de cualquier escritura.
[!NOTE] El log de auditoría usa seudónimos para
operatoryowner, y no imprime el token en modo ejecución. El dry-run sí expone el token dentro del bloque de confirmación.
Related
Este script depende directamente del servicio services/api/src/modules/reconciliation/pending-requirement-recovery.service.js, que encapsula inspectPendingRequirement, eligibility, createRecoveryContext y resumePendingRequirementDeletion. Ese servicio reutiliza el protocolo de borrado existente en modo resumeOnly y toma el dueño desde el documento persistido.
También se apoya en services/api/src/scripts/lib/localMongoTarget.js para el control del destino local autorizado. Las pruebas estáticas del repositorio confirman que la CLI usa esas guardas antes de conectar y que la ruta HTTP no participa en esta recuperación.
El servicio interno ejecuta la recuperación de un único Requirement pendiente. Antes de actuar, vuelve a inspeccionar el documento, calcula su elegibilidad y solo continúa si el estado sigue siendo compatible. Esa inspección y esa repetición de precondiciones son parte central del contrato que la CLI consume.
La CLI produce líneas JSONL con kind: "dry_run", kind: "confirmation", kind: "recovery_started" y kind: "recovery_result". El diseño deja trazabilidad suficiente para detectar una interrupción: una línea started sin su resultado posterior indica que la ejecución fue cortada a mitad del proceso.