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, --operator y --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] --execute no funciona sin RECOVERY_IMAGEKIT_DELETES=allow, salvo en NODE_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 operator y owner, y no imprime el token en modo ejecución. El dry-run sí expone el token dentro del bloque de confirmación.

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.