bepago 1.0.0 → 1.1.0

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.
Files changed (3) hide show
  1. package/README.md +27 -16
  2. package/index.mjs +31 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,7 +62,9 @@ await bp.payouts.balance();
62
62
  ### Verificar un webhook
63
63
 
64
64
  bePago firma cada webhook con `HMAC-SHA256` sobre `timestamp + body` usando el
65
- secreto del webhook. Verificá la firma antes de procesar el evento:
65
+ secreto del webhook. `verify` chequea la firma **y la frescura del timestamp**
66
+ (rechaza entregas fuera de ±5 minutos por defecto, configurable) y devuelve el
67
+ evento ya parseado, con su `id`:
66
68
 
67
69
  ```js
68
70
  import BePago from 'bepago';
@@ -70,23 +72,32 @@ import BePago from 'bepago';
70
72
  const bp = BePago('sk_live_…');
71
73
 
72
74
  // En tu handler HTTP (body como string crudo, sin re-serializar):
73
- const timestamp = req.headers['x-timestamp'];
74
- const signature = req.headers['x-signature'];
75
- const rawBody = req.rawBody; // el cuerpo tal cual llegó
76
-
77
- const ok = bp.webhooks.verify(
78
- 'whsec_tu_secreto_de_webhook',
79
- timestamp,
80
- rawBody,
81
- signature,
82
- );
83
-
84
- if (!ok) {
85
- return res.status(400).send('firma inválida');
75
+ let evt;
76
+ try {
77
+ evt = bp.webhooks.verify('whsec_tu_secreto_de_webhook', {
78
+ id: req.headers['x-bepago-event-id'],
79
+ timestamp: req.headers['x-webhook-timestamp'],
80
+ signature: req.headers['x-webhook-signature'],
81
+ body: req.rawBody, // el cuerpo tal cual llegó, sin re-serializar
82
+ });
83
+ } catch (e) {
84
+ return res.status(400).send('webhook inválido'); // firma o timestamp fuera de ventana
86
85
  }
87
86
 
88
- const evento = JSON.parse(rawBody);
89
- // procesar evento.type …
87
+ // Anti-replay: cada evento trae un id ESTABLE entre reintentos. Guardá los ids
88
+ // ya procesados y descartá los duplicados (los reintentos reenvían el mismo id
89
+ // con un timestamp nuevo).
90
+ if (await yaProcesado(evt.id)) return res.status(200).end();
91
+ await marcarProcesado(evt.id);
92
+
93
+ // procesar evt.event / evt.data …
94
+ res.status(200).end();
95
+ ```
96
+
97
+ Tolerancia configurable (ej. ±10 minutos):
98
+
99
+ ```js
100
+ bp.webhooks.verify(secret, { id, timestamp, signature, body }, { toleranceSeconds: 600 });
90
101
  ```
91
102
 
92
103
  ## Documentación
package/index.mjs CHANGED
@@ -70,9 +70,37 @@ export default function BePago(secret, opts = {}) {
70
70
  balance: () => request('GET', '/v1/balance'),
71
71
  },
72
72
  webhooks: {
73
- verify: (webhookSecret, timestamp, body, signature) => {
74
- const expected = createHmac('sha256', webhookSecret).update(timestamp + body, 'utf8').digest('hex');
75
- return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
73
+ /**
74
+ * Verifica un webhook de bePago y devuelve el evento parseado.
75
+ *
76
+ * Chequea la firma HMAC y la FRESCURA del timestamp: por defecto rechaza
77
+ * entregas fuera de ±5 minutos (configurable con `toleranceSeconds`). Lanza
78
+ * si la firma no valida o el timestamp está fuera de la ventana.
79
+ *
80
+ * Expone `id` (header X-BePago-Event-Id, estable entre reintentos): guardá
81
+ * los ids ya procesados para descartar duplicados de forma idempotente.
82
+ *
83
+ * const evt = bp.webhooks.verify(secret, {
84
+ * id: req.headers['x-bepago-event-id'],
85
+ * timestamp: req.headers['x-webhook-timestamp'],
86
+ * signature: req.headers['x-webhook-signature'],
87
+ * body: rawBody, // el cuerpo EXACTO recibido, sin reserializar
88
+ * });
89
+ * if (seen.has(evt.id)) return; // duplicado
90
+ * seen.add(evt.id);
91
+ *
92
+ * @returns {{ id: string, event: string, livemode: boolean, data: object }}
93
+ */
94
+ verify(webhookSecret, { id, timestamp, body, signature }, opts = {}) {
95
+ const toleranceMs = (opts.toleranceSeconds ?? 300) * 1000;
96
+ const expected = createHmac('sha256', webhookSecret).update(String(timestamp) + body, 'utf8').digest('hex');
97
+ const a = Buffer.from(expected, 'hex');
98
+ const b = Buffer.from(String(signature ?? ''), 'hex');
99
+ if (a.length !== b.length || !timingSafeEqual(a, b)) throw new Error('bePago webhook: firma inválida');
100
+ const age = Math.abs(Date.now() - Number(timestamp));
101
+ if (!Number.isFinite(age) || age > toleranceMs) throw new Error('bePago webhook: timestamp fuera de la ventana permitida');
102
+ const payload = JSON.parse(body);
103
+ return { id: id ?? payload.id, event: payload.event, livemode: payload.livemode, data: payload.data };
76
104
  },
77
105
  },
78
106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bepago",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "SDK oficial de bePago para Node.js (18+, sin dependencias)",
5
5
  "type": "module",
6
6
  "main": "index.mjs",