@garuhq/node 4.1.0 → 5.0.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/CHANGELOG.md CHANGED
@@ -3,6 +3,55 @@
3
3
  All notable changes to `@garuhq/node` are documented in this file. Format:
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
5
5
 
6
+ ## [5.0.0] — 2026-09-09
7
+
8
+ **Breaking:** requests that omit `idempotencyKey` no longer carry an
9
+ `X-Idempotency-Key` header. No API contract changed and no gateway endpoint
10
+ requires the header, so most integrations need no code change — but if you were
11
+ relying on a key always being present, you must now pass one, which is the only
12
+ way it ever gave you real protection. See below.
13
+
14
+ ### Fixed
15
+
16
+ - **The SDK no longer invents an idempotency key.** Every write that takes
17
+ `idempotencyKey` (`charges.create`, `charges.refund`, `customers.create`,
18
+ `products.create`, `scheduledCharges.create`, `installmentPlans.create`,
19
+ `installmentPlans.requestRefund`, `webhookEvents.resend`) used to generate a
20
+ fresh UUIDv4 when you omitted one. That protected nothing: an idempotency key
21
+ only works if the SAME key comes back on a retry, and a key invented per call
22
+ is different every time. The header is now sent only when you pass a key.
23
+
24
+ This was not theoretical. On 2026-09-08 an integrator's HTTP client timed out
25
+ at 30s on a card charge that was still being created, retried, drew a fresh
26
+ UUIDv4, and charged a real buyer twice.
27
+
28
+ Docstrings that promised "Safe to retry: the same key returns the original
29
+ charge for 24h" were describing a guarantee the code did not provide. They now
30
+ say what actually happens.
31
+
32
+ ### Changed
33
+
34
+ - Behaviour change, no API change: requests where you omit `idempotencyKey` now
35
+ carry no `X-Idempotency-Key` header. Nothing rejects a missing key — the header
36
+ is optional on every gateway endpoint — and the gateway also gained a duplicate
37
+ guard that replays the original charge when the same buyer, product, rail and
38
+ instalment count arrive again within 60 seconds.
39
+
40
+ **To get real protection, pass a key derived from your own domain** so a retry
41
+ reproduces it:
42
+
43
+ ```ts
44
+ await garu.charges.create({
45
+ productId,
46
+ paymentMethod: 'creditCard',
47
+ customer,
48
+ idempotencyKey: `booking:${booking.id}:charge`
49
+ });
50
+ ```
51
+
52
+ - `generateIdempotencyKey()` is still exported and unchanged. It is only useful
53
+ if you store the result and reuse it across retries of the same operation.
54
+
6
55
  ## [4.1.0] — 2026-08-22
7
56
 
8
57
  ### Added
