{
  "name": "Praesidia - explicit protected HTTP prepare and resume",
  "nodes": [
    {
      "parameters": {},
      "id": "19bb5460-61df-5971-a954-1030362195df",
      "name": "Manual evaluation",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"phase\": \"prepare\",\n  \"resumeConfirmed\": false,\n  \"apiOrigin\": \"https://api.praesidia.ai\",\n  \"organizationId\": \"REPLACE_WITH_ORGANIZATION_UUID\",\n  \"targetId\": \"REPLACE_WITH_REGISTERED_TARGET_ID\",\n  \"body\": {\n    \"message\": \"harmless evaluation request\"\n  },\n  \"checkpoint\": {\n    \"runtime\": \"custom\",\n    \"threadId\": \"REPLACE_WITH_UNIQUE_WORKFLOW_RUN_ID\",\n    \"nodeId\": \"protected-http\"\n  },\n  \"description\": \"n8n explicit protected HTTP evaluation\",\n  \"approvalId\": \"\",\n  \"expectedRequestCommitment\": \"\"\n}",
        "options": {}
      },
      "id": "d7fb2e3e-62e9-59ad-a3fc-bfd64b9d86f4",
      "name": "Request settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        230,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const settings = $input.first().json;\nconst uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst origin = new URL(settings.apiOrigin);\nif (origin.protocol !== 'https:' || origin.origin !== settings.apiOrigin || origin.username || origin.password) throw new Error('Use the operator-provided HTTPS API origin only.');\nif (!uuid.test(settings.organizationId)) throw new Error('Set the intended organization UUID.');\nif (!settings.targetId || settings.targetId.startsWith('REPLACE_') || settings.checkpoint.threadId.startsWith('REPLACE_')) throw new Error('Set the registered target and unique stable workflow run ID.');\nif (!['prepare', 'resume'].includes(settings.phase)) throw new Error('Select prepare or resume explicitly.');\nif (settings.phase === 'resume' && (settings.resumeConfirmed !== true || !uuid.test(settings.approvalId) || !/^[a-f0-9]{64}$/.test(settings.expectedRequestCommitment))) throw new Error('Resume requires explicit confirmation and the original approval ID and request commitment.');\nreturn [{ json: settings }];"
      },
      "id": "2d1f7337-d890-5c04-917b-08b1f62ac688",
      "name": "Validate request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        430,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "phase-check",
              "leftValue": "={{ $json.phase }}",
              "rightValue": "prepare",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "db703578-edd5-5237-b5c0-6d252c03cf75",
      "name": "Prepare phase",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Request settings').first().json.apiOrigin + '/organizations/' + $('Request settings').first().json.organizationId + '/protected-actions/http/prepare' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 30000
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ targetId: $json.targetId, body: $json.body, checkpoint: $json.checkpoint, description: $json.description }) }}"
      },
      "id": "2516187d-d263-542a-a36c-a4a662914f3a",
      "name": "Prepare and stop for review",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        900,
        -130
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Praesidia personal caller"
        }
      },
      "retryOnFail": false
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $('Request settings').first().json.apiOrigin + '/organizations/' + $('Request settings').first().json.organizationId + '/protected-actions/http/checkpoints/' + $('Request settings').first().json.approvalId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 30000
        }
      },
      "id": "8190f873-bed9-5d12-8a85-342d4bf08aaf",
      "name": "Read owned checkpoint",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        900,
        130
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Praesidia personal caller"
        }
      },
      "retryOnFail": false
    },
    {
      "parameters": {
        "jsCode": "const request = $('Request settings').first().json;\nconst checkpoint = $input.first().json;\n// This fresh read is a convenience gate. The backend still owns the atomic approval/binding check.\nif (request.phase !== 'resume' || request.resumeConfirmed !== true) throw new Error('Resume was not explicitly selected.');\nif (checkpoint.approvalId !== request.approvalId || checkpoint.requestCommitment !== request.expectedRequestCommitment) throw new Error('Approval or exact request commitment mismatch. Stop and investigate.');\nif (checkpoint.consumedAt !== null || checkpoint.status !== 'APPROVED') return [];\nif (!checkpoint.approverId || !Number.isFinite(Date.parse(checkpoint.expiresAt)) || Date.parse(checkpoint.expiresAt) <= Date.now()) return [];\nreturn [{ json: request }];"
      },
      "id": "fb8b9354-253a-5163-8e42-15485fb4be4a",
      "name": "Approved exact request only",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1130,
        130
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Request settings').first().json.apiOrigin + '/organizations/' + $('Request settings').first().json.organizationId + '/protected-actions/http/resume' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 30000
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ approvalId: $json.approvalId, targetId: $json.targetId, body: $json.body, checkpoint: $json.checkpoint }) }}"
      },
      "id": "9060733a-0121-5c64-bfeb-85d05944cd96",
      "name": "Resume approved request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1360,
        130
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Praesidia personal caller"
        }
      },
      "retryOnFail": false
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $('Request settings').first().json.apiOrigin + '/organizations/' + $('Request settings').first().json.organizationId + '/protected-actions/http/checkpoints/' + $('Request settings').first().json.approvalId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 30000
        }
      },
      "id": "349f3df4-383d-5eb2-8903-d996c4c5cbe8",
      "name": "Read committed outcome",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1590,
        130
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Praesidia personal caller"
        }
      },
      "retryOnFail": false
    }
  ],
  "connections": {
    "Manual evaluation": {
      "main": [
        [
          {
            "node": "Request settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request settings": {
      "main": [
        [
          {
            "node": "Validate request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare phase": {
      "main": [
        [
          {
            "node": "Prepare and stop for review",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Read owned checkpoint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read owned checkpoint": {
      "main": [
        [
          {
            "node": "Approved exact request only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved exact request only": {
      "main": [
        [
          {
            "node": "Resume approved request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resume approved request": {
      "main": [
        [
          {
            "node": "Read committed outcome",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate request": {
      "main": [
        [
          {
            "node": "Prepare phase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
