{
  "name": "Open Calls · verify native notification and inspect current call",
  "active": false,
  "meta": { "templateCredsSetupCompleted": false },
  "nodes": [
    {
      "id": "webhook",
      "name": "Open Calls webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [0, 0],
      "parameters": {
        "httpMethod": "POST",
        "path": "open-calls-update",
        "responseMode": "responseNode",
        "options": { "rawBody": true }
      }
    },
    {
      "id": "verify",
      "name": "Verify exact raw body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [220, 0],
      "parameters": {
        "jsCode": "function verifyUpdate(raw, headers, secret, crypto, nowSeconds) {\n  const reject = (statusCode) => ({ accepted: false, statusCode });\n  if (typeof secret !== \"string\" || !/^whsec_[A-Za-z0-9+/]{43}=$/.test(secret))\n    return reject(503);\n  const key = Buffer.from(secret.slice(6), \"base64\");\n  if (key.length !== 32 || key.toString(\"base64\") !== secret.slice(6))\n    return reject(503);\n  if (!Buffer.isBuffer(raw) || raw.length === 0 || raw.length > 65536)\n    return reject(400);\n  const header = (name) => {\n    const entries = Object.entries(headers ?? {}).filter(\n      ([key]) => key.toLowerCase() === name,\n    );\n    return entries.length === 1 && typeof entries[0][1] === \"string\"\n      ? entries[0][1]\n      : null;\n  };\n  const id = header(\"webhook-id\");\n  const timestamp = header(\"webhook-timestamp\");\n  const signature = header(\"webhook-signature\");\n  if (\n    !id ||\n    !/^[A-Za-z0-9_-]{1,160}$/.test(id) ||\n    !timestamp ||\n    !/^[1-9][0-9]{8,11}$/.test(timestamp) ||\n    !signature ||\n    !/^v1,[A-Za-z0-9+/]{43}=$/.test(signature)\n  )\n    return reject(401);\n  if (\n    !Number.isFinite(nowSeconds) ||\n    Math.abs(nowSeconds - Number(timestamp)) > 300\n  )\n    return reject(401);\n  const expected = crypto\n    .createHmac(\"sha256\", key)\n    .update(`${id}.${timestamp}.`)\n    .update(raw)\n    .digest();\n  const supplied = Buffer.from(signature.slice(3), \"base64\");\n  if (\n    supplied.length !== expected.length ||\n    supplied.toString(\"base64\") !== signature.slice(3) ||\n    !crypto.timingSafeEqual(expected, supplied)\n  )\n    return reject(401);\n  let event;\n  try {\n    event = JSON.parse(raw.toString(\"utf8\"));\n  } catch {\n    return reject(400);\n  }\n  if (\n    !event ||\n    event.id !== id ||\n    ![\n      \"webhook.test\",\n      \"call.ended\",\n      \"call.question\",\n      \"call.result_updated\",\n    ].includes(event.type)\n  )\n    return reject(400);\n  if (event.type === \"webhook.test\")\n    return { accepted: true, statusCode: 204, test: true, eventId: id };\n  const callId = event.data?.call_id;\n  if (\n    typeof callId !== \"string\" ||\n    !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(\n      callId,\n    )\n  )\n    return reject(400);\n  // Deliberately discard event URLs and content. Read only this ID at the configured origin.\n  return {\n    accepted: true,\n    statusCode: 204,\n    test: false,\n    eventId: id,\n    eventType: event.type,\n    callId,\n  };\n}\n\nconst item = $input.first();\nlet raw;\ntry { raw = item.binary?.data ? await this.helpers.getBinaryDataBuffer(0, 'data') : null; } catch { return [{json: {accepted: false, statusCode: 503}}]; }\nreturn [{json: verifyUpdate(raw, item.json.headers, $env.OPEN_CALLS_WEBHOOK_SECRET, require('crypto'), Date.now() / 1000)}];"
      }
    },
    {
      "id": "valid",
      "name": "Verified?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [440, 0],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "typeValidation": "strict" },
          "combinator": "and",
          "conditions": [
            {
              "id": "valid",
              "leftValue": "={{ $json.accepted }}",
              "rightValue": true,
              "operator": { "type": "boolean", "operation": "true" }
            }
          ]
        }
      }
    },
    {
      "id": "reject",
      "name": "Reject unverified update",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [660, 220],
      "parameters": {
        "respondWith": "noData",
        "options": { "responseCode": "={{ $json.statusCode }}" }
      }
    },
    {
      "id": "test",
      "name": "Test notification?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [660, -100],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "typeValidation": "strict" },
          "combinator": "and",
          "conditions": [
            {
              "id": "test",
              "leftValue": "={{ $json.test }}",
              "rightValue": true,
              "operator": { "type": "boolean", "operation": "true" }
            }
          ]
        }
      }
    },
    {
      "id": "test-ok",
      "name": "Acknowledge test",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [880, -240],
      "parameters": {
        "respondWith": "noData",
        "options": { "responseCode": 204 }
      }
    },
    {
      "id": "read",
      "name": "Read current call",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [880, 0],
      "parameters": {
        "method": "GET",
        "url": "=https://YOUR-OPEN-CALLS-HOST.example/api/v1/calls/{{ $json.callId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 10000,
          "redirect": { "redirect": { "followRedirects": false } },
          "response": {
            "response": { "responseFormat": "json", "neverError": false }
          }
        }
      },
      "credentials": { "httpBearerAuth": { "name": "Open Calls" } },
      "retryOnFail": false
    },
    {
      "id": "result",
      "name": "Inspect grounded result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1100, 0],
      "parameters": {
        "jsCode": "function groundedResult(call) {\n  const audit = call.audit;\n  if (\n    !audit ||\n    typeof audit.terminal !== \"boolean\" ||\n    !Number.isSafeInteger(call.event_sequence) ||\n    call.event_sequence < 0\n  )\n    throw new Error(\"Missing native call audit.\");\n  const terminal = audit.terminal;\n  const charge = audit.billing?.charged_micro;\n  if (\n    terminal &&\n    (audit.billing?.state !== \"final\" ||\n      !Number.isSafeInteger(charge) ||\n      charge < 0)\n  )\n    throw new Error(\"Incomplete final receipt; read this call again.\");\n  if (!Array.isArray(audit.confirmed) || !Array.isArray(audit.unresolved))\n    throw new Error(\"Missing grounded evidence.\");\n  return {\n    callId: call.id,\n    eventSequence: call.event_sequence,\n    observationKey: `${call.id}:${call.event_sequence}`,\n    state: terminal ? \"final\" : \"pending\",\n    status: call.status,\n    objectiveCompleted: terminal && call.objective_completed === true,\n    simulation: call.result?.simulation === true,\n    outcome: audit.outcome,\n    confirmed: audit.confirmed,\n    unresolved: audit.unresolved,\n    pendingQuestion: audit.pending_question ?? null,\n    chargeMicro: terminal ? charge : null,\n    durationSeconds: audit.duration_seconds ?? null,\n    resultUrl: call.result_url,\n    note: terminal\n      ? \"Report confirmed evidence and unresolved items; call status alone is not success.\"\n      : \"Still pending. Resume by this call ID; do not place another call.\",\n  };\n}\n\nconst call = $input.first().json;\nconst event = $('Verify exact raw body').first().json;\nif (call.id !== event.callId) throw new Error('Call identity mismatch.');\nreturn [{json: {...groundedResult(call), eventId: event.eventId, eventType: event.eventType}}];"
      }
    },
    {
      "id": "ok",
      "name": "Acknowledge inspection",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [1320, 0],
      "parameters": {
        "respondWith": "noData",
        "options": { "responseCode": 204 }
      }
    }
  ],
  "connections": {
    "Open Calls webhook": {
      "main": [
        [{ "node": "Verify exact raw body", "type": "main", "index": 0 }]
      ]
    },
    "Verify exact raw body": {
      "main": [[{ "node": "Verified?", "type": "main", "index": 0 }]]
    },
    "Verified?": {
      "main": [
        [{ "node": "Test notification?", "type": "main", "index": 0 }],
        [{ "node": "Reject unverified update", "type": "main", "index": 0 }]
      ]
    },
    "Test notification?": {
      "main": [
        [{ "node": "Acknowledge test", "type": "main", "index": 0 }],
        [{ "node": "Read current call", "type": "main", "index": 0 }]
      ]
    },
    "Read current call": {
      "main": [
        [{ "node": "Inspect grounded result", "type": "main", "index": 0 }]
      ]
    },
    "Inspect grounded result": {
      "main": [
        [{ "node": "Acknowledge inspection", "type": "main", "index": 0 }]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "none",
    "saveManualExecutions": false,
    "executionTimeout": 1200
  }
}
