@bepago/sdk 1.1.0 → 1.2.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.
- package/index.mjs +117 -1
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -56,12 +56,28 @@ export default function BePago(secret, opts = {}) {
|
|
|
56
56
|
return data;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const api = {
|
|
60
60
|
charges: {
|
|
61
61
|
create: (params) => request('POST', '/v1/charges', params),
|
|
62
62
|
get: (ref) => request('GET', `/v1/charges/${encodeURIComponent(ref)}`),
|
|
63
63
|
refund: (ref, reason) => request('POST', `/v1/charges/${encodeURIComponent(ref)}/refund`, { reason }),
|
|
64
64
|
balance: () => request('GET', '/v1/charges/balance'),
|
|
65
|
+
/**
|
|
66
|
+
* Confirma el estado REAL de un cobro consultando la API (fuente de verdad).
|
|
67
|
+
* Usalo para decidir si entregás: nunca confíes en el navegador ni en el
|
|
68
|
+
* iframe. Devuelve `{ paid, status, charge }` — `paid` es true solo si el
|
|
69
|
+
* cobro figura `paid` en bePago.
|
|
70
|
+
*
|
|
71
|
+
* const { paid, charge } = await bp.charges.confirm(orderId);
|
|
72
|
+
* if (paid && charge.amount === order.amount) entregar();
|
|
73
|
+
*
|
|
74
|
+
* @returns {Promise<{ paid: boolean, status: string, charge: object }>}
|
|
75
|
+
*/
|
|
76
|
+
async confirm(ref) {
|
|
77
|
+
const charge = await request('GET', `/v1/charges/${encodeURIComponent(ref)}`);
|
|
78
|
+
const status = charge?.status ?? charge?.data?.status;
|
|
79
|
+
return { paid: status === 'paid', status, charge };
|
|
80
|
+
},
|
|
65
81
|
},
|
|
66
82
|
payouts: {
|
|
67
83
|
create: (params, idempotencyKey) => request('POST', '/v1/payouts', params, { idempotencyKey }),
|
|
@@ -102,6 +118,106 @@ export default function BePago(secret, opts = {}) {
|
|
|
102
118
|
const payload = JSON.parse(body);
|
|
103
119
|
return { id: id ?? payload.id, event: payload.event, livemode: payload.livemode, data: payload.data };
|
|
104
120
|
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Crea un manejador de webhooks seguro-por-defecto. Verifica firma y
|
|
124
|
+
* frescura (±5 min), descarta duplicados por X-BePago-Event-Id y SOLO
|
|
125
|
+
* invoca `onPaid` con eventos `charge.paid` válidos y no vistos. Siempre
|
|
126
|
+
* responde 200 a entregas válidas (aunque sean duplicadas, para que
|
|
127
|
+
* bePago deje de reintentar) y 400 a firmas/timestamps inválidos.
|
|
128
|
+
*
|
|
129
|
+
* No montes el parser de JSON del framework delante: el handler necesita
|
|
130
|
+
* el cuerpo CRUDO (string/Buffer) para validar la firma byte a byte. Usá
|
|
131
|
+
* los adapters `bp.webhooks.express()` / `bp.webhooks.fastify()` que ya
|
|
132
|
+
* resuelven eso.
|
|
133
|
+
*
|
|
134
|
+
* const wh = bp.webhooks.handler({
|
|
135
|
+
* secret: process.env.BEPAGO_WEBHOOK_SECRET,
|
|
136
|
+
* onPaid: async ({ reference, data }) => { await entregar(reference); },
|
|
137
|
+
* onEvent: async (evt) => {}, // opcional: todos los eventos
|
|
138
|
+
* store: myRedisStore, // opcional: persistir ids vistos
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* `store` por defecto es un Set en memoria. Para no entregar dos veces
|
|
142
|
+
* tras un reinicio, pasá un store con `has(id)` y `add(id)` (pueden ser
|
|
143
|
+
* async) respaldado por tu base.
|
|
144
|
+
*
|
|
145
|
+
* @returns {{ process(input): Promise<{status:number, body:string, duplicate:boolean, ok:boolean}>, express(): Function, fastify(): Function }}
|
|
146
|
+
*/
|
|
147
|
+
handler(config = {}) {
|
|
148
|
+
const { secret, onPaid, onEvent } = config;
|
|
149
|
+
if (!secret) throw new Error('bePago webhooks.handler: falta `secret`');
|
|
150
|
+
const seen = config.store ?? defaultSeenStore();
|
|
151
|
+
|
|
152
|
+
async function process({ headers = {}, body }) {
|
|
153
|
+
const h = lowerHeaders(headers);
|
|
154
|
+
const raw = toRawBody(body);
|
|
155
|
+
let evt;
|
|
156
|
+
try {
|
|
157
|
+
evt = api.webhooks.verify(secret, {
|
|
158
|
+
id: h['x-bepago-event-id'],
|
|
159
|
+
timestamp: h['x-webhook-timestamp'],
|
|
160
|
+
signature: h['x-webhook-signature'],
|
|
161
|
+
body: raw,
|
|
162
|
+
}, config);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return { status: 400, body: JSON.stringify({ error: 'invalid_signature' }), duplicate: false, ok: false };
|
|
165
|
+
}
|
|
166
|
+
if (await seen.has(evt.id)) {
|
|
167
|
+
return { status: 200, body: JSON.stringify({ received: true, duplicate: true }), duplicate: true, ok: true };
|
|
168
|
+
}
|
|
169
|
+
await seen.add(evt.id);
|
|
170
|
+
try {
|
|
171
|
+
if (typeof onEvent === 'function') await onEvent(evt);
|
|
172
|
+
if (evt.event === 'charge.paid' && typeof onPaid === 'function') {
|
|
173
|
+
await onPaid({ reference: evt.data?.reference, data: evt.data, event: evt });
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return { status: 500, body: JSON.stringify({ error: 'handler_error' }), duplicate: false, ok: false };
|
|
177
|
+
}
|
|
178
|
+
return { status: 200, body: JSON.stringify({ received: true }), duplicate: false, ok: true };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
process,
|
|
183
|
+
/** Middleware Express. Montalo con el raw body: app.post(url, express.raw({type:'*/*'}), wh.express()) */
|
|
184
|
+
express() {
|
|
185
|
+
return async (req, res) => {
|
|
186
|
+
const r = await process({ headers: req.headers, body: req.body });
|
|
187
|
+
res.status(r.status).set('content-type', 'application/json').send(r.body);
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
/** Handler Fastify. Registrá un parser que deje el raw body (ver ejemplo en /docs). */
|
|
191
|
+
fastify() {
|
|
192
|
+
return async (req, reply) => {
|
|
193
|
+
const r = await process({ headers: req.headers, body: req.rawBody ?? req.body });
|
|
194
|
+
reply.code(r.status).header('content-type', 'application/json').send(r.body);
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
},
|
|
105
199
|
},
|
|
106
200
|
};
|
|
201
|
+
return api;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Store de ids vistos en memoria (por defecto). Para producción multi-proceso, pasá uno respaldado por tu base. */
|
|
205
|
+
function defaultSeenStore() {
|
|
206
|
+
const set = new Set();
|
|
207
|
+
return { has: (id) => set.has(id), add: (id) => { set.add(id); } };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function lowerHeaders(headers) {
|
|
211
|
+
const out = {};
|
|
212
|
+
for (const [k, v] of Object.entries(headers || {})) out[String(k).toLowerCase()] = Array.isArray(v) ? v[0] : v;
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function toRawBody(body) {
|
|
217
|
+
if (body == null) return '';
|
|
218
|
+
if (typeof body === 'string') return body;
|
|
219
|
+
if (Buffer.isBuffer(body)) return body.toString('utf8');
|
|
220
|
+
// Último recurso: si el framework ya parseó a objeto, reserializar romperá la
|
|
221
|
+
// firma. Lo exponemos igual para que falle ruidoso y no silencioso.
|
|
222
|
+
return typeof body === 'object' ? JSON.stringify(body) : String(body);
|
|
107
223
|
}
|