> ## Documentation Index
> Fetch the complete documentation index at: https://docs.triglit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CNP (Custom Node Protocol)

> Entenda o protocolo CNP para comunicação entre o Triglit e seus servidores customizados

O **CNP (Custom Node Protocol)** é o protocolo HTTP usado pelo Triglit para se comunicar com seus servidores e executar custom nodes. Ele define como o Triglit envia requisições e como você deve responder.

## O que é o CNP?

O CNP é um protocolo simples baseado em HTTP/JSON que permite:

* ✅ Executar lógica customizada nos seus próprios servidores
* ✅ Receber dados do workflow (inputs, config, metadata)
* ✅ Retornar resultados para o workflow
* ✅ Autenticação segura via HMAC SHA256
* ✅ Suporte a heartbeat para verificação de saúde

## Por que usar CNP?

<AccordionGroup>
  <Accordion title="Flexibilidade">
    Execute qualquer lógica nos seus servidores, sem limitações do Triglit.
  </Accordion>

  <Accordion title="Integração">
    Conecte workflows diretamente com sua infraestrutura existente.
  </Accordion>

  <Accordion title="Segurança">
    Autenticação via HMAC garante que apenas o Triglit pode chamar seus endpoints.
  </Accordion>
</AccordionGroup>

## Como Funciona

### Fluxo de Execução

```
Workflow → Custom Node → Triglit → CNP Request → Seu Servidor
                                              ↓
Workflow ← Custom Node ← Triglit ← CNP Response ← Seu Servidor
```

1. **Workflow executa**: Um workflow chega em um custom node
2. **Triglit prepara requisição**: Monta payload e gera assinatura HMAC
3. **Requisição HTTP**: POST para seu endpoint CNP
4. **Você valida e processa**: Valida assinatura e executa lógica
5. **Resposta**: Retorna resultado ou erro
6. **Workflow continua**: Triglit usa o resultado para continuar o workflow

## Configuração

### 1. Configurar Endpoint no Triglit

No painel do Triglit, configure seu endpoint CNP:

1. Acesse **Configurações** → **CNP**
2. Configure o **Custom Nodes Endpoint**: `https://api.seudominio.com/triglit/cnp`
3. O secret será gerado automaticamente (ou você pode usar um existente)

### 2. Implementar Servidor CNP

Seu servidor deve expor um endpoint POST que aceita requisições CNP:

