@garuhq/node 0.1.1 → 0.3.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,20 @@
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
+ ## [0.3.0] — 2026-04-28
7
+
8
+ ### Added
9
+
10
+ - `products` resource on the `Garu` client.
11
+ - `products.list({ page, limit, search, tab })` — paginated listing of the
12
+ authenticated seller's products (`GET /api/products/seller`).
13
+ - `products.get(uuid)` — fetch a single product by UUID
14
+ (`GET /api/products/uuid/{uuid}`). The UUID is the same identifier
15
+ accepted by `charges.create({ productId })`, so `products.list` is
16
+ the discovery path before creating a charge.
17
+ - `Product`, `ProductList`, `ListProductsParams` types exported from the
18
+ package root.
19
+
6
20
  ## [0.1.1] — 2026-04-08
7
21
 
8
22
  ### Security
package/README.md CHANGED
@@ -1,15 +1,32 @@
1
+ <div align="center">
2
+
1
3
  # @garuhq/node
2
4
 
3
- Official Node.js / TypeScript SDK for the [Garu](https://garu.com.br) payment gateway.
5
+ The official Node.js / TypeScript SDK for the [Garu](https://garu.com.br) payment gateway.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@garuhq/node.svg)](https://www.npmjs.com/package/@garuhq/node)
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/Garu-Pagamentos/garu-node/release.yml?label=CI)](https://github.com/Garu-Pagamentos/garu-node/actions)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
11
+
12
+ <p>
13
+ <a href="#quickstart">Quickstart</a> ·
14
+ <a href="#charges">Charges</a> ·
15
+ <a href="#customers">Customers</a> ·
16
+ <a href="#webhooks">Webhooks</a> ·
17
+ <a href="#error-handling">Errors</a>
18
+ </p>
19
+
20
+ </div>
4
21
 
5
- - **Typed end-to-end** — wire types are generated from the backend's OpenAPI spec, so the
6
- SDK can never drift from the API. Run `npm run generate` to refresh.
7
- - **LLM-optimized** every public method has a JSDoc `@example` so agents can autocomplete a
8
- working call from partial input.
9
- - **Tiny footprint** — one runtime dependency ([`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/),
10
- ~4 KB). Node.js ≥ 18, native `fetch`, native `crypto`.
11
- - **Safe to retry** — automatic idempotency keys on every mutation, exponential backoff with
12
- full jitter, honors `Retry-After`.
22
+ ---
23
+
24
+ Brazilian payments (PIX, credit card, boleto) in a few lines of code.
25
+
26
+ - **Typed end-to-end** — wire types generated from the backend's OpenAPI spec; the SDK can never drift from the API.
27
+ - **Tiny footprint** — one runtime dependency ([`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/), ~4 KB). Native `fetch`, native `crypto`.
28
+ - **Safe to retry** — automatic idempotency keys on every mutation, exponential backoff with full jitter, honors `Retry-After`.
29
+ - **LLM-friendly** every public method has JSDoc `@example` blocks for agent autocomplete.
13
30
  - **ESM + CJS** dual build.
14
31
 
15
32
  ## Install
@@ -22,7 +39,7 @@ pnpm add @garuhq/node
22
39
  yarn add @garuhq/node
23
40
  ```
24
41
 
25
- ## 60-second quickstart
42
+ ## Quickstart
26
43
 
27
44
  ```ts
28
45
  import { Garu } from '@garuhq/node';
@@ -37,53 +54,129 @@ const charge = await garu.charges.create({
37
54
  name: 'Maria Silva',
38
55
  email: 'maria@exemplo.com.br',
39
56
  document: '12345678909', // CPF, digits only
40
- phone: '11987654321'
41
- }
57
+ phone: '11987654321',
58
+ },
42
59
  });
43
60
 
44
61
  console.log(charge.id, charge.status);
45
62
  ```
46
63
 
47
- ## Authentication
64
+ ## Setup
48
65
 
49
- Get your API key from the [Garu dashboard](https://garu.com.br/inicio) → **API Keys**. Use
50
- `sk_test_…` for test mode and `sk_live_…` for production.
66
+ Get your API key from the [Garu dashboard](https://garu.com.br/inicio) → **API Keys**.
51
67
 
52
68
  ```ts
