@wondai/n8n-nodes-nucleo 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ const n8n_workflow_1 = require("n8n-workflow");
15
15
  * signature = HMAC_SHA256(signingSecret, canônica) em hex.
16
16
  */
17
17
  const SIGNATURE_VERSION = "v1";
18
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18
19
  const HEADERS = {
19
20
  key: "x-wondai-key",
20
21
  timestamp: "x-wondai-timestamp",
@@ -351,8 +352,8 @@ class Nucleo {
351
352
  name: "idempotencyKey",
352
353
  type: "string",
353
354
  default: "",
354
- description: "Opcional. Vazio = gerada automaticamente. Use a MESMA chave num retry para não duplicar o pedido.",
355
- displayOptions: { show: { resource: ["pedido"], operation: ["criar"] } },
355
+ description: "Opcional. Para criar: vazio = gerada automaticamente. Para alterar/cancelar: use a MESMA chave (ex.: messageID do WhatsApp) num retry para não reaplicar a operação.",
356
+ displayOptions: { show: { resource: ["pedido"], operation: ["criar", "alterar", "cancelar"] } },
356
357
  },
357
358
  // ----------------------------------------------------------------- pedido:alterar / cancelar
358
359
  {
@@ -477,6 +478,14 @@ class Nucleo {
477
478
  description: "Opcional. UUID de pedido gerado na conversa (validado contra o tenant).",
478
479
  displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
479
480
  },
481
+ {
482
+ displayName: "Idempotency Key",
483
+ name: "convIdempotencyKey",
484
+ type: "string",
485
+ default: "",
486
+ description: "Opcional. Use a MESMA chave (ex.: messageID do WhatsApp) num retry para não registrar a conversa duas vezes.",
487
+ displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
488
+ },
480
489
  ],
481
490
  };
482
491
  }