<CodeGroup>
  ```typescript TypeScript (Recomendado) theme={null}
  import express from 'express';
  import { Triglit, type CNPRequestBody } from 'triglit';

  const app = express();
  app.use(express.json());

  const CNP_SECRET = process.env.TRIGLIT_CNP_SECRET!;
  const triglit = new Triglit({ apiKey: 'your-api-key' });

  // Endpoint CNP
  app.post('/triglit/cnp', async (req, res) => {
    try {
      const signature = req.headers['x-triglit-signature'] as string;
      
      if (!signature) {
        return res.status(401).json({
          status: 'failed',
          error: 'Missing signature header'
        });
      }
      
      // Tipar o body usando o tipo do SDK
      const body = req.body as CNPRequestBody<Record<string, unknown>, Record<string, unknown>>;
      
      // Serializar payload como JSON string (igual ao que o Triglit envia)
      const payload = JSON.stringify(body);
      
      // Validar assinatura usando o SDK (recomendado)
      const isValid = await triglit.customNodes.validateCNPSignature(
        payload,
        signature,
        CNP_SECRET
      );
      
      if (!isValid) {
        return res.status(401).json({
          status: 'failed',
          error: 'Invalid signature'
        });
      }
      
      // Heartbeat
      if (body.type === 'heartbeat') {
        return res.status(200).json({ status: 'ok' });
      }
      
      // Executar custom node (body.type === 'run' neste ponto)
      if (body.type === 'run') {
        const { nodeType, inputs, config } = body.payload;
        
        // Sua lógica customizada aqui
        const result = await executeCustomNode(nodeType, inputs, config);
        
        return res.status(200).json({
          status: 'completed',
          outputs: result
        });
      }
      
    } catch (error) {
      return res.status(500).json({
        status: 'failed',
        error: (error as Error).message
      });
    }
  });

  app.listen(3000);
  ```

  ```javascript JavaScript (Manual) theme={null}
  const express = require('express');
  const crypto = require('crypto');

  const app = express();
  app.use(express.json());

  const CNP_SECRET = process.env.TRIGLIT_CNP_SECRET;

  // Comparação time-safe para prevenir timing attacks
  function timingSafeEqual(a, b) {
    if (a.length !== b.length) return false;
    
    const bufA = Buffer.from(a);
    const bufB = Buffer.from(b);
    
    return crypto.timingSafeEqual(bufA, bufB);
  }

  // Validar assinatura HMAC
  function validateSignature(payload, signature, secret) {
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(payload, 'utf8');
    // Formato: sha256=<base64>
    const expectedSignature = `sha256=${hmac.digest('base64')}`;
    
    return timingSafeEqual(signature, expectedSignature);
  }

  // Endpoint CNP
  app.post('/triglit/cnp', async (req, res) => {
    try {
      const signature = req.headers['x-triglit-signature'];
      
      if (!signature) {
        return res.status(401).json({
          status: 'failed',
          error: 'Missing signature header'
        });
      }
      
      // Serializar payload como JSON string (igual ao que o Triglit envia)
      const payload = JSON.stringify(req.body);
      
      // Validar assinatura
      if (!validateSignature(payload, signature, CNP_SECRET)) {
        return res.status(401).json({
          status: 'failed',
          error: 'Invalid signature'
        });
      }
      
      // Heartbeat
      if (req.body.type === 'heartbeat') {
        return res.status(200).json({ status: 'ok' });
      }
      
      // Executar custom node
      const { nodeType, inputs, config, metadata } = req.body;
      
      // Sua lógica customizada aqui
      const result = await executeCustomNode(nodeType, inputs, config);
      
      return res.status(200).json({
        status: 'completed',
        outputs: result
      });
      
    } catch (error) {
      return res.status(500).json({
        status: 'failed',
        error: error.message
      });
    }
  });

  app.listen(3000);
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify
  import hmac
  import hashlib
  import base64
  import os
  import json

  app = Flask(__name__)
  CNP_SECRET = os.getenv('TRIGLIT_CNP_SECRET')

  def validate_signature(payload, signature, secret):
      """Valida assinatura HMAC SHA256"""
      # Calcular HMAC SHA256
      expected_hmac = hmac.new(
          secret.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).digest()
      
      # Formato: sha256=<base64>
      expected_signature = f'sha256={base64.b64encode(expected_hmac).decode()}'
      
      # Comparação time-safe
      return hmac.compare_digest(signature, expected_signature)

  @app.route('/triglit/cnp', methods=['POST'])
  def handle_cnp():
      try:
          signature = request.headers.get('X-Triglit-Signature')
          
          if not signature:
              return jsonify({
                  'status': 'failed',
                  'error': 'Missing signature header'
              }), 401
          
          # Serializar payload como JSON string
          payload = json.dumps(request.json, separators=(',', ':'))
          
          # Validar assinatura
          if not validate_signature(payload, signature, CNP_SECRET):
              return jsonify({
                  'status': 'failed',
                  'error': 'Invalid signature'
              }), 401
          
          # Heartbeat
          if request.json.get('type') == 'heartbeat':
              return jsonify({'status': 'ok'})
          
          # Executar custom node
          node_type = request.json.get('nodeType')
          inputs = request.json.get('inputs', {})
          config = request.json.get('config', {})
          
          # Sua lógica customizada aqui
          result = execute_custom_node(node_type, inputs, config)
          
          return jsonify({
              'status': 'completed',
              'outputs': result
          })
          
      except Exception as e:
          return jsonify({
              'status': 'failed',
              'error': str(e)
          }), 500

  if __name__ == '__main__':
      app.run(port=3000)
  ```
</CodeGroup>

## Estrutura da Requisição

Quando o Triglit executa um custom node, ele envia uma requisição POST com:

### Headers

```
Content-Type: application/json
X-Triglit-Signature: sha256=<hmac_sha256_base64>
```

### Body (Payload)