53
69
  const garu = new Garu({ apiKey: process.env.GARU_API_KEY });
54
70
  ```
55
71
 
56
- Public endpoints (`meta.get`, public checkout-style `charges.create`) also work without an
57
- API key.
72
+ > [!NOTE]
73
+ > Use `sk_test_…` for test mode and `sk_live_…` for production. Public endpoints like `meta.get` work without a key.
74
+
75
+ ## Configuration
58
76
 
59
- ## Surface
77
+ ```ts
78
+ const garu = new Garu({
79
+ apiKey: process.env.GARU_API_KEY,
80
+ timeoutMs: 30_000, // default
81
+ maxRetries: 2, // default (3 total attempts)
82
+ });
83
+ ```
60
84
 
61
- ### `garu.charges`
85
+ ## Charges
62
86
 
63
- | Method | What it does |
87
+ | Method | Description |
64
88
  | --------------------- | -------------------------------------------- |
65
89
  | `create(params)` | Create a PIX, credit-card, or boleto charge. |
90
+ | `list(params?)` | List charges with pagination and filters. |
66
91
  | `get(id)` | Fetch a single charge by ID. |
67
92
  | `refund(id, params?)` | Refund a charge fully or partially. |
68
93
 
69
- Every mutation automatically attaches an `X-Idempotency-Key` header (UUIDv4) unless you pass
70
- one via `params.idempotencyKey`. Safe to retry — the backend caches the first response for
71
- 24h.
94
+ ### Create a PIX charge
95
+
96
+ ```ts
97
+ const charge = await garu.charges.create({
98
+ productId: 'b3f2c1e8-6e4a-4b9f-9d1c-2a1f6c3d4e5f',
99
+ paymentMethod: 'pix',
100
+ customer: {
101
+ name: 'Maria Silva',
102
+ email: 'maria@exemplo.com.br',
103
+ document: '12345678909',
104
+ phone: '11987654321',
105
+ },
106
+ });
107
+ ```
72
108
 
73
- ### `garu.meta`
109
+ ### Create a credit card charge
110
+
111
+ ```ts
112
+ const charge = await garu.charges.create({
113
+ productId: 'b3f2c1e8-6e4a-4b9f-9d1c-2a1f6c3d4e5f',
114
+ paymentMethod: 'credit_card',
115
+ card: {
116
+ number: '4111111111111111',
117
+ holderName: 'MARIA SILVA',
118
+ expirationMonth: '12',
119
+ expirationYear: '2028',
120
+ cvv: '123',
121
+ },
122
+ customer: {
123
+ name: 'Maria Silva',
124
+ email: 'maria@exemplo.com.br',
125
+ document: '12345678909',
126
+ phone: '11987654321',
127
+ },
128
+ });
129
+ ```
130
+
131
+ ### List charges
132
+
133
+ ```ts
134
+ const { data, meta } = await garu.charges.list({ limit: 10 });
135
+ ```
136
+
137
+ ### Refund a charge
138
+
139
+ ```ts
140
+ await garu.charges.refund(4472, { amount: 1000 }); // partial refund (R$10.00)
141
+ ```
142
+
143
+ > [!TIP]
144
+ > Every mutation automatically attaches an `X-Idempotency-Key` header (UUIDv4) unless you provide one via `params.idempotencyKey`. Safe to retry — the backend caches the first response for 24h.
145
+
146
+ ## Customers
147
+
148
+ | Method | Description |
149
+ | ------------------------- | --------------------------------------------- |
150
+ | `create(params)` | Create a new customer. |
151
+ | `list(params?)` | List customers with pagination and search. |
152
+ | `get(id)` | Fetch a single customer by ID. |
153
+ | `update(id, params)` | Update a customer's profile. |
154
+ | `delete(id)` | Delete a customer. |
155
+
156
+ ```ts
157
+ const customer = await garu.customers.create({
158
+ name: 'Maria Silva',
159
+ email: 'maria@exemplo.com.br',
160
+ document: '12345678909',
161
+ phone: '11987654321',
162
+ personType: 'fisica',
163
+ });
164
+
165
+ const { data, meta } = await garu.customers.list({ search: 'maria', limit: 10 });
166
+ ```
167
+
168
+ ## Meta
169
+
170
+ Discover available payment methods and webhook events. No authentication required.
74
171
 
75
172
  ```ts
