@boelabs/billete-sdk 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -1,28 +1,130 @@
1
- # @boelabs/billete-sdk
1
+ <div align="center">
2
2
 
3
- SDK oficial de [Billete](https://billete.run): pagos y suscripciones para Venezuela con Pago Móvil,
4
- precios en bolívares o en divisas a tasa BCV y cuotas por plan. Tipado de punta a punta, sin
5
- dependencias y compatible con Node, Bun, Deno y runtimes edge.
3
+ # Billete SDK
4
+
5
+ **SDK oficial de TypeScript para la API de [Billete](https://billete.run).**
6
+
7
+ Cobros con Pago Móvil, precios en bolívares o en divisas a tasa BCV y suscripciones, desde tu
8
+ servidor y tu frontend.
9
+
10
+ [![npm](https://img.shields.io/npm/v/@boelabs/billete-sdk?color=000&label=npm)](https://www.npmjs.com/package/@boelabs/billete-sdk)
11
+ [![Licencia: MIT](https://img.shields.io/badge/licencia-MIT-000)](./LICENSE)
12
+ ![Tipos incluidos](https://img.shields.io/npm/types/@boelabs/billete-sdk?color=000)
13
+ ![Sin dependencias](https://img.shields.io/badge/dependencias-0-000)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ - **Tipado de punta a punta:** cada recurso, entrada y evento tiene su tipo.
20
+ - **Sin dependencias:** solo `fetch` y Web Crypto; funciona en Node.js 20+, Bun, Deno y
21
+ runtimes edge (Cloudflare Workers, Vercel).
22
+ - **Seguro ante reintentos:** cada mutación lleva una clave de idempotencia, así un reintento
23
+ nunca cobra dos veces.
24
+
25
+ ## Contenido
26
+
27
+ - [Instalación](#instalación)
28
+ - [Inicio rápido](#inicio-rápido)
29
+ - [Configuración](#configuración)
30
+ - [Recursos](#recursos)
31
+ - [Montos](#montos)
32
+ - [Paginación](#paginación)
33
+ - [Errores](#errores)
34
+ - [Idempotencia y reintentos](#idempotencia-y-reintentos)
35
+ - [Navegador](#navegador)
36
+ - [Webhooks](#webhooks)
37
+ - [Licencia](#licencia)
38
+
39
+ ## Instalación
6
40
 
7
41
  ```bash
8
42
  npm install @boelabs/billete-sdk
9
43
  ```
10
44
 
11
- ## Servidor
45
+ El paquete tiene tres puntos de entrada:
46
+
47
+ | Import | Dónde se usa | Para qué |
48
+ | --- | --- | --- |
49
+ | `@boelabs/billete-sdk` | Servidor | Toda la API, con tu clave secreta |
50
+ | `@boelabs/billete-sdk/client` | Navegador | Checkout y estado de un cliente, sin clave secreta |
51
+ | `@boelabs/billete-sdk/webhooks` | Servidor | Verificar la firma de los webhooks |
52
+
53
+ ## Inicio rápido
54
+
55
+ Crea una clave secreta en el dashboard (**Desarrolladores → Claves API**) y guárdala en
56
+ `BILLETE_API_KEY`.
12
57
 
13
58
  ```ts
14
59
  import { createBillete } from "@boelabs/billete-sdk";
15
60
 
61
+ const billete = createBillete({ environment: "test" });
62
+
63
+ const checkout = await billete.checkouts.create({
64
+ plan: "plan_…",
65
+ months: 1,
66
+ customer: { email: "ana@example.com", externalId: "user_123" },
67
+ });
68
+
69
+ // Redirige a tu cliente a checkout.url para que pague.
70
+ ```
71
+
72
+ Cuando el pago se confirma, Billete te avisa con el webhook `checkout.completed` (ver
73
+ [Webhooks](#webhooks)).
74
+
75
+ ## Configuración
76
+
77
+ ```ts
16
78
  const billete = createBillete({
17
- environment: "test", // obligatorio: "test" o "production"
18
- // apiKey: por defecto lee BILLETE_API_KEY
19
- // baseUrl: por defecto lee BILLETE_BASE_URL, o usa https://api.billete.run
79
+ environment: "production",
80
+ apiKey: process.env.BILLETE_API_KEY,
81
+ timeoutMs: 30_000,
82
+ maxRetries: 2,
20
83
  });
21
84
  ```
22
85
 
23
- `environment` debe coincidir con la clave (`sk_test_…` o `sk_prod_…`): si no coinciden, el cliente
24
- falla al crearse, así una clave nunca opera en el entorno equivocado. Las claves se crean en el
25
- dashboard, en **Desarrolladores → Claves API**. Nunca uses una clave en el navegador.
86
+ | Opción | Por defecto | Descripción |
87
+ | --- | --- | --- |
88
+ | `environment` | — (obligatoria) | `"test"` o `"production"`. Debe coincidir con la clave. |
89
+ | `apiKey` | `BILLETE_API_KEY` | Clave secreta: `sk_test_…` o `sk_prod_…`. |
90
+ | `baseUrl` | `BILLETE_BASE_URL`, o `https://api.billete.run` | URL de la API. |
91
+ | `timeoutMs` | `30000` | Tiempo máximo por intento. |
92
+ | `maxRetries` | `2` | Reintentos ante errores de red, `429` y `5xx`. |
93
+ | `fetch` | `globalThis.fetch` | Implementación de `fetch` propia (proxies, tests). |
94
+
95
+ Si `environment` no coincide con el prefijo de la clave, `createBillete` lanza un error: una
96
+ clave de prueba nunca opera en producción ni al revés. Por la misma razón, el cliente de servidor
97
+ se niega a crearse en un navegador.
98
+
99
+ > [!WARNING]
100
+ > La clave secreta da acceso completo a tu negocio. Úsala solo en el servidor y nunca la
101
+ > incluyas en código que llegue al navegador.
102
+
103
+ ## Recursos
104
+
105
+ Todos los métodos devuelven promesas con el recurso tipado. Los recursos con listado tienen
106
+ `create`, `retrieve`, `update`, `list` e `iterate`, además de sus acciones propias:
107
+
108
+ | Recurso | Acciones propias |
109
+ | --- | --- |
110
+ | `customers` | — |
111
+ | `customerSessions` | `create` |
112
+ | `products` | `retire`, `retireVariant` |
113
+ | `plans` | — |
114
+ | `features` | `check`, `isEnabled`, `listForCustomer`, `consume`, `release`, `set` |
115
+ | `coupons` | — |
116
+ | `checkouts` | `expire` |
117
+ | `paymentLinks` | `retire` |
118
+ | `subscriptions` | `cancel`, `resume` |
119
+ | `payments` | `refund`, `listRefunds` |
120
+ | `payouts` | — |
121
+ | `balance` | `retrieve` |
122
+ | `webhookEndpoints` | `delete` |
123
+ | `events` | `retrieve`, `list`, `iterate` |
124
+ | `exchangeRates` | `list` |
125
+ | `currencies` | `list` |
126
+
127
+ ### Ejemplo: plan con cuotas
26
128
 
27
129
  ```ts
28
130
  await billete.features.create({
@@ -43,48 +145,125 @@ const plan = await billete.plans.create({
43
145
  features: { tokens: 100_000 },
44
146
  });
45
147
 
46
- const checkout = await billete.checkouts.create({
47
- plan: plan.id,
48
- months: 1,
49
- customer: { email: "ana@example.com", externalId: "user_123" },
50
- });
51
- // Redirige al cliente a checkout.url
52
-
148
+ // `customer` acepta el ID de Billete (cus_…) o tu propio externalId.
53
149
  const { allowed } = await billete.features.consume("user_123", "tokens", 850);
54
150
  ```
55
151
 
56
- Los montos son siempre enteros en unidad menor (céntimos): `1_000` son 10,00 USD. Las mutaciones
57
- llevan una `Idempotency-Key` automática, así los reintentos ante fallos de red nunca duplican un
58
- cobro. Los errores de la API llegan como `BilleteError`, con `code` estable y `status`.
152
+ `consume` es atómico: si el consumo supera el límite, no se descuenta nada y `allowed` es
153
+ `false`.
154
+
155
+ ## Montos
156
+
157
+ Los montos son siempre **enteros en unidad menor** (céntimos), nunca decimales: `1_000` en `USD`
158
+ son 10,00 USD y `1_000` en `VES` son 10,00 Bs. Las tasas de cambio llegan como texto decimal para
159
+ no perder precisión.
160
+
161
+ ## Paginación
162
+
163
+ `list` devuelve una página (`{ data, nextCursor }`); `iterate` recorre todas por ti:
164
+
165
+ ```ts
166
+ const page = await billete.payments.list({ limit: 50 });
167
+ const next = await billete.payments.list({ limit: 50, cursor: page.nextCursor ?? undefined });
168
+
169
+ for await (const payment of billete.payments.iterate()) {
170
+ console.log(payment.id, payment.amount);
171
+ }
172
+ ```
173
+
174
+ ## Errores
175
+
176
+ Toda respuesta fallida lanza un `BilleteError`:
177
+
178
+ ```ts
179
+ import { BilleteError } from "@boelabs/billete-sdk";
180
+
181
+ try {
182
+ await billete.payments.refund("pay_…");
183
+ } catch (error) {
184
+ if (error instanceof BilleteError && error.code === "payment_not_refundable") {
185
+ // …
186
+ }
187
+ throw error;
188
+ }
189
+ ```
190
+
191
+ | Propiedad | Descripción |
192
+ | --- | --- |
193
+ | `type` | Categoría: `invalid_request`, `authentication`, `permission`, `not_found`, `conflict`, `validation`, `rate_limit`, `provider`, `internal` o `network`. |
194
+ | `code` | Código estable, apto para decidir en tu código. |
195
+ | `message` | Descripción legible del error. |
196
+ | `status` | Código HTTP (`0` si no hubo respuesta). |
197
+ | `param` | Campo que causó el error, cuando aplica. |
198
+ | `requestId` | Identificador de la petición; inclúyelo al pedir soporte. |
199
+
200
+ ## Idempotencia y reintentos
201
+
202
+ Cada mutación envía una `Idempotency-Key` generada automáticamente, que se mantiene entre
203
+ reintentos. Si quieres que un reintento tuyo (por ejemplo, de un job) también sea seguro, pasa
204
+ una clave estable:
205
+
206
+ ```ts
207
+ await billete.payments.refund("pay_…", {}, { idempotencyKey: `refund-${orderId}` });
208
+ ```
209
+
210
+ Todos los métodos aceptan como último argumento `{ idempotencyKey?, signal? }`; `signal` permite
211
+ cancelar la petición con un `AbortController`.
59
212
 
60
213
  ## Navegador
61
214
 
62
- Sin clave: tu servidor crea una sesión de cliente de corta duración
63
- (`billete.customerSessions.create`) y el navegador la usa para leer el estado de ese cliente.
215
+ El cliente de navegador nunca usa tu clave secreta. Tu servidor crea una **sesión de cliente**
216
+ de corta duración y se la entrega al frontend:
64
217
 
65
218
  ```ts
219
+ // Servidor
220
+ const session = await billete.customerSessions.create({ customer: "user_123" });
221
+ return { token: session.token };
222
+ ```
223
+
224
+ ```ts
225
+ // Navegador
66
226
  import { createClient } from "@boelabs/billete-sdk/client";
67
227
 
68
- const billete = createClient({ customerSession: tokenFromYourServer });
69
- const [subscription] = (await billete.customer.subscriptions()).data;
70
- // subscription.renewal → { status: "due" | "overdue", daysRemaining, checkoutUrl, … }
228
+ const billete = createClient({ customerSession: token });
229
+
230
+ const { data: subscriptions } = await billete.customer.subscriptions();
231
+ const tokens = await billete.customer.feature("tokens");
71
232
  ```
72
233
 
234
+ `customerSession` también acepta una función, útil cuando el token se renueva. Sin sesión, el
235
+ cliente puede leer y pagar checkouts y links de pago, y consultar tasas y monedas.
236
+
73
237
  ## Webhooks
74
238
 
75
- Firma [Standard Webhooks](https://www.standardwebhooks.com). Pasa el cuerpo tal como llegó:
239
+ Billete firma los webhooks con [Standard Webhooks](https://www.standardwebhooks.com). Verifica la
240
+ firma con el cuerpo **exactamente como llegó**, antes de parsearlo:
76
241
 
77
242
  ```ts
78
- import { verifyWebhook } from "@boelabs/billete-sdk/webhooks";
243
+ import { verifyWebhook, WebhookVerificationError } from "@boelabs/billete-sdk/webhooks";
79
244
 
80
- const event = await verifyWebhook({
81
- payload: rawBody,
82
- headers: request.headers,
83
- secret: process.env.BILLETE_WEBHOOK_SECRET!,
84
- });
85
- if (event.type === "subscription.past_due") notify(event.data.customerId);
245
+ export async function POST(request: Request) {
246
+ try {
247
+ const event = await verifyWebhook({
248
+ payload: await request.text(),
249
+ headers: request.headers,
250
+ secret: process.env.BILLETE_WEBHOOK_SECRET!,
251
+ });
252
+
253
+ if (event.type === "checkout.completed") {
254
+ // event.data es un Checkout
255
+ }
256
+ return new Response(null, { status: 204 });
257
+ } catch (error) {
258
+ if (error instanceof WebhookVerificationError) return new Response(null, { status: 400 });
259
+ throw error;
260
+ }
261
+ }
86
262
  ```
87
263
 
264
+ `event` es una unión discriminada por `type`, así que `event.data` tiene el tipo correcto en cada
265
+ rama. Se rechazan las firmas con más de 5 minutos (`toleranceSeconds`).
266
+
88
267
  ## Licencia
89
268
 
90
- MIT. El SDK es el único código público de Billete; el resto del proyecto es propietario.
269
+ [MIT](./LICENSE)
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as segment, r as BilleteError, t as createTransport } from "./http-kkjWt1D4.js";
1
+ import { n as segment, r as BilleteError, t as createTransport } from "./http-CTnngMSn.js";
2
2
  //#region src/create-client.ts
3
3
  /**
4
4
  * Browser client. It never holds a secret key: checkouts are addressed by their unguessable ID
@@ -18,7 +18,7 @@ var BilleteError = class extends Error {
18
18
  };
19
19
  //#endregion
20
20
  //#region src/http.ts
21
- const SDK_VERSION = "0.3.2";
21
+ const SDK_VERSION = "0.3.3";
22
22
  const DEFAULT_BASE_URL = "https://api.billete.run";
23
23
  const RETRYABLE_STATUS = /* @__PURE__ */ new Set([
24
24
  429,
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as segment, r as BilleteError, t as createTransport } from "./http-kkjWt1D4.js";
1
+ import { n as segment, r as BilleteError, t as createTransport } from "./http-CTnngMSn.js";
2
2
  import { WebhookVerificationError, verifyWebhook } from "./webhooks.js";
3
3
  //#region ../contracts/src/environment.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boelabs/billete-sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Billete SDK: server client, browser client and webhook verification",
5
5
  "keywords": [
6
6
  "billete",