```json theme={null}
{
  "nodeType": "process-payment",
  "runId": "run_123",
  "tenantId": "tenant_abc",
  "subTenantId": "sub_xyz",
  "inputs": {
    "orderId": "order_456",
    "amount": 100.00,
    "currency": "USD"
  },
  "config": {
    "gateway": "stripe",
    "autoCapture": true
  },
  "metadata": {
    "timestamp": "2024-01-15T10:00:00Z",
    "version": "1.0.0"
  }
}
```

### Campos do Payload

* **`nodeType`**: Tipo do custom node sendo executado
* **`runId`**: ID da execução (run) atual
* **`tenantId`**: ID do tenant
* **`subTenantId`**: ID do sub-tenant (opcional)
* **`inputs`**: Dados de entrada do workflow (output do node anterior)
* **`config`**: Configuração do node no workflow
* **`metadata`**: Metadados da execução (timestamp, versão do node)

## Estrutura da Resposta

### Sucesso

```json theme={null}
{
  "status": "completed",
  "outputs": {
    "paymentId": "pay_123",
    "status": "succeeded",
    "amount": 100.00
  }
}
```

### Erro

```json theme={null}
{
  "status": "failed",
  "error": "Payment gateway unavailable"
}
```

### Heartbeat

```json theme={null}
{
  "status": "ok"
}
```

## Autenticação HMAC

### Como Funciona

1. **Triglit serializa** o payload como JSON string
2. **Gera assinatura** HMAC SHA256 usando o secret do tenant
3. **Envia** no header `X-Triglit-Signature` no formato `sha256=<base64>`
4. **Você valida** recalculando a assinatura e comparando (ou usando o SDK TypeScript)

### Algoritmo de Validação

A assinatura é enviada no formato `sha256=<base64>`:

```typescript theme={null}
// Usando o SDK TypeScript (recomendado)
import { Triglit } from 'triglit';

const triglit = new Triglit({ apiKey: 'your-api-key' });
const isValid = await triglit.customNodes.validateCNPSignature(
  payload,
  signature,
  cnpSecret
);
```

```javascript theme={null}
// Validação manual
function validateSignature(payload, signature, secret) {
  // 1. Calcular HMAC SHA256
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(payload, 'utf8');
  // 2. Formato: sha256=<base64>
  const expectedSignature = `sha256=${hmac.digest('base64')}`;
  
  // 3. Comparação time-safe
  return timingSafeEqual(signature, expectedSignature);
}
```

**Formato da assinatura:**

* Algoritmo: HMAC SHA256
* Encoding: Base64 (com prefixo `sha256=`)
* Header: `X-Triglit-Signature`
* Valor: String no formato `sha256=<base64>` (ex: `sha256=a1b2c3d4e5f6...`)

<Warning>
  Sempre use comparação time-safe (timing-safe comparison) para validar assinaturas. Comparações normais podem ser vulneráveis a timing attacks.
</Warning>

## Heartbeat

O Triglit pode enviar requisições de **heartbeat** para verificar se seu servidor está online:

```json theme={null}
{
  "type": "heartbeat"
}
```

Responda com:

```json theme={null}
{
  "status": "ok"
}
```

<Note>
  Heartbeats não requerem processamento de lógica. Apenas valide a assinatura e retorne `{ status: "ok" }`.
</Note>

## Timeout e Retries

### Timeout

Cada custom node tem um timeout configurável (padrão: 30 segundos). Se seu servidor não responder dentro do timeout, o Triglit considera como falha.

### Retries

O Triglit retenta automaticamente em caso de falha:

* **Máximo de tentativas**: Configurável por node (padrão: 3)
* **Backoff**: Exponencial entre tentativas
* **Falhas que disparam retry**: Timeout, erro 5xx, erro de rede

## Boas Práticas

<AccordionGroup>
  <Accordion title="Validação de Assinatura">
    Sempre valide a assinatura antes de processar. Use comparação time-safe.
  </Accordion>

  <Accordion title="Idempotência">
    Torne seus handlers idempotentes usando `runId` para evitar processamento duplicado.
  </Accordion>

  <Accordion title="Resposta Rápida">
    Retorne rapidamente (dentro do timeout). Processe operações longas de forma assíncrona.
  </Accordion>

  <Accordion title="Tratamento de Erros">
    Sempre retorne erros estruturados com mensagens claras.
  </Accordion>

  <Accordion title="Logging">
    Logue todas as requisições usando `runId` para rastreabilidade.
  </Accordion>

  <Accordion title="HTTPS">
    Use sempre HTTPS em produção para proteger dados em trânsito.
  </Accordion>