@@ -567,6 +576,9 @@ class Nucleo {
567
576
  b.definir_itens = definir;
568
577
  if (removerProdutos.length)
569
578
  b.remover_produto_ids = removerProdutos;
579
+ // Idempotência (opcional): mesma chave num retry → backend não reaplica a alteração.
580
+ idempotencyKey =
581
+ this.getNodeParameter("idempotencyKey", i, "").trim() || undefined;
570
582
  method = "PATCH";
571
583
  path = `/api/v1/agent/pedido/${encodeURIComponent(pedidoId)}`;
572
584
  bodyObj = b;
@@ -574,6 +586,8 @@ class Nucleo {
574
586
  else if (resource === "pedido" && operation === "cancelar") {
575
587
  const pedidoId = this.getNodeParameter("pedidoId", i).trim();
576
588
  const motivo = this.getNodeParameter("motivo", i, "").trim();
589
+ idempotencyKey =
590
+ this.getNodeParameter("idempotencyKey", i, "").trim() || undefined;
577
591
  method = "POST";
578
592
  path = `/api/v1/agent/pedido/${encodeURIComponent(pedidoId)}/cancelar`;
579
593
  bodyObj = motivo ? { motivo } : {};
@@ -586,8 +600,12 @@ class Nucleo {
586
600
  const b = { resumo, resultado };
587
601
  if (convTelefone)
588
602
  b.telefone = convTelefone;
589
- if (convPedidoId)
603
+ // pedido_id é opcional e a IA costuma alucinar ("nenhum", texto, UUID stale).
604
+ // Só envia se for UUID bem-formado — lixo nunca chega ao Núcleo.
605
+ if (convPedidoId && UUID_RE.test(convPedidoId))
590
606
  b.pedido_id = convPedidoId;
607
+ idempotencyKey =
608
+ this.getNodeParameter("convIdempotencyKey", i, "").trim() || undefined;
591
609
  method = "POST";
592
610
  path = "/api/v1/agent/conversa/fechar";
593
611
  bodyObj = b;
@@ -598,38 +616,54 @@ class Nucleo {
598
616
  });
599
617
  }
600
618
  // ---- assina e envia (HMAC v1) ----
601
- const rawBody = bodyObj === undefined ? "" : JSON.stringify(bodyObj);
602
- const timestamp = Math.floor(Date.now() / 1000).toString();
603
- const nonce = (0, node_crypto_1.randomUUID)();
604
- const canonical = canonicalString(method, path, timestamp, nonce, rawBody);
605
- const signature = sign(signingSecret, canonical);
606
- const headers = {
607
- [HEADERS.key]: tokenId,
608
- [HEADERS.timestamp]: timestamp,
609
- [HEADERS.nonce]: nonce,
610
- [HEADERS.signature]: signature,
619
+ const sendSigned = async (rawBody) => {
620
+ const timestamp = Math.floor(Date.now() / 1000).toString();
621
+ const nonce = (0, node_crypto_1.randomUUID)();
622
+ const canonical = canonicalString(method, path, timestamp, nonce, rawBody);
623
+ const signature = sign(signingSecret, canonical);
624
+ const headers = {
625
+ [HEADERS.key]: tokenId,
626
+ [HEADERS.timestamp]: timestamp,
627
+ [HEADERS.nonce]: nonce,
628
+ [HEADERS.signature]: signature,
629
+ };
630
+ if (rawBody)
631
+ headers["content-type"] = "application/json";
632
+ if (idempotencyKey)
633
+ headers["idempotency-key"] = idempotencyKey;
634
+ const res = await this.helpers.httpRequest({
635
+ method,
636
+ url: baseUrl + path,
637
+ headers,
638
+ body: rawBody || undefined,
639
+ json: false,
640
+ returnFullResponse: true,
641
+ ignoreHttpStatusErrors: true,
642
+ });
643
+ const status = res.statusCode;
644
+ const rawRes = res.body;
645
+ let payload;
646
+ try {
647
+ payload = typeof rawRes === "string" ? JSON.parse(rawRes) : rawRes;
648
+ }
649
+ catch {
650
+ payload = { raw: rawRes };
651
+ }
652
+ return { status, payload };
611
653
  };
612
- if (rawBody)
613
- headers["content-type"] = "application/json";
614
- if (idempotencyKey)
615
- headers["idempotency-key"] = idempotencyKey;
616
- const res = await this.helpers.httpRequest({
617
- method,
618
- url: baseUrl + path,
619
- headers,
620
- body: rawBody || undefined,
621
- json: false,
622
- returnFullResponse: true,
623
- ignoreHttpStatusErrors: true,
624
- });
625
- const status = res.statusCode;
626
- const rawRes = res.body;
627
- let payload;
628
- try {
629
- payload = typeof rawRes === "string" ? JSON.parse(rawRes) : rawRes;
630
- }
631
- catch {
632
- payload = { raw: rawRes };
654
+ let rawBody = bodyObj === undefined ? "" : JSON.stringify(bodyObj);
655
+ let { status, payload } = await sendSigned(rawBody);
656
+ // Registrar conversa nunca deve falhar por causa de um pedido_id inválido
657
+ // (a IA pode mandar um UUID que não existe no tenant). Se o Núcleo recusar
658
+ // o pedido, reenvia sem ele — a conversa ainda é registrada.
659
+ if (status === 404 &&
660
+ resource === "conversa" &&
661
+ operation === "registrar" &&
662
+ bodyObj &&
663
+ bodyObj.pedido_id) {
664
+ delete bodyObj.pedido_id;
665
+ rawBody = JSON.stringify(bodyObj);
666
+ ({ status, payload } = await sendSigned(rawBody));
633
667
  }
634
668
  if (status >= 400) {
635
669
  const msg = (payload && payload.error) || `Núcleo respondeu HTTP ${status}.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wondai/n8n-nodes-nucleo",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Node n8n para o Núcleo Wondai — atendimento de IA (cliente, catálogo fuzzy, pedidos) com assinatura HMAC v1. Tenant vem do token (a parede, ADR-013).",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",