76
173
  const meta = await garu.meta.get();
77
174
  console.log(meta.version, meta.payment_methods, meta.webhook_events);
78
175
  ```
79
176
 
80
- Unauthenticated. Use this to discover which payment methods and webhook events are
81
- currently supported.
82
-
83
- ### `Garu.webhooks.verify`
177
+ ## Webhooks
84
178
 
85
- Verify an incoming webhook before trusting its body. Uses HMAC-SHA256 with constant-time
86
- comparison.
179
+ Verify incoming webhooks with HMAC-SHA256 and constant-time comparison.
87
180
 
88
181
  ```ts
89
182
  import express from 'express';
@@ -96,10 +189,9 @@ app.post('/webhooks/garu', express.raw({ type: 'application/json' }), (req, res)
96
189
  const { event } = Garu.webhooks.verify({
97
190
  payload: req.body, // raw Buffer — do NOT re-serialize parsed JSON
98
191
  signature: req.header('x-garu-signature') ?? '',
99
- secret: process.env.GARU_WEBHOOK_SECRET!
192
+ secret: process.env.GARU_WEBHOOK_SECRET!,
100
193
  });
101
194
 
102
- // handle event
103
195
  console.log('Received', event);
104
196
  res.sendStatus(200);
105
197
  } catch (err) {
@@ -109,16 +201,19 @@ app.post('/webhooks/garu', express.raw({ type: 'application/json' }), (req, res)
109
201
  });
110
202
  ```
111
203
 
112
- ## Errors
204
+ > [!IMPORTANT]
205
+ > Always pass the raw request body to `verify()`. Parsing and re-serializing JSON will break the signature check.
113
206
 
114
- Every error extends `GaruError` and has a stable `code`:
207
+ ## Error handling
208
+
209
+ Every error extends `GaruError`. API errors include `status`, `requestId`, and `body`.
115
210
 
116
211
  ```ts
117
212
  import {
118
213
  GaruAPIError,
119
214
  GaruNotFoundError,
120
215
  GaruRateLimitError,
121
- GaruValidationError
216
+ GaruValidationError,
122
217
  } from '@garuhq/node';
123
218
 