</AccordionGroup>

## Exemplo Completo

### Servidor CNP Completo

<CodeGroup>
  ```typescript TypeScript (Recomendado) theme={null}
  import express from 'express';
  import { Triglit, type CNPRequestBody } from 'triglit';

  const app = express();
  app.use(express.json());

  const CNP_SECRET = process.env.TRIGLIT_CNP_SECRET!;
  const triglit = new Triglit({ apiKey: 'your-api-key' });

  async function executeCustomNode(nodeType: string, inputs: any, config: any) {
    // Exemplo: Processar pagamento
    if (nodeType === 'process-payment') {
      const { orderId, amount } = inputs;
      const { gateway } = config;
      
      // Simular processamento
      const paymentId = `pay_${Date.now()}`;
      
      return {
        paymentId,
        status: 'succeeded',
        amount,
        gateway
      };
    }
    
    // Outros tipos de nodes...
    throw new Error(`Unknown node type: ${nodeType}`);
  }

  app.post('/triglit/cnp', async (req, res) => {
    try {
      const signature = req.headers['x-triglit-signature'] as string;
      
      if (!signature) {
        return res.status(401).json({
          status: 'failed',
          error: 'Missing signature'
        });
      }
      
      // Tipar o body usando o tipo do SDK
      const body = req.body as CNPRequestBody<Record<string, unknown>, Record<string, unknown>>;
      const payload = JSON.stringify(body);
      
      // Validar assinatura usando o SDK
      const isValid = await triglit.customNodes.validateCNPSignature(
        payload,
        signature,
        CNP_SECRET
      );
      
      if (!isValid) {
        return res.status(401).json({
          status: 'failed',
          error: 'Invalid signature'
        });
      }
      
      // Heartbeat
      if (body.type === 'heartbeat') {
        return res.json({ status: 'ok' });
      }
      
      // Executar node (body.type === 'run' neste ponto)
      if (body.type === 'run') {
        const { nodeType, inputs, config, runId } = body.payload;
        
        console.log(`[CNP] Executing ${nodeType} for run ${runId}`);
        
        const outputs = await executeCustomNode(nodeType, inputs, config);
        
        return res.json({
          status: 'completed',
          outputs
        });
      }
      
    } catch (error) {
      console.error('[CNP] Error:', error);
      return res.status(500).json({
        status: 'failed',
        error: (error as Error).message
      });
    }
  });

  app.listen(3000, () => {
    console.log('CNP server running on port 3000');
  });
  ```

  ```javascript JavaScript (Manual) theme={null}
  const express = require('express');
  const crypto = require('crypto');

  const app = express();
  app.use(express.json());

  const CNP_SECRET = process.env.TRIGLIT_CNP_SECRET;

  function timingSafeEqual(a, b) {
    if (a.length !== b.length) return false;
    const bufA = Buffer.from(a);
    const bufB = Buffer.from(b);
    return crypto.timingSafeEqual(bufA, bufB);
  }

  function validateSignature(payload, signature, secret) {
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(payload, 'utf8');
    // Formato: sha256=<base64>
    const expectedSignature = `sha256=${hmac.digest('base64')}`;
    return timingSafeEqual(signature, expectedSignature);
  }

  async function executeCustomNode(nodeType, inputs, config) {
    // Exemplo: Processar pagamento
    if (nodeType === 'process-payment') {
      const { orderId, amount } = inputs;
      const { gateway } = config;
      
      // Simular processamento
      const paymentId = `pay_${Date.now()}`;
      
      return {
        paymentId,
        status: 'succeeded',
        amount,
        gateway
      };
    }
    
    // Outros tipos de nodes...
    throw new Error(`Unknown node type: ${nodeType}`);
  }

  app.post('/triglit/cnp', async (req, res) => {
    try {
      const signature = req.headers['x-triglit-signature'];
      
      if (!signature) {
        return res.status(401).json({
          status: 'failed',
          error: 'Missing signature'
        });
      }
      
      const payload = JSON.stringify(req.body);
      
      if (!validateSignature(payload, signature, CNP_SECRET)) {
        return res.status(401).json({
          status: 'failed',
          error: 'Invalid signature'
        });
      }
      
      // Heartbeat
      if (req.body.type === 'heartbeat') {
        return res.json({ status: 'ok' });
      }
      
      // Executar node
      const { nodeType, inputs, config, runId } = req.body;
      
      console.log(`[CNP] Executing ${nodeType} for run ${runId}`);
      
      const outputs = await executeCustomNode(nodeType, inputs, config);
      
      return res.json({
        status: 'completed',
        outputs
      });
      
    } catch (error) {
      console.error('[CNP] Error:', error);
      return res.status(500).json({
        status: 'failed',
        error: error.message
      });
    }
  });

  app.listen(3000, () => {
    console.log('CNP server running on port 3000');
  });
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify
  import hmac
  import hashlib
  import base64
  import os
  import json
  import time

  app = Flask(__name__)
  CNP_SECRET = os.getenv('TRIGLIT_CNP_SECRET')

  def validate_signature(payload, signature, secret):
      """Valida assinatura HMAC SHA256"""
      # Calcular HMAC SHA256
      expected_hmac = hmac.new(
          secret.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).digest()
      
      # Formato: sha256=<base64>
      expected_signature = f'sha256={base64.b64encode(expected_hmac).decode()}'
      
      # Comparação time-safe
      return hmac.compare_digest(signature, expected_signature)

  def execute_custom_node(node_type, inputs, config):
      if node_type == 'process-payment':
          order_id = inputs.get('orderId')
          amount = inputs.get('amount')
          gateway = config.get('gateway')
          
          return {
              'paymentId': f'pay_{int(time.time())}',
              'status': 'succeeded',
              'amount': amount,
              'gateway': gateway
          }
      
      raise ValueError(f'Unknown node type: {node_type}')

  @app.route('/triglit/cnp', methods=['POST'])
  def handle_cnp():
      try:
          signature = request.headers.get('X-Triglit-Signature')
          
          if not signature:
              return jsonify({
                  'status': 'failed',
                  'error': 'Missing signature'
              }), 401
          
          payload = json.dumps(request.json, separators=(',', ':'))
          
          if not validate_signature(payload, signature, CNP_SECRET):
              return jsonify({
                  'status': 'failed',
                  'error': 'Invalid signature'
              }), 401
          
          if request.json.get('type') == 'heartbeat':
              return jsonify({'status': 'ok'})
          
          node_type = request.json.get('nodeType')
          inputs = request.json.get('inputs', {})
          config = request.json.get('config', {})
          run_id = request.json.get('runId')
          
          print(f'[CNP] Executing {node_type} for run {run_id}')
          
          outputs = execute_custom_node(node_type, inputs, config)
          
          return jsonify({
              'status': 'completed',
              'outputs': outputs
          })
          
      except Exception as e:
          print(f'[CNP] Error: {e}')
          return jsonify({
              'status': 'failed',
              'error': str(e)
          }), 500

  if __name__ == '__main__':
      app.run(port=3000)
  ```
</CodeGroup>

## Limitações

* **Timeout máximo**: 5 minutos por requisição (configurável por node)
* **Tamanho de payload**: 1MB máximo
* **Retries**: Máximo de 10 tentativas (configurável por node)

## Troubleshooting

### Erro: Invalid Signature

1. Verifique se o secret está correto
2. Certifique-se de serializar o payload exatamente como recebido
3. Use comparação time-safe
4. Verifique se está usando o formato correto: `sha256=<base64>` (não hexadecimal)
5. **Recomendado**: Use o SDK TypeScript com `validateCNPSignature` para evitar erros de implementação

### Erro: Timeout

1. Otimize seu código para responder rapidamente
2. Processe operações longas de forma assíncrona
3. Aumente o timeout do node se necessário

### Erro: Connection Refused

1. Verifique se seu servidor está acessível
2. Confirme que o endpoint está correto no Triglit
3. Verifique firewall e configurações de rede

<Tip>
  Use o painel do Triglit para testar seu endpoint CNP antes de usar em produção. O painel oferece ferramentas de validação e teste.
</Tip>
