@garuhq/node 0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@garuhq/node` are documented in this file. Format:
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
5
+
6
+ ## [0.1.0] — 2026-04-08
7
+
8
+ ### Added
9
+
10
+ - Initial public beta. Reference Node.js / TypeScript SDK for the Garu payment gateway.
11
+ - `Garu` client class with `apiKey`, `baseUrl`, `timeoutMs`, `maxRetries` options.
12
+ - `charges.create` — PIX, credit card, and boleto charges. Auto-generates `X-Idempotency-Key`
13
+ (UUIDv4) unless the caller supplies one; safe to retry against the Chunk 2 backend cache.
14
+ - `charges.get` / `charges.list` / `charges.refund`.
15
+ - `meta.get` — capability introspection via `GET /api/meta`.
16
+ - `webhooks.verify` — HMAC-SHA256 signature verification, constant-time comparison,
17
+ configurable tolerance window (default 300s). Matches backend `X-Garu-Signature` format
18
+ `t=<ts>,v1=<hex>`.
19
+ - Typed error hierarchy: `GaruAuthenticationError`, `GaruPermissionError`, `GaruNotFoundError`,
20
+ `GaruValidationError`, `GaruRateLimitError`, `GaruServerError`, `GaruConnectionError`,
21
+ `GaruSignatureVerificationError`. Every error has a stable `code` string.
22
+ - Automatic retries with exponential backoff + full jitter on 408/429/5xx and connection
23
+ errors. Honors `Retry-After`. Never retries 4xx validation errors.
24
+ - One runtime dependency (`openapi-fetch`). Node.js ≥ 18 (native `fetch`, `crypto.randomUUID`).
25
+ - Dual ESM + CJS build via `tsup` with full `.d.ts`.
26
+ - Wire types generated from the live backend OpenAPI spec via `openapi-typescript` —
27
+ `npm run generate` refreshes `src/generated/{openapi.json, schema.d.ts}`.
28
+ - Contract tests assert the generated snapshot still covers every endpoint the SDK uses.
29
+ - GitHub Actions CI runs typecheck, tests, and build on Node 18 / 20 / 22.
30
+
31
+ ### Known limitations
32
+
33
+ - Only `charges` and `meta` namespaces. Customers, products, subscriptions, checkout
34
+ sessions, and webhook-endpoint management land in a future release.
35
+ - `charges.list` is intentionally omitted — the backend does not yet expose
36
+ `GET /api/transactions`. It will land when the backend grows a seller-scoped list endpoint.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Garu Pagamentos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # @garuhq/node
2
+
3
+ Official Node.js / TypeScript SDK for the [Garu](https://garu.com.br) payment gateway.
4
+
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`.
13
+ - **ESM + CJS** dual build.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @garuhq/node
19
+ # or
20
+ pnpm add @garuhq/node
21
+ # or
22
+ yarn add @garuhq/node
23
+ ```
24
+
25
+ ## 60-second quickstart
26
+
27
+ ```ts
28
+ import { Garu } from '@garuhq/node';
29
+
30
+ const garu = new Garu({ apiKey: process.env.GARU_API_KEY });
31
+
32
+ // Create a PIX charge
33
+ const charge = await garu.charges.create({
34
+ productId: 'b3f2c1e8-6e4a-4b9f-9d1c-2a1f6c3d4e5f',
35
+ paymentMethod: 'pix',
36
+ customer: {
37
+ name: 'Maria Silva',
38
+ email: 'maria@exemplo.com.br',
39
+ document: '12345678909', // CPF, digits only
40
+ phone: '11987654321'
41
+ }
42
+ });
43
+
44
+ console.log(charge.id, charge.status);
45
+ ```
46
+
47
+ ## Authentication
48
+
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.
51
+
52
+ ```ts
53
+ const garu = new Garu({ apiKey: process.env.GARU_API_KEY });
54
+ ```
55
+
56
+ Public endpoints (`meta.get`, public checkout-style `charges.create`) also work without an
57
+ API key.
58
+
59
+ ## Surface
60
+
61
+ ### `garu.charges`
62
+
63
+ | Method | What it does |
64
+ | --------------------- | -------------------------------------------- |
65
+ | `create(params)` | Create a PIX, credit-card, or boleto charge. |
66
+ | `get(id)` | Fetch a single charge by ID. |
67
+ | `refund(id, params?)` | Refund a charge fully or partially. |
68
+
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.
72
+
73
+ ### `garu.meta`
74
+
75
+ ```ts
76
+ const meta = await garu.meta.get();
77
+ console.log(meta.version, meta.payment_methods, meta.webhook_events);
78
+ ```
79
+
80
+ Unauthenticated. Use this to discover which payment methods and webhook events are
81
+ currently supported.
82
+
83
+ ### `Garu.webhooks.verify`
84
+
85
+ Verify an incoming webhook before trusting its body. Uses HMAC-SHA256 with constant-time
86
+ comparison.
87
+
88
+ ```ts
89
+ import express from 'express';
90
+ import { Garu, GaruSignatureVerificationError } from '@garuhq/node';
91
+
92
+ const app = express();
93
+
94
+ app.post('/webhooks/garu', express.raw({ type: 'application/json' }), (req, res) => {
95
+ try {
96
+ const { event } = Garu.webhooks.verify({
97
+ payload: req.body, // raw Buffer — do NOT re-serialize parsed JSON
98
+ signature: req.header('x-garu-signature') ?? '',
99
+ secret: process.env.GARU_WEBHOOK_SECRET!
100
+ });
101
+
102
+ // handle event
103
+ console.log('Received', event);
104
+ res.sendStatus(200);
105
+ } catch (err) {
106
+ if (err instanceof GaruSignatureVerificationError) return res.sendStatus(400);
107
+ throw err;
108
+ }
109
+ });
110
+ ```
111
+
112
+ ## Errors
113
+
114
+ Every error extends `GaruError` and has a stable `code`:
115
+
116
+ ```ts
117
+ import {
118
+ GaruAPIError,
119
+ GaruNotFoundError,
120
+ GaruRateLimitError,
121
+ GaruValidationError
122
+ } from '@garuhq/node';
123
+
124
+ try {
125
+ await garu.charges.refund(4472, { amount: 1000 });
126
+ } catch (err) {
127
+ if (err instanceof GaruNotFoundError) {
128
+ /* 404 */
129
+ }
130
+ if (err instanceof GaruValidationError) {
131
+ /* 400/422 */
132
+ }
133
+ if (err instanceof GaruRateLimitError) {
134
+ console.log('Retry in', err.retryAfterSec, 'seconds');
135
+ }
136
+ if (err instanceof GaruAPIError) {
137
+ console.log(err.status, err.requestId, err.body);
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## Retries
143
+
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
+ ```
155
+
156
+ ## TypeScript
157
+
158
+ Ships with full `.d.ts` and strict types. All public types are exported from the root:
159
+
160
+ ```ts
161
+ import type {
162
+ Charge,
163
+ ChargeStatus,
164
+ CreateChargeParams,
165
+ Customer,
166
+ CardInfo,
167
+ PaymentMethod,
168
+ MetaResponse
169
+ } from '@garuhq/node';
170
+ ```
171
+
172
+ ## License
173
+
174
+ MIT.
package/dist/index.cjs ADDED
@@ -0,0 +1,445 @@
1
+ 'use strict';
2
+
3
+ var createClient = require('openapi-fetch');
4
+ var crypto = require('crypto');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var createClient__default = /*#__PURE__*/_interopDefault(createClient);
9
+
10
+ // src/http.ts
11
+
12
+ // src/errors.ts
13
+ var GaruError = class extends Error {
14
+ code;
15
+ constructor(code, message) {
16
+ super(message);
17
+ this.name = "GaruError";
18
+ this.code = code;
19
+ }
20
+ };
21
+ var GaruConnectionError = class extends GaruError {
22
+ connectionCause;
23
+ constructor(message, connectionCause) {
24
+ super("connection_error", message);
25
+ this.name = "GaruConnectionError";
26
+ this.connectionCause = connectionCause;
27
+ }
28
+ };
29
+ var GaruSignatureVerificationError = class extends GaruError {
30
+ constructor(message) {
31
+ super("signature_verification_failed", message);
32
+ this.name = "GaruSignatureVerificationError";
33
+ }
34
+ };
35
+ var GaruAPIError = class extends GaruError {
36
+ status;
37
+ requestId;
38
+ body;
39
+ constructor(code, message, status, requestId, body) {
40
+ super(code, message);
41
+ this.name = "GaruAPIError";
42
+ this.status = status;
43
+ this.requestId = requestId;
44
+ this.body = body;
45
+ }
46
+ };
47
+ var GaruAuthenticationError = class extends GaruAPIError {
48
+ constructor(message, status, requestId, body) {
49
+ super("authentication_error", message, status, requestId, body);
50
+ this.name = "GaruAuthenticationError";
51
+ }
52
+ };
53
+ var GaruPermissionError = class extends GaruAPIError {
54
+ constructor(message, status, requestId, body) {
55
+ super("permission_error", message, status, requestId, body);
56
+ this.name = "GaruPermissionError";
57
+ }
58
+ };
59
+ var GaruNotFoundError = class extends GaruAPIError {
60
+ constructor(message, status, requestId, body) {
61
+ super("not_found", message, status, requestId, body);
62
+ this.name = "GaruNotFoundError";
63
+ }
64
+ };
65
+ var GaruValidationError = class extends GaruAPIError {
66
+ constructor(message, status, requestId, body) {
67
+ super("validation_error", message, status, requestId, body);
68
+ this.name = "GaruValidationError";
69
+ }
70
+ };
71
+ var GaruRateLimitError = class extends GaruAPIError {
72
+ retryAfterSec;
73
+ constructor(message, status, requestId, body, retryAfterSec) {
74
+ super("rate_limited", message, status, requestId, body);
75
+ this.name = "GaruRateLimitError";
76
+ this.retryAfterSec = retryAfterSec;
77
+ }
78
+ };
79
+ var GaruServerError = class extends GaruAPIError {
80
+ constructor(message, status, requestId, body) {
81
+ super("server_error", message, status, requestId, body);
82
+ this.name = "GaruServerError";
83
+ }
84
+ };
85
+ function mapApiError(status, body, requestId, retryAfterSec) {
86
+ const message = extractMessage(body) ?? `Garu API returned HTTP ${status}`;
87
+ if (status === 401)
88
+ return new GaruAuthenticationError(message, status, requestId, body);
89
+ if (status === 403)
90
+ return new GaruPermissionError(message, status, requestId, body);
91
+ if (status === 404)
92
+ return new GaruNotFoundError(message, status, requestId, body);
93
+ if (status === 400 || status === 422) {
94
+ return new GaruValidationError(message, status, requestId, body);
95
+ }
96
+ if (status === 429) {
97
+ return new GaruRateLimitError(message, status, requestId, body, retryAfterSec);
98
+ }
99
+ if (status >= 500)
100
+ return new GaruServerError(message, status, requestId, body);
101
+ return new GaruAPIError("api_error", message, status, requestId, body);
102
+ }
103
+ function extractMessage(body) {
104
+ if (typeof body === "string")
105
+ return body;
106
+ if (body && typeof body === "object") {
107
+ const m = body.message;
108
+ if (typeof m === "string")
109
+ return m;
110
+ if (Array.isArray(m) && m.every((x) => typeof x === "string"))
111
+ return m.join("; ");
112
+ }
113
+ return null;
114
+ }
115
+
116
+ // src/http.ts
117
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
118
+ var HttpClient = class {
119
+ client;
120
+ cfg;
121
+ constructor(cfg) {
122
+ this.cfg = cfg;
123
+ const fetchImpl = cfg.fetch ?? globalThis.fetch;
124
+ if (!fetchImpl) {
125
+ throw new GaruConnectionError(
126
+ "No fetch implementation available. Node.js >= 18 is required."
127
+ );
128
+ }
129
+ const headers = {
130
+ Accept: "application/json",
131
+ "User-Agent": cfg.userAgent
132
+ };
133
+ if (cfg.apiKey)
134
+ headers.Authorization = `Bearer ${cfg.apiKey}`;
135
+ this.client = createClient__default.default({
136
+ baseUrl: cfg.baseUrl.replace(/\/+$/, ""),
137
+ fetch: fetchImpl,
138
+ headers
139
+ });
140
+ }
141
+ /**
142
+ * Issue one HTTP call against the typed client, with retries + error mapping.
143
+ *
144
+ * `fn` is invoked up to `maxRetries + 1` times. The timeout in `cfg.timeoutMs`
145
+ * is enforced via `AbortController`.
146
+ */
147
+ async call(fn) {
148
+ let lastError = null;
149
+ for (let attempt = 0; attempt <= this.cfg.maxRetries; attempt++) {
150
+ const controller = new AbortController();
151
+ const timer = setTimeout(() => controller.abort(), this.cfg.timeoutMs);
152
+ try {
153
+ const { data, error, response } = await fn(controller.signal);
154
+ clearTimeout(timer);
155
+ if (response.ok) {
156
+ return data;
157
+ }
158
+ const requestId = response.headers.get("x-request-id");
159
+ const retryAfterSec = parseRetryAfter(response.headers.get("retry-after"));
160
+ const apiError = mapApiError(response.status, error ?? null, requestId, retryAfterSec);
161
+ lastError = apiError;
162
+ if (!RETRYABLE_STATUSES.has(response.status) || attempt === this.cfg.maxRetries) {
163
+ throw apiError;
164
+ }
165
+ await sleep(backoffDelay(attempt, retryAfterSec));
166
+ continue;
167
+ } catch (err) {
168
+ clearTimeout(timer);
169
+ if (isGaruApiError(err))
170
+ throw err;
171
+ const connErr = err instanceof Error && err.name === "AbortError" ? new GaruConnectionError(`Request timed out after ${this.cfg.timeoutMs}ms`, err) : new GaruConnectionError(err instanceof Error ? err.message : "Network error", err);
172
+ lastError = connErr;
173
+ if (attempt === this.cfg.maxRetries)
174
+ throw connErr;
175
+ await sleep(backoffDelay(attempt, null));
176
+ continue;
177
+ }
178
+ }
179
+ throw lastError ?? new GaruConnectionError("Request failed with no error captured");
180
+ }
181
+ };
182
+ function isGaruApiError(err) {
183
+ return err instanceof Error && err.name.startsWith("Garu") && err.name.endsWith("Error");
184
+ }
185
+ function parseRetryAfter(value) {
186
+ if (!value)
187
+ return null;
188
+ const n = Number(value);
189
+ return Number.isFinite(n) && n >= 0 ? n : null;
190
+ }
191
+ function backoffDelay(attempt, retryAfterSec) {
192
+ if (retryAfterSec !== null) {
193
+ return retryAfterSec * 1e3 + Math.random() * 250;
194
+ }
195
+ const base = 500 * 2 ** attempt;
196
+ const cap = 8e3;
197
+ return Math.min(cap, base) * Math.random();
198
+ }
199
+ function sleep(ms) {
200
+ return new Promise((resolve) => setTimeout(resolve, ms));
201
+ }
202
+ function generateIdempotencyKey() {
203
+ return crypto.randomUUID();
204
+ }
205
+
206
+ // src/types.ts
207
+ function toWirePaymentMethod(pm) {
208
+ return pm === "credit_card" ? "creditcard" : pm;
209
+ }
210
+
211
+ // src/resources/charges.ts
212
+ var Charges = class {
213
+ constructor(http) {
214
+ this.http = http;
215
+ }
216
+ /**
217
+ * Create a charge (PIX, credit card, or boleto).
218
+ *
219
+ * Automatically attaches an `X-Idempotency-Key` header — if you don't pass
220
+ * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the backend
221
+ * caches the first response for 24h.
222
+ *
223
+ * @example
224
+ * // PIX charge
225
+ * const charge = await garu.charges.create({
226
+ * productId: 'b3f2c1e8-6e4a-4b9f-9d1c-2a1f6c3d4e5f',
227
+ * paymentMethod: 'pix',
228
+ * customer: {
229
+ * name: 'Maria Silva',
230
+ * email: 'maria@exemplo.com.br',
231
+ * document: '12345678909',
232
+ * phone: '11987654321'
233
+ * }
234
+ * });
235
+ * console.log(charge.id, charge.status);
236
+ *
237
+ * @example
238
+ * // Credit card charge, 3 installments
239
+ * const charge = await garu.charges.create({
240
+ * productId: 'b3f2c1e8-6e4a-4b9f-9d1c-2a1f6c3d4e5f',
241
+ * paymentMethod: 'credit_card',
242
+ * customer: { name: 'Maria Silva', email: 'maria@exemplo.com.br', document: '12345678909', phone: '11987654321' },
243
+ * cardInfo: {
244
+ * cardNumber: '4111111111111111',
245
+ * cvv: '123',
246
+ * expirationDate: '2030-12',
247
+ * holderName: 'MARIA SILVA',
248
+ * installments: 3
249
+ * }
250
+ * });
251
+ */
252
+ async create(params) {
253
+ const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
254
+ const body = this.buildCreateBody(params);
255
+ return this.http.call(
256
+ (signal) => this.http.client.POST("/api/transactions", {
257
+ body,
258
+ headers: { "X-Idempotency-Key": idempotencyKey },
259
+ signal
260
+ })
261
+ );
262
+ }
263
+ /**
264
+ * Fetch a single charge by numeric ID.
265
+ *
266
+ * @example
267
+ * const charge = await garu.charges.get(4472);
268
+ * if (charge.status === 'paid') { ... }
269
+ */
270
+ async get(id) {
271
+ return this.http.call(
272
+ (signal) => this.http.client.GET("/api/transactions/{id}", {
273
+ params: { path: { id } },
274
+ signal
275
+ })
276
+ );
277
+ }
278
+ /**
279
+ * Refund a charge — fully, or partially by passing `amount` in centavos.
280
+ *
281
+ * @example
282
+ * // Full refund
283
+ * await garu.charges.refund(4472);
284
+ *
285
+ * @example
286
+ * // Partial refund of R$ 10,00
287
+ * await garu.charges.refund(4472, { amount: 1000, reason: 'customer_request' });
288
+ */
289
+ async refund(id, params = {}) {
290
+ const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
291
+ const body = {};
292
+ if (params.amount !== void 0)
293
+ body.amount = params.amount;
294
+ if (params.reason !== void 0)
295
+ body.reason = params.reason;
296
+ return this.http.call(
297
+ (signal) => this.http.client.POST("/api/transactions/{id}/refund", {
298
+ params: { path: { id } },
299
+ body,
300
+ headers: { "X-Idempotency-Key": idempotencyKey },
301
+ signal
302
+ })
303
+ );
304
+ }
305
+ buildCreateBody(params) {
306
+ const body = {
307
+ customer: params.customer,
308
+ productId: params.productId,
309
+ paymentMethodId: toWirePaymentMethod(params.paymentMethod),
310
+ link: params.link ?? null,
311
+ affiliateId: params.affiliateId ?? null
312
+ };
313
+ if (params.additionalInfo !== void 0)
314
+ body.additionalInfo = params.additionalInfo;
315
+ if (params.priceId !== void 0)
316
+ body.priceId = params.priceId;
317
+ if (params.checkoutSessionToken !== void 0) {
318
+ body.checkoutSessionToken = params.checkoutSessionToken;
319
+ }
320
+ if (params.cardInfo)
321
+ body.CardInfo = params.cardInfo;
322
+ return body;
323
+ }
324
+ };
325
+
326
+ // src/resources/meta.ts
327
+ var Meta = class {
328
+ constructor(http) {
329
+ this.http = http;
330
+ }
331
+ /**
332
+ * Fetch the API's current capability payload.
333
+ *
334
+ * @example
335
+ * const meta = await garu.meta.get();
336
+ * console.log(meta.version, meta.payment_methods);
337
+ * if (meta.features.subscriptions) { ... }
338
+ */
339
+ async get() {
340
+ return this.http.call(
341
+ (signal) => this.http.client.GET("/api/meta", { signal })
342
+ );
343
+ }
344
+ };
345
+ var webhooks = {
346
+ verify(params) {
347
+ const { signature, secret, payload } = params;
348
+ const toleranceSec = params.toleranceSec ?? 300;
349
+ const now = params.now ?? Date.now;
350
+ if (!signature || typeof signature !== "string") {
351
+ throw new GaruSignatureVerificationError("Missing or malformed X-Garu-Signature header");
352
+ }
353
+ const parts = parseSignatureHeader(signature);
354
+ if (parts === null) {
355
+ throw new GaruSignatureVerificationError(
356
+ "X-Garu-Signature header does not match expected format t=<ts>,v1=<hex>"
357
+ );
358
+ }
359
+ const payloadStr = typeof payload === "string" ? payload : payload.toString("utf8");
360
+ const signedPayload = `${parts.timestamp}.${payloadStr}`;
361
+ const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
362
+ const expectedBuf = Buffer.from(expected, "hex");
363
+ const providedBuf = Buffer.from(parts.v1, "hex");
364
+ if (expectedBuf.length !== providedBuf.length || !crypto.timingSafeEqual(expectedBuf, providedBuf)) {
365
+ throw new GaruSignatureVerificationError("Signature does not match computed HMAC");
366
+ }
367
+ const nowSec = Math.floor(now() / 1e3);
368
+ if (Math.abs(nowSec - parts.timestamp) > toleranceSec) {
369
+ throw new GaruSignatureVerificationError(
370
+ `Signature timestamp outside tolerance window (${toleranceSec}s)`
371
+ );
372
+ }
373
+ let event;
374
+ try {
375
+ event = JSON.parse(payloadStr);
376
+ } catch {
377
+ throw new GaruSignatureVerificationError("Webhook payload is not valid JSON");
378
+ }
379
+ return { timestamp: parts.timestamp, event };
380
+ }
381
+ };
382
+ function parseSignatureHeader(header) {
383
+ const fields = {};
384
+ for (const part of header.split(",")) {
385
+ const idx = part.indexOf("=");
386
+ if (idx === -1)
387
+ return null;
388
+ const key = part.slice(0, idx).trim();
389
+ const value = part.slice(idx + 1).trim();
390
+ if (!key || !value)
391
+ return null;
392
+ fields[key] = value;
393
+ }
394
+ const t = fields.t;
395
+ const v1 = fields.v1;
396
+ if (!t || !v1)
397
+ return null;
398
+ const timestamp = Number(t);
399
+ if (!Number.isFinite(timestamp) || !/^[a-f0-9]+$/i.test(v1))
400
+ return null;
401
+ return { timestamp, v1 };
402
+ }
403
+
404
+ // src/client.ts
405
+ var DEFAULT_BASE_URL = "https://garu.com.br";
406
+ var DEFAULT_TIMEOUT_MS = 3e4;
407
+ var DEFAULT_MAX_RETRIES = 2;
408
+ var SDK_VERSION = "0.1.0";
409
+ var Garu = class {
410
+ charges;
411
+ meta;
412
+ /**
413
+ * Webhook helpers. Available both as an instance member and as a static —
414
+ * `Garu.webhooks.verify(...)` works without constructing a client.
415
+ */
416
+ static webhooks = webhooks;
417
+ webhooks = webhooks;
418
+ constructor(options = {}) {
419
+ const http = new HttpClient({
420
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
421
+ apiKey: options.apiKey,
422
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
423
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
424
+ userAgent: `garu-node/${SDK_VERSION}`,
425
+ fetch: options.fetch
426
+ });
427
+ this.charges = new Charges(http);
428
+ this.meta = new Meta(http);
429
+ }
430
+ };
431
+
432
+ exports.Garu = Garu;
433
+ exports.GaruAPIError = GaruAPIError;
434
+ exports.GaruAuthenticationError = GaruAuthenticationError;
435
+ exports.GaruConnectionError = GaruConnectionError;
436
+ exports.GaruError = GaruError;
437
+ exports.GaruNotFoundError = GaruNotFoundError;
438
+ exports.GaruPermissionError = GaruPermissionError;
439
+ exports.GaruRateLimitError = GaruRateLimitError;
440
+ exports.GaruServerError = GaruServerError;
441
+ exports.GaruSignatureVerificationError = GaruSignatureVerificationError;
442
+ exports.GaruValidationError = GaruValidationError;
443
+ exports.webhooks = webhooks;
444
+ //# sourceMappingURL=out.js.map
445
+ //# sourceMappingURL=index.cjs.map