package/dist/index.cjs CHANGED
@@ -188,8 +188,8 @@ function backoffDelay(attempt, retryAfterSec) {
188
188
  function sleep(ms) {
189
189
  return new Promise((resolve) => setTimeout(resolve, ms));
190
190
  }
191
- function generateIdempotencyKey() {
192
- return crypto.randomUUID();
191
+ function idempotencyHeaders(key) {
192
+ return key ? { "X-Idempotency-Key": key } : {};
193
193
  }
194
194
 
195
195
  // src/resources/charges.ts
@@ -201,9 +201,11 @@ var Charges = class {
201
201
  /**
202
202
  * Create a charge (PIX, boleto, or credit card).
203
203
  *
204
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
205
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
206
- * returns the original charge for 24h.
204
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
205
+ * original charge for 24h. Derive it from something stable in your own
206
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
207
+ * no key is sent — the SDK does NOT invent one, because a key generated per
208
+ * call is different every time and protects nothing.
207
209
  *
208
210
  * @example
209
211
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -236,7 +238,6 @@ var Charges = class {
236
238
  * // charge.amount is the base price; charge.chargedTotal is what was charged.
237
239
  */
238
240
  async create(params) {
239
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
240
241
  const body = {
241
242
  productId: params.productId,
242
243
  paymentMethod: params.paymentMethod,
@@ -245,7 +246,7 @@ var Charges = class {
245
246
  if (params.card) body.card = params.card;
246
247
  if (params.checkoutSessionToken) body.checkoutSessionToken = params.checkoutSessionToken;
247
248
  if (params.additionalInfo !== void 0) body.additionalInfo = params.additionalInfo;
248
- return this.post("/api/v1/charges", body, { "X-Idempotency-Key": idempotencyKey });
249
+ return this.post("/api/v1/charges", body, idempotencyHeaders(params.idempotencyKey));
249
250
  }
250
251
  /**
251
252
  * Retrieve a charge by uuid.
@@ -286,23 +287,20 @@ var Charges = class {
286
287
  * settles.
287
288
  *
288
289
  * For Pix/boleto (which open a refund request instead of an automated
289
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
290
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
291
- * card, which reverses automatically and has no manual request to
292
- * duplicate.
290
+ * reversal), pass `idempotencyKey` to make a retry return the original
291
+ * request rather than opening a second one; omit it and no key is sent.
292
+ * Ignored for card, which reverses automatically and has no manual request
293
+ * to duplicate.
293
294
  *
294
295
  * @example
295
296
  * await garu.charges.refund('6f1c9b2e-...'); // full
296
297
  * await garu.charges.refund('6f1c9b2e-...', { amount: 10.0 }); // R$10,00
297
298
  */
298
299
  async refund(uuid, params = {}) {
299
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
300
300
  const body = {};
301
301
  if (params.amount !== void 0) body.amount = params.amount;
302
302
  if (params.reason !== void 0) body.reason = params.reason;
303
- return this.post(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, {
304
- "X-Idempotency-Key": idempotencyKey
305
- });
303
+ return this.post(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, idempotencyHeaders(params.idempotencyKey));
306
304
  }
307
305
  /**
308
306
  * Cancel an unpaid charge.
@@ -343,10 +341,16 @@ var InstallmentPlans = class {
343
341
  }
344
342
  http;
345
343
  /**
346
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
347
- * you don't pass `idempotencyKey`), which matters more here than anywhere
348
- * else in the API: this call registers a REAL boleto at the bank, so a
349
- * blind retry can put two payable barcodes in one buyer's hands.
344
+ * Sell a product as a carnê.
345
+ *
346
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
347
+ * original plan for 24h. Derive it from something stable in your own
348
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
349
+ * no key is sent — the SDK does NOT invent one, because a key generated per
350
+ * call is different every time and protects nothing.
351
+ * This matters more here than anywhere else in the API: the call registers a
352
+ * REAL boleto at the bank, so a retry without a key can put two payable
353
+ * barcodes in one buyer's hands.
350
354
  *
351
355
  * @example
352
356
  * const carne = await garu.installmentPlans.create({
@@ -372,12 +376,11 @@ var InstallmentPlans = class {
372
376
  * });
373
377
  */
374
378
  async create(params) {
375
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
376
379
  const { idempotencyKey: _omit, ...body } = params;
377
380
  return this.http.call(
378
381
  (signal) => this.http.client.POST("/api/v1/installment-plans", {
379
382
  body,
380
- headers: { "X-Idempotency-Key": idempotencyKey },
383
+ headers: idempotencyHeaders(params.idempotencyKey),
381
384
  signal
382
385
  }).then((r) => r)
383
386
  );
@@ -520,10 +523,9 @@ var InstallmentPlans = class {
520
523
  * team. Transfer the money to the buyer yourself, then close it with
521
524
  * `garu.refundRequests.confirm`.
522
525
  *
523
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
524
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
525
- * a second pending request for the same carnê, so this is mainly
526
- * defense-in-depth for the request-in-flight window.
526
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
527
+ * key is sent. The backend already dedupes a second pending request for the
528
+ * same carnê, so this is defense-in-depth rather than the main guard.
527
529
  *
528
530
  * @example
529
531
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -533,12 +535,11 @@ var InstallmentPlans = class {
533
535
  * request.amount; // defaults to everything the carnê collected
534
536
  */
535
537
  async requestRefund(uuid, params = {}) {
536
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
537
538
  const { idempotencyKey: _omit, ...body } = params;
538
539
  return this.http.call(
539
540
  (signal) => this.http.client.POST(`/api/v1/installment-plans/${uuid}/refund-requests`, {
540
541
  body,
541
- headers: { "X-Idempotency-Key": idempotencyKey },
542
+ headers: idempotencyHeaders(params.idempotencyKey),
542
543
  signal
543
544
  }).then((r) => r)
544
545
  );
@@ -646,9 +647,11 @@ var Customers = class {
646
647
  /**
647
648
  * Register a customer for the current seller.
648
649
  *
649
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
650
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
651
- * returns the originally-created/matched customer for 24h.
650
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
651
+ * original customer for 24h. Derive it from something stable in your own
652
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
653
+ * no key is sent — the SDK does NOT invent one, because a key generated per
654
+ * call is different every time and protects nothing.
652
655
  *
653
656
  * @example
654
657
  * const customer = await garu.customers.create({
@@ -661,12 +664,11 @@ var Customers = class {
661
664
  * customer.uuid;
662
665
  */
663
666
  async create(params) {
664
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
665
667
  const { idempotencyKey: _omit, ...body } = params;
666
668
  return this.http.call(
667
669
  (signal) => this.http.client.POST("/api/v1/customers", {
668
670
  body,
669
- headers: { "X-Idempotency-Key": idempotencyKey },
671
+ headers: idempotencyHeaders(params.idempotencyKey),
670
672
  signal
671
673
  }).then((r) => r)
672
674
  );
@@ -911,10 +913,11 @@ var Products = class {
911
913
  * product (HTTP 201). Only `name` is required; everything else falls back
912
914
  * to seller/server defaults.
913
915
  *
914
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
915
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
916
- * retry on transient failures safe: a retried POST returns the original
917
- * product instead of creating a duplicate.
916
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
917
+ * original product for 24h. Derive it from something stable in your own
918
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
919
+ * no key is sent — the SDK does NOT invent one, because a key generated per
920
+ * call is different every time and protects nothing.
918
921
  *
919
922
  * @example
920
923
  * const product = await garu.products.create({
@@ -930,11 +933,10 @@ var Products = class {
930
933
  */
931
934
  async create(params) {
932
935
  const { idempotencyKey, ...body } = params;
933
- const key = idempotencyKey ?? generateIdempotencyKey();
934
936
  return this.http.call(
935
937
  (signal) => this.http.client.POST("/api/v1/products", {
936
938
  body,
937
- headers: { "X-Idempotency-Key": key },
939
+ headers: idempotencyHeaders(idempotencyKey),
938
940
  signal
939
941
  }).then((r) => r)
940
942
  );
@@ -970,10 +972,15 @@ var ScheduledCharges = class {
970
972
  }
971
973
  http;
972
974
  /**
973
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
974
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
975
- * UUIDv4. Safe to retry: the same key returns the originally-created
976
- * series for 24h instead of double-booking recurring billing.
975
+ * Create a new scheduled charge.
976
+ *
977
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
978
+ * original series for 24h. Derive it from something stable in your own
979
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
980
+ * no key is sent — the SDK does NOT invent one, because a key generated per
981
+ * call is different every time and protects nothing.
982
+ * Worth the effort here: without a key, a retry double-books recurring
983
+ * billing for the same customer.
977
984
  *
978
985
  * @example
979
986
  * const charge = await garu.scheduledCharges.create({
@@ -1000,12 +1007,11 @@ var ScheduledCharges = class {
1000
1007
  * });
1001
1008
  */
1002
1009
  async create(params) {
1003
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
1004
1010
  const { idempotencyKey: _omit, ...body } = params;
1005
1011
  return this.http.call(
1006
1012
  (signal) => this.http.client.POST("/api/v1/scheduled-charges", {
1007
1013
  body,
1008
- headers: { "X-Idempotency-Key": idempotencyKey },
1014
+ headers: idempotencyHeaders(params.idempotencyKey),
1009
1015
  signal
1010
1016
  }).then((r) => r)
1011
1017
  );
@@ -1394,11 +1400,10 @@ var WebhookEvents = class {
1394
1400
  * original — distinguishable both by the `resend_` prefix and by reading
1395
1401
  * the response payload's `manualResendOf` field.
1396
1402
  *
1397
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1398
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1399
- * `/resend` calls against it, so retrying this call from your own code
1400
- * after a network failure can create more than one clone — pair it with
1401
- * your own retry-suppression if that matters for your integration.
1403
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1404
+ * Either way the gateway does not currently deduplicate `/resend` calls
1405
+ * against it, so retrying after a network failure can create more than one
1406
+ * clone pair it with your own retry-suppression if that matters.
1402
1407
  *
1403
1408
  * Returns the *clone* event (new uuid), not the original. The original is
1404
1409
  * unchanged on the server.
@@ -1410,11 +1415,10 @@ var WebhookEvents = class {
1410
1415
  * clone.manualResendOf === event.uuid; // true — points back at the source
1411
1416
  */
1412
1417
  async resend(uuid, params = {}) {
1413
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
1414
1418
  return this.http.call(
1415
1419
  (signal) => this.http.client.POST(`/api/v1/webhook-events/${uuid}/resend`, {
1416
1420
  body: {},
1417
- headers: { "X-Idempotency-Key": idempotencyKey },
1421
+ headers: idempotencyHeaders(params.idempotencyKey),
1418
1422
  signal
1419
1423
  }).then((r) => r)
1420
1424
  );
package/dist/index.d.cts CHANGED
@@ -1100,9 +1100,11 @@ declare class Charges {
1100
1100
  /**
1101
1101
  * Create a charge (PIX, boleto, or credit card).
1102
1102
  *
1103
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1104
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1105
- * returns the original charge for 24h.
1103
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1104
+ * original charge for 24h. Derive it from something stable in your own
1105
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1106
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1107
+ * call is different every time and protects nothing.
1106
1108
  *
1107
1109
  * @example
1108
1110
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -1159,10 +1161,10 @@ declare class Charges {
1159
1161
  * settles.
1160
1162
  *
1161
1163
  * For Pix/boleto (which open a refund request instead of an automated
1162
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
1163
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
1164
- * card, which reverses automatically and has no manual request to
1165
- * duplicate.
1164
+ * reversal), pass `idempotencyKey` to make a retry return the original
1165
+ * request rather than opening a second one; omit it and no key is sent.
1166
+ * Ignored for card, which reverses automatically and has no manual request
1167
+ * to duplicate.
1166
1168
  *
1167
1169
  * @example
1168
1170
  * await garu.charges.refund('6f1c9b2e-...'); // full
@@ -1196,10 +1198,16 @@ declare class InstallmentPlans {
1196
1198
  private readonly http;
1197
1199
  constructor(http: HttpClient);
1198
1200
  /**
1199
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
1200
- * you don't pass `idempotencyKey`), which matters more here than anywhere
1201
- * else in the API: this call registers a REAL boleto at the bank, so a
1202
- * blind retry can put two payable barcodes in one buyer's hands.
1201
+ * Sell a product as a carnê.
1202
+ *
1203
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1204
+ * original plan for 24h. Derive it from something stable in your own
1205
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1206
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1207
+ * call is different every time and protects nothing.
1208
+ * This matters more here than anywhere else in the API: the call registers a
1209
+ * REAL boleto at the bank, so a retry without a key can put two payable
1210
+ * barcodes in one buyer's hands.
1203
1211
  *
1204
1212
  * @example
1205
1213
  * const carne = await garu.installmentPlans.create({
@@ -1310,10 +1318,9 @@ declare class InstallmentPlans {
1310
1318
  * team. Transfer the money to the buyer yourself, then close it with
1311
1319
  * `garu.refundRequests.confirm`.
1312
1320
  *
1313
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1314
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
1315
- * a second pending request for the same carnê, so this is mainly
1316
- * defense-in-depth for the request-in-flight window.
1321
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
1322
+ * key is sent. The backend already dedupes a second pending request for the
1323
+ * same carnê, so this is defense-in-depth rather than the main guard.
1317
1324
  *
1318
1325
  * @example
1319
1326
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -1404,9 +1411,11 @@ declare class Customers {
1404
1411
  /**
1405
1412
  * Register a customer for the current seller.
1406
1413
  *
1407
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1408
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1409
- * returns the originally-created/matched customer for 24h.
1414
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1415
+ * original customer for 24h. Derive it from something stable in your own
1416
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1417
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1418
+ * call is different every time and protects nothing.
1410
1419
  *
1411
1420
  * @example
1412
1421
  * const customer = await garu.customers.create({
@@ -1576,10 +1585,11 @@ declare class Products {
1576
1585
  * product (HTTP 201). Only `name` is required; everything else falls back
1577
1586
  * to seller/server defaults.
1578
1587
  *
1579
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
1580
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
1581
- * retry on transient failures safe: a retried POST returns the original
1582
- * product instead of creating a duplicate.
1588
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1589
+ * original product for 24h. Derive it from something stable in your own
1590
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1591
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1592
+ * call is different every time and protects nothing.
1583
1593
  *
1584
1594
  * @example
1585
1595
  * const product = await garu.products.create({
@@ -1629,10 +1639,15 @@ declare class ScheduledCharges {
1629
1639
  private readonly http;
1630
1640
  constructor(http: HttpClient);
1631
1641
  /**
1632
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
1633
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
1634
- * UUIDv4. Safe to retry: the same key returns the originally-created
1635
- * series for 24h instead of double-booking recurring billing.
1642
+ * Create a new scheduled charge.
1643
+ *
1644
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1645
+ * original series for 24h. Derive it from something stable in your own
1646
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1647
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1648
+ * call is different every time and protects nothing.
1649
+ * Worth the effort here: without a key, a retry double-books recurring
1650
+ * billing for the same customer.
1636
1651
  *
1637
1652
  * @example
1638
1653
  * const charge = await garu.scheduledCharges.create({
@@ -1899,11 +1914,10 @@ declare class WebhookEvents {
1899
1914
  * original — distinguishable both by the `resend_` prefix and by reading
1900
1915
  * the response payload's `manualResendOf` field.
1901
1916
  *
1902
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1903
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1904
- * `/resend` calls against it, so retrying this call from your own code
1905
- * after a network failure can create more than one clone — pair it with
1906
- * your own retry-suppression if that matters for your integration.
1917
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1918
+ * Either way the gateway does not currently deduplicate `/resend` calls
1919
+ * against it, so retrying after a network failure can create more than one
1920
+ * clone pair it with your own retry-suppression if that matters.
1907
1921
  *
1908
1922
  * Returns the *clone* event (new uuid), not the original. The original is
1909
1923
  * unchanged on the server.
package/dist/index.d.ts CHANGED
@@ -1100,9 +1100,11 @@ declare class Charges {
1100
1100
  /**
1101
1101
  * Create a charge (PIX, boleto, or credit card).
1102
1102
  *
1103
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1104
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1105
- * returns the original charge for 24h.
1103
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1104
+ * original charge for 24h. Derive it from something stable in your own
1105
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1106
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1107
+ * call is different every time and protects nothing.
1106
1108
  *
1107
1109
  * @example
1108
1110
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -1159,10 +1161,10 @@ declare class Charges {
1159
1161
  * settles.
1160
1162
  *
1161
1163
  * For Pix/boleto (which open a refund request instead of an automated
1162
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
1163
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
1164
- * card, which reverses automatically and has no manual request to
1165
- * duplicate.
1164
+ * reversal), pass `idempotencyKey` to make a retry return the original
1165
+ * request rather than opening a second one; omit it and no key is sent.
1166
+ * Ignored for card, which reverses automatically and has no manual request
1167
+ * to duplicate.
1166
1168
  *
1167
1169
  * @example
1168
1170
  * await garu.charges.refund('6f1c9b2e-...'); // full
@@ -1196,10 +1198,16 @@ declare class InstallmentPlans {
1196
1198
  private readonly http;
1197
1199
  constructor(http: HttpClient);
1198
1200
  /**
1199
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
1200
- * you don't pass `idempotencyKey`), which matters more here than anywhere
1201
- * else in the API: this call registers a REAL boleto at the bank, so a
1202
- * blind retry can put two payable barcodes in one buyer's hands.
1201
+ * Sell a product as a carnê.
1202
+ *
1203
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1204
+ * original plan for 24h. Derive it from something stable in your own
1205
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1206
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1207
+ * call is different every time and protects nothing.
1208
+ * This matters more here than anywhere else in the API: the call registers a
1209
+ * REAL boleto at the bank, so a retry without a key can put two payable
1210
+ * barcodes in one buyer's hands.
1203
1211
  *
1204
1212
  * @example
1205
1213
  * const carne = await garu.installmentPlans.create({
@@ -1310,10 +1318,9 @@ declare class InstallmentPlans {
1310
1318
  * team. Transfer the money to the buyer yourself, then close it with
1311
1319
  * `garu.refundRequests.confirm`.
1312
1320
  *
1313
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1314
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
1315
- * a second pending request for the same carnê, so this is mainly
1316
- * defense-in-depth for the request-in-flight window.
1321
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
1322
+ * key is sent. The backend already dedupes a second pending request for the
1323
+ * same carnê, so this is defense-in-depth rather than the main guard.
1317
1324
  *
1318
1325
  * @example
1319
1326
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -1404,9 +1411,11 @@ declare class Customers {
1404
1411
  /**
1405
1412
  * Register a customer for the current seller.
1406
1413
  *
1407
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1408
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1409
- * returns the originally-created/matched customer for 24h.
1414
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1415
+ * original customer for 24h. Derive it from something stable in your own
1416
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1417
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1418
+ * call is different every time and protects nothing.
1410
1419
  *
1411
1420
  * @example
1412
1421
  * const customer = await garu.customers.create({
@@ -1576,10 +1585,11 @@ declare class Products {
1576
1585
  * product (HTTP 201). Only `name` is required; everything else falls back
1577
1586
  * to seller/server defaults.
1578
1587
  *
1579
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
1580
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
1581
- * retry on transient failures safe: a retried POST returns the original
1582
- * product instead of creating a duplicate.
1588
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1589
+ * original product for 24h. Derive it from something stable in your own
1590
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1591
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1592
+ * call is different every time and protects nothing.
1583
1593
  *
1584
1594
  * @example
1585
1595
  * const product = await garu.products.create({
@@ -1629,10 +1639,15 @@ declare class ScheduledCharges {
1629
1639
  private readonly http;
1630
1640
  constructor(http: HttpClient);
1631
1641
  /**
1632
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
1633
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
1634
- * UUIDv4. Safe to retry: the same key returns the originally-created
1635
- * series for 24h instead of double-booking recurring billing.
1642
+ * Create a new scheduled charge.
1643
+ *
1644
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1645
+ * original series for 24h. Derive it from something stable in your own
1646
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1647
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1648
+ * call is different every time and protects nothing.
1649
+ * Worth the effort here: without a key, a retry double-books recurring
1650
+ * billing for the same customer.
1636
1651
  *
1637
1652
  * @example
1638
1653
  * const charge = await garu.scheduledCharges.create({
@@ -1899,11 +1914,10 @@ declare class WebhookEvents {
1899
1914
  * original — distinguishable both by the `resend_` prefix and by reading
1900
1915
  * the response payload's `manualResendOf` field.
1901
1916
  *
1902
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1903
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1904
- * `/resend` calls against it, so retrying this call from your own code
1905
- * after a network failure can create more than one clone — pair it with
1906
- * your own retry-suppression if that matters for your integration.
1917
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1918
+ * Either way the gateway does not currently deduplicate `/resend` calls
1919
+ * against it, so retrying after a network failure can create more than one
1920
+ * clone pair it with your own retry-suppression if that matters.
1907
1921
  *
1908
1922
  * Returns the *clone* event (new uuid), not the original. The original is
1909
1923
  * unchanged on the server.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import createClient from 'openapi-fetch';
2
- import { createHmac, timingSafeEqual, randomUUID } from 'crypto';
2
+ import { createHmac, timingSafeEqual } from 'crypto';
3
3
 
4
4
  // src/http.ts
5
5
 
@@ -182,8 +182,8 @@ function backoffDelay(attempt, retryAfterSec) {
182
182
  function sleep(ms) {
183
183
  return new Promise((resolve) => setTimeout(resolve, ms));
184
184
  }
185
- function generateIdempotencyKey() {
186
- return randomUUID();
185
+ function idempotencyHeaders(key) {
186
+ return key ? { "X-Idempotency-Key": key } : {};
187
187
  }
188
188
 
189
189
  // src/resources/charges.ts
@@ -195,9 +195,11 @@ var Charges = class {
195
195
  /**
196
196
  * Create a charge (PIX, boleto, or credit card).
197
197
  *
198
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
199
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
200
- * returns the original charge for 24h.
198
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
199
+ * original charge for 24h. Derive it from something stable in your own
200
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
201
+ * no key is sent — the SDK does NOT invent one, because a key generated per
202
+ * call is different every time and protects nothing.
201
203
  *
202
204
  * @example
203
205
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -230,7 +232,6 @@ var Charges = class {
230
232
  * // charge.amount is the base price; charge.chargedTotal is what was charged.
231
233
  */
232
234
  async create(params) {
233
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
234
235
  const body = {
235
236
  productId: params.productId,
236
237
  paymentMethod: params.paymentMethod,
@@ -239,7 +240,7 @@ var Charges = class {
239
240
  if (params.card) body.card = params.card;
240
241
  if (params.checkoutSessionToken) body.checkoutSessionToken = params.checkoutSessionToken;
241
242
  if (params.additionalInfo !== void 0) body.additionalInfo = params.additionalInfo;
242
- return this.post("/api/v1/charges", body, { "X-Idempotency-Key": idempotencyKey });
243
+ return this.post("/api/v1/charges", body, idempotencyHeaders(params.idempotencyKey));
243
244
  }
244
245
  /**
245
246
  * Retrieve a charge by uuid.
@@ -280,23 +281,20 @@ var Charges = class {
280
281
  * settles.
281
282
  *
282
283
  * For Pix/boleto (which open a refund request instead of an automated
283
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
284
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
285
- * card, which reverses automatically and has no manual request to
286
- * duplicate.
284
+ * reversal), pass `idempotencyKey` to make a retry return the original
285
+ * request rather than opening a second one; omit it and no key is sent.
286
+ * Ignored for card, which reverses automatically and has no manual request
287
+ * to duplicate.
287
288
  *
288
289
  * @example
289
290
  * await garu.charges.refund('6f1c9b2e-...'); // full
290
291
  * await garu.charges.refund('6f1c9b2e-...', { amount: 10.0 }); // R$10,00
291
292
  */
292
293
  async refund(uuid, params = {}) {
293
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
294
294
  const body = {};
295
295
  if (params.amount !== void 0) body.amount = params.amount;
296
296
  if (params.reason !== void 0) body.reason = params.reason;
297
- return this.post(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, {
298
- "X-Idempotency-Key": idempotencyKey
299
- });
297
+ return this.post(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, idempotencyHeaders(params.idempotencyKey));
300
298
  }
301
299
  /**
302
300
  * Cancel an unpaid charge.
@@ -337,10 +335,16 @@ var InstallmentPlans = class {
337
335
  }
338
336
  http;
339
337
  /**
340
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
341
- * you don't pass `idempotencyKey`), which matters more here than anywhere
342
- * else in the API: this call registers a REAL boleto at the bank, so a
343
- * blind retry can put two payable barcodes in one buyer's hands.
338
+ * Sell a product as a carnê.
339
+ *
340
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
341
+ * original plan for 24h. Derive it from something stable in your own
342
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
343
+ * no key is sent — the SDK does NOT invent one, because a key generated per
344
+ * call is different every time and protects nothing.
345
+ * This matters more here than anywhere else in the API: the call registers a
346
+ * REAL boleto at the bank, so a retry without a key can put two payable
347
+ * barcodes in one buyer's hands.
344
348
  *
345
349
  * @example
346
350
  * const carne = await garu.installmentPlans.create({
@@ -366,12 +370,11 @@ var InstallmentPlans = class {
366
370
  * });
367
371
  */
368
372
  async create(params) {
369
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
370
373
  const { idempotencyKey: _omit, ...body } = params;
371
374
  return this.http.call(
372
375
  (signal) => this.http.client.POST("/api/v1/installment-plans", {
373
376
  body,
374
- headers: { "X-Idempotency-Key": idempotencyKey },
377
+ headers: idempotencyHeaders(params.idempotencyKey),
375
378
  signal
376
379
  }).then((r) => r)
377
380
  );
@@ -514,10 +517,9 @@ var InstallmentPlans = class {
514
517
  * team. Transfer the money to the buyer yourself, then close it with
515
518
  * `garu.refundRequests.confirm`.
516
519
  *
517
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
518
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
519
- * a second pending request for the same carnê, so this is mainly
520
- * defense-in-depth for the request-in-flight window.
520
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
521
+ * key is sent. The backend already dedupes a second pending request for the
522
+ * same carnê, so this is defense-in-depth rather than the main guard.
521
523
  *
522
524
  * @example
523
525
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -527,12 +529,11 @@ var InstallmentPlans = class {
527
529
  * request.amount; // defaults to everything the carnê collected
528
530
  */
529
531
  async requestRefund(uuid, params = {}) {
530
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
531
532
  const { idempotencyKey: _omit, ...body } = params;
532
533
  return this.http.call(
533
534
  (signal) => this.http.client.POST(`/api/v1/installment-plans/${uuid}/refund-requests`, {
534
535
  body,
535
- headers: { "X-Idempotency-Key": idempotencyKey },
536
+ headers: idempotencyHeaders(params.idempotencyKey),
536
537
  signal
537
538
  }).then((r) => r)
538
539
  );
@@ -640,9 +641,11 @@ var Customers = class {
640
641
  /**
641
642
  * Register a customer for the current seller.
642
643
  *
643
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
644
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
645
- * returns the originally-created/matched customer for 24h.
644
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
645
+ * original customer for 24h. Derive it from something stable in your own
646
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
647
+ * no key is sent — the SDK does NOT invent one, because a key generated per
648
+ * call is different every time and protects nothing.
646
649
  *
647
650
  * @example
648
651
  * const customer = await garu.customers.create({
@@ -655,12 +658,11 @@ var Customers = class {
655
658
  * customer.uuid;
656
659
  */
657
660
  async create(params) {
658
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
659
661
  const { idempotencyKey: _omit, ...body } = params;
660
662
  return this.http.call(
661
663
  (signal) => this.http.client.POST("/api/v1/customers", {
662
664
  body,
663
- headers: { "X-Idempotency-Key": idempotencyKey },
665
+ headers: idempotencyHeaders(params.idempotencyKey),
664
666
  signal
665
667
  }).then((r) => r)
666
668
  );
@@ -905,10 +907,11 @@ var Products = class {
905
907
  * product (HTTP 201). Only `name` is required; everything else falls back
906
908
  * to seller/server defaults.
907
909
  *
908
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
909
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
910
- * retry on transient failures safe: a retried POST returns the original
911
- * product instead of creating a duplicate.
910
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
911
+ * original product for 24h. Derive it from something stable in your own
912
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
913
+ * no key is sent — the SDK does NOT invent one, because a key generated per
914
+ * call is different every time and protects nothing.
912
915
  *
913
916
  * @example
914
917
  * const product = await garu.products.create({
@@ -924,11 +927,10 @@ var Products = class {
924
927
  */
925
928
  async create(params) {
926
929
  const { idempotencyKey, ...body } = params;
927
- const key = idempotencyKey ?? generateIdempotencyKey();
928
930
  return this.http.call(
929
931
  (signal) => this.http.client.POST("/api/v1/products", {
930
932
  body,
931
- headers: { "X-Idempotency-Key": key },
933
+ headers: idempotencyHeaders(idempotencyKey),
932
934
  signal
933
935
  }).then((r) => r)
934
936
  );
@@ -964,10 +966,15 @@ var ScheduledCharges = class {
964
966
  }
965
967
  http;
966
968
  /**
967
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
968
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
969
- * UUIDv4. Safe to retry: the same key returns the originally-created
970
- * series for 24h instead of double-booking recurring billing.
969
+ * Create a new scheduled charge.
970
+ *
971
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
972
+ * original series for 24h. Derive it from something stable in your own
973
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
974
+ * no key is sent — the SDK does NOT invent one, because a key generated per
975
+ * call is different every time and protects nothing.
976
+ * Worth the effort here: without a key, a retry double-books recurring
977
+ * billing for the same customer.
971
978
  *
972
979
  * @example
973
980
  * const charge = await garu.scheduledCharges.create({
@@ -994,12 +1001,11 @@ var ScheduledCharges = class {
994
1001
  * });
995
1002
  */
996
1003
  async create(params) {
997
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
998
1004
  const { idempotencyKey: _omit, ...body } = params;
999
1005
  return this.http.call(
1000
1006
  (signal) => this.http.client.POST("/api/v1/scheduled-charges", {
1001
1007
  body,
1002
- headers: { "X-Idempotency-Key": idempotencyKey },
1008
+ headers: idempotencyHeaders(params.idempotencyKey),
1003
1009
  signal
1004
1010
  }).then((r) => r)
1005
1011
  );
@@ -1388,11 +1394,10 @@ var WebhookEvents = class {
1388
1394
  * original — distinguishable both by the `resend_` prefix and by reading
1389
1395
  * the response payload's `manualResendOf` field.
1390
1396
  *
1391
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1392
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1393
- * `/resend` calls against it, so retrying this call from your own code
1394
- * after a network failure can create more than one clone — pair it with
1395
- * your own retry-suppression if that matters for your integration.
1397
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1398
+ * Either way the gateway does not currently deduplicate `/resend` calls
1399
+ * against it, so retrying after a network failure can create more than one
1400
+ * clone pair it with your own retry-suppression if that matters.
1396
1401
  *
1397
1402
  * Returns the *clone* event (new uuid), not the original. The original is
1398
1403
  * unchanged on the server.
@@ -1404,11 +1409,10 @@ var WebhookEvents = class {
1404
1409
  * clone.manualResendOf === event.uuid; // true — points back at the source
1405
1410
  */
1406
1411
  async resend(uuid, params = {}) {
1407
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
1408
1412
  return this.http.call(
1409
1413
  (signal) => this.http.client.POST(`/api/v1/webhook-events/${uuid}/resend`, {
1410
1414
  body: {},
1411
- headers: { "X-Idempotency-Key": idempotencyKey },
1415
+ headers: idempotencyHeaders(params.idempotencyKey),
1412
1416
  signal
1413
1417
  }).then((r) => r)
1414
1418
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garuhq/node",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Official Node.js / TypeScript SDK for the Garu payment gateway.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://garu.com.br",