@pimia/sdk 0.1.0 → 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/README.md +90 -7
- package/dist/api.d.ts +1217 -313
- package/dist/client.d.ts +440 -14
- package/dist/client.js +75 -12
- package/dist/index.d.ts +10 -1
- package/dist/index.js +8 -0
- package/dist/webhooks.d.ts +305 -0
- package/dist/webhooks.js +230 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,13 +12,10 @@ Requisitos: Node ≥ 20 (o cualquier runtime con `fetch` y WebCrypto global).
|
|
|
12
12
|
npm install @pimia/sdk
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
> tarball (`npm pack`). Ojo, `npm install git+https://…` **no** vale: npm
|
|
20
|
-
> instalaría la raíz del monorepo, no `typescript/`. El detalle está en el
|
|
21
|
-
> [README del monorepo](https://github.com/Pimia-AI/pimia-sdks#instalación).
|
|
15
|
+
Publicado desde v0.1.0, con
|
|
16
|
+
[provenance SLSA](https://docs.npmjs.com/generating-provenance-statements)
|
|
17
|
+
firmada por el workflow de release: el tarball es verificablemente
|
|
18
|
+
[este repositorio](https://github.com/Pimia-AI/pimia-sdks).
|
|
22
19
|
|
|
23
20
|
## Uso en 20 líneas
|
|
24
21
|
|
|
@@ -66,6 +63,92 @@ cliente exige un `TokenStore` en lugar de un string: persiste el conjunto de
|
|
|
66
63
|
tokens tras cada refresco y no refresques dos veces en paralelo con el mismo
|
|
67
64
|
token. Las dos cosas las cubre el SDK si lo usas como está pensado.
|
|
68
65
|
|
|
66
|
+
## Reintentar un `POST` sin duplicar
|
|
67
|
+
|
|
68
|
+
Manda una `Idempotency-Key` única por operación y Pimia ejecuta la escritura
|
|
69
|
+
una sola vez, por muchos reintentos que haya:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const clave = crypto.randomUUID()
|
|
73
|
+
await client.estimates.create(presupuesto, { idempotencyKey: clave })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Reúsala **solo** en los reintentos de esa misma operación: la misma clave con
|
|
77
|
+
otro cuerpo responde `422`.
|
|
78
|
+
|
|
79
|
+
Tras un reintento el cuerpo que recibes es idéntico al de la primera llamada
|
|
80
|
+
—ese es justo el contrato—, así que el cuerpo solo no dice si Pimia escribió o
|
|
81
|
+
se limitó a repetirse. Para saberlo, `requestWithMeta`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const { data, meta } = await client.requestWithMeta('/estimates', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: presupuesto,
|
|
87
|
+
idempotencyKey: clave,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (meta.idempotentReplay) {
|
|
91
|
+
// ya existía: no se ha creado nada nuevo
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Recibir webhooks
|
|
96
|
+
|
|
97
|
+
`verifyWebhook` comprueba la firma `PIMIA-WEBHOOK-v1` y te devuelve el evento
|
|
98
|
+
tipado. No reimplementes el HMAC:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import express from 'express'
|
|
102
|
+
import { verifyWebhook, WebhookVerificationError } from '@pimia/sdk'
|
|
103
|
+
|
|
104
|
+
// ⚠️ express.raw(), NO express.json(): Pimia firma los bytes que envía, y
|
|
105
|
+
// parsear + volver a serializar rompe la firma sin que se vea por qué.
|
|
106
|
+
app.post('/pimia', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
107
|
+
let hook
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
hook = await verifyWebhook({
|
|
111
|
+
secret: process.env.PIMIA_WEBHOOK_SECRET,
|
|
112
|
+
headers: req.headers,
|
|
113
|
+
body: req.body,
|
|
114
|
+
})
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return res.status(400).send((error as WebhookVerificationError).reason)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Pimia reintenta: la misma entrega llega con el mismo `delivery`.
|
|
120
|
+
// Procesar cada uno una sola vez es todo el exactly-once que necesitas.
|
|
121
|
+
if (await yaProcesado(hook.delivery)) return res.sendStatus(200)
|
|
122
|
+
|
|
123
|
+
if (hook.known) {
|
|
124
|
+
switch (hook.event) {
|
|
125
|
+
case 'estimate.accepted':
|
|
126
|
+
await facturar(hook.payload.id) // payload tipado, sin castings
|
|
127
|
+
break
|
|
128
|
+
case 'invoice.paid':
|
|
129
|
+
await cobrar(hook.payload.id)
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
res.sendStatus(200) // responde rápido; el trabajo pesado, a una cola
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Los ocho eventos del catálogo (`approval.decided`, `invoice.received`,
|
|
139
|
+
`app.revoked`, `customer.created`, `customer.updated`, `invoice.created`,
|
|
140
|
+
`estimate.accepted`, `invoice.paid`) vienen tipados. Uno que este SDK todavía
|
|
141
|
+
no conozca **no es un error**: se verifica igual y llega con `known: false`.
|
|
142
|
+
|
|
143
|
+
Detalles que ahorran un rato:
|
|
144
|
+
|
|
145
|
+
- `secret` acepta una **lista** de secretos, para rotarlo sin ventana de caída.
|
|
146
|
+
- La ventana anti-replay son 300 s; ajústala con `toleranceSeconds`.
|
|
147
|
+
- Los errores traen un `reason` (`signature_mismatch`, `timestamp_out_of_window`,
|
|
148
|
+
`missing_headers`, `invalid_timestamp`, `invalid_json`) para tus métricas.
|
|
149
|
+
- `signWebhook()` firma un cuerpo como lo haría Pimia: úsalo en **tus tests**,
|
|
150
|
+
no en producción.
|
|
151
|
+
|
|
69
152
|
## Más
|
|
70
153
|
|
|
71
154
|
Documentación completa, modelo mental (un tenant = una base URL = un token),
|