124
219
  try {
@@ -128,7 +223,7 @@ try {
128
223
  /* 404 */
129
224
  }
130
225
  if (err instanceof GaruValidationError) {
131
- /* 400/422 */
226
+ /* 400 / 422 */
132
227
  }
133
228
  if (err instanceof GaruRateLimitError) {
134
229
  console.log('Retry in', err.retryAfterSec, 'seconds');
@@ -139,23 +234,24 @@ try {
139
234
  }
140
235
  ```
141
236
 
237
+ | Error class | HTTP status |
238
+ | ----------------------------------- | ------------------ |
239
+ | `GaruAuthenticationError` | `401` |
240
+ | `GaruPermissionError` | `403` |
241
+ | `GaruNotFoundError` | `404` |
242
+ | `GaruValidationError` | `400` / `422` |
243
+ | `GaruRateLimitError` | `429` |
244
+ | `GaruServerError` | `5xx` |
245
+ | `GaruConnectionError` | Network failure |
246
+ | `GaruSignatureVerificationError` | Webhook mismatch |
247
+
142
248
  ## Retries
143
249
 
144
- The SDK retries idempotent requests automatically on connection errors, `408`, `429`, and
145
- `5xx` responses. Exponential backoff with full jitter. Honors `Retry-After`. Never retries
146
- `4xx` validation errors.
147
-
148
- ```ts
149
- const garu = new Garu({
150
- apiKey: process.env.GARU_API_KEY,
151
- timeoutMs: 30_000, // default
152
- maxRetries: 2 // default (so 3 total attempts)
153
- });
154
- ```
250
+ The SDK retries automatically on connection errors, `408`, `429`, and `5xx` responses. Exponential backoff with full jitter. Honors `Retry-After`. Never retries `4xx` validation errors.
155
251
 
156
252
  ## TypeScript
157
253
 
158
- Ships with full `.d.ts` and strict types. All public types are exported from the root:
254
+ Ships with full `.d.ts` and strict types. All public types are re-exported from the root:
159
255
 
160
256
  ```ts
161
257
  import type {
@@ -165,10 +261,14 @@ import type {
165
261
  Customer,
166
262
  CardInfo,
167
263
  PaymentMethod,
168
- MetaResponse
264
+ MetaResponse,
169
265
  } from '@garuhq/node';
170
266
  ```
171
267
 
268
+ ## Security
269
+
270
+ To report a vulnerability, **do not open a public issue**. See [SECURITY.md](SECURITY.md) for responsible disclosure instructions.
271
+
172
272
  ## License
173
273
 
174
- MIT.
274
+ MIT — see [LICENSE](LICENSE) for details.
package/dist/index.cjs CHANGED
@@ -222,7 +222,7 @@ var Charges = class {
222
222
  * phone: '11987654321'
223
223
  * }
224
224
  * });
225
- * console.log(charge.id, charge.status);
225
+ * // charge.id, charge.status
226
226
  *
227
227
  * @example
228
228
  * // Credit card charge, 3 installments
@@ -250,6 +250,28 @@ var Charges = class {
250
250
  })
251
251
  );
252
252
  }
253
+ /**
254
+ * List charges for the authenticated seller, with pagination and filters.
255
+ *
256
+ * @example
257
+ * const { data, meta } = await garu.charges.list({ status: 'paid', limit: 10 });
258
+ * // meta.total paid charges
259
+ */
260
+ async list(params = {}) {
261
+ const query = {};
262
+ if (params.page !== void 0) query.page = String(params.page);
263
+ if (params.limit !== void 0) query.limit = String(params.limit);
264
+ if (params.status) query.status = params.status;
265
+ if (params.search) query.search = params.search;
266
+ if (params.paymentMethod) query.paymentMethod = params.paymentMethod;
267
+ const qs = new URLSearchParams(query).toString();
268
+ const url = `/api/transactions${qs ? `?${qs}` : ""}`;
269
+ return this.http.call(
270
+ (signal) => this.http.client.GET(url, { signal }).then(
271
+ (r) => r
272
+ )
273
+ );
274
+ }
253
275
  /**
254
276
  * Fetch a single charge by numeric ID.
255
277
  *
@@ -308,6 +330,93 @@ var Charges = class {
308
330
  }
309
331
  };
310
332
 
333
+ // src/resources/customers.ts
334
+ var Customers = class {
335
+ constructor(http) {
336
+ this.http = http;
337
+ }
338
+ http;
339
+ /**
340
+ * Create a customer and link it to the current seller.
341
+ *
342
+ * @example
343
+ * const customer = await garu.customers.create({
344
+ * name: 'Maria Silva',
345
+ * email: 'maria@exemplo.com.br',
346
+ * document: '12345678909',
347
+ * phone: '11987654321',
348
+ * personType: 'fisica'
349
+ * });
350
+ */
351
+ async create(params) {
352
+ return this.http.call(
353
+ (signal) => this.http.client.POST("/api/customers", {
354
+ body: params,
355
+ signal
356
+ }).then((r) => r)
357
+ );
358
+ }
359
+ /**
360
+ * List customers for the authenticated seller, with pagination and search.
361
+ *
362
+ * @example
363
+ * const { data, meta } = await garu.customers.list({ search: 'maria', limit: 10 });
364
+ */
365
+ async list(params = {}) {
366
+ const query = {};
367
+ if (params.page !== void 0) query.page = String(params.page);
368
+ if (params.limit !== void 0) query.limit = String(params.limit);
369
+ if (params.search) query.search = params.search;
370
+ const qs = new URLSearchParams(query).toString();
371
+ const url = `/api/customers${qs ? `?${qs}` : ""}`;
372
+ return this.http.call(
373
+ (signal) => this.http.client.GET(url, { signal }).then(
374
+ (r) => r
375
+ )
376
+ );
377
+ }
378
+ /**
379
+ * Fetch a single customer by numeric ID.
380
+ *
381
+ * @example
382
+ * const customer = await garu.customers.get(42);
383
+ */
384
+ async get(id) {
385
+ return this.http.call(
386
+ (signal) => this.http.client.GET(`/api/customers/${id}`, { signal }).then(
387
+ (r) => r
388
+ )
389
+ );
390
+ }
391
+ /**
392
+ * Update a customer's profile for the current seller.
393
+ *
394
+ * @example
395
+ * const updated = await garu.customers.update(42, { name: 'Maria Santos' });
396
+ */
397
+ async update(id, params) {
398
+ return this.http.call(
399
+ (signal) => this.http.client.PUT(`/api/customers/${id}`, {
400
+ body: params,
401
+ signal
402
+ }).then((r) => r)
403
+ );
404
+ }
405
+ /**
406
+ * Remove a customer from the current seller.
407
+ *
408
+ * @example
409
+ * await garu.customers.delete(42);
410
+ */
411
+ async delete(id) {
412
+ await this.http.call(
413
+ (signal) => this.http.client.DELETE(`/api/customers/${id}`, { signal }).then(
414
+ (r) => r
415
+ )
416
+ );
417
+ }
418
+ };
419
+
311
420
  // src/resources/meta.ts
312
421
  var Meta = class {
313
422
  constructor(http) {
@@ -328,6 +437,48 @@ var Meta = class {
328
437
  );
329
438
  }
330
439
  };
440
+
441
+ // src/resources/products.ts
442
+ var Products = class {
443
+ constructor(http) {
444
+ this.http = http;
445
+ }
446
+ http;
447
+ /**
448
+ * List products for the authenticated seller, with pagination and search.
449
+ *
450
+ * @example
451
+ * const { data, meta } = await garu.products.list({ search: 'curso', limit: 10 });
452
+ */
453
+ async list(params = {}) {
454
+ const query = {};
455
+ if (params.page !== void 0) query.page = String(params.page);
456
+ if (params.limit !== void 0) query.limit = String(params.limit);
457
+ if (params.search) query.search = params.search;
458
+ if (params.tab) query.tab = params.tab;
459
+ const qs = new URLSearchParams(query).toString();
460
+ const url = `/api/products/seller${qs ? `?${qs}` : ""}`;
461
+ return this.http.call(
462
+ (signal) => this.http.client.GET(url, { signal }).then(
463
+ (r) => r
464
+ )
465
+ );
466
+ }
467
+ /**
468
+ * Fetch a single product by UUID — the same identifier used by
469
+ * `charges.create({ productId })`.
470
+ *
471
+ * @example
472
+ * const product = await garu.products.get('b3f2c1e8-6e4a-4b9f-9d1c-2a1f6c3d4e5f');
473
+ */
474
+ async get(uuid) {
475
+ return this.http.call(
476
+ (signal) => this.http.client.GET(`/api/products/uuid/${uuid}`, { signal }).then(
477
+ (r) => r
478
+ )
479
+ );
480
+ }
481
+ };
331
482
  var webhooks = {
332
483
  verify(params) {
333
484
  const { signature, secret, payload } = params;
@@ -387,10 +538,12 @@ function parseSignatureHeader(header) {
387
538
  var DEFAULT_BASE_URL = "https://garu.com.br";
388
539
  var DEFAULT_TIMEOUT_MS = 3e4;
389
540
  var DEFAULT_MAX_RETRIES = 2;
390
- var SDK_VERSION = "0.1.0";
541
+ var SDK_VERSION = "0.3.0";
391
542
  var Garu = class {
392
543
  charges;
544
+ customers;
393
545
  meta;
546
+ products;
394
547
  /**
395
548
  * Webhook helpers. Available both as an instance member and as a static —
396
549
  * `Garu.webhooks.verify(...)` works without constructing a client.
@@ -407,7 +560,9 @@ var Garu = class {
407
560
  fetch: options.fetch
408
561
  });
409
562
  this.charges = new Charges(http);
563
+ this.customers = new Customers(http);
410
564
  this.meta = new Meta(http);
565
+ this.products = new Products(http);
411
566
  }
412
567
  };
413
568
 
@@ -423,5 +578,3 @@ exports.GaruServerError = GaruServerError;
423
578
  exports.GaruSignatureVerificationError = GaruSignatureVerificationError;
424
579
  exports.GaruValidationError = GaruValidationError;
425
580
  exports.webhooks = webhooks;
426
- //# sourceMappingURL=index.cjs.map
427
- //# sourceMappingURL=index.cjs.map