@pimia/sdk 0.6.0 → 0.7.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 CHANGED
@@ -92,6 +92,56 @@ if (meta.idempotentReplay) {
92
92
  }
93
93
  ```
94
94
 
95
+ ## Subir un fichero
96
+
97
+ Diez operaciones de la API son `multipart/form-data`: el justificante de un
98
+ gasto, el documento de una factura recibida, un extracto bancario, el membrete
99
+ de una plantilla, el certificado de firma, el avatar. Para ésas pásale un
100
+ `FormData` y el cliente lo manda tal cual — **no le pongas `content-type`**: el
101
+ runtime escribe el suyo con el `boundary` que separa las partes, y una cabecera
102
+ puesta a mano se lo quita (el cliente lo rechaza antes de salir, con un aviso
103
+ que lo explica).
104
+
105
+ `toFormData` hace las tres conversiones que el servidor espera y que `FormData`
106
+ sola no hace: los booleanos como `1`/`0`, los objetos y arrays como cadena
107
+ JSON, y los `null` omitidos en vez de mandados como la cadena `"null"`.
108
+
109
+ ```ts
110
+ import { toFormData } from '@pimia/sdk'
111
+
112
+ // Un gasto con su justificante en PDF, de una sola llamada.
113
+ await client.post('/expenses', toFormData({
114
+ expense_date: '2026-08-24',
115
+ expense_category_id: 3,
116
+ amount: 12100, // céntimos, como todo importe
117
+ attachment_receipt: ficheroDelInput, // un File del navegador
118
+ customFields: [{ id: 3, value: 'REF-42' }],
119
+ }))
120
+
121
+ // El documento de una factura recibida, con un Blob al que le das nombre.
122
+ const form = new FormData()
123
+ form.append('document', blobPdf, 'factura-proveedor.pdf')
124
+ await client.post(`/received-invoices/${id}/upload/document`, form)
125
+ ```
126
+
127
+ Los campos de fichero salen tipados como `Blob` en `@pimia/sdk/api`, así que un
128
+ `File` del navegador encaja sin ceremonia.
129
+
130
+ ⚠️ Lo que **no** puedes pasar es un `ReadableStream`: el cliente reintenta ante
131
+ un 401 y ante un 429, y un cuerpo de un solo uso no se puede volver a mandar.
132
+
133
+ ## Descargar un fichero
134
+
135
+ Para las dos operaciones que devuelven un binario, `download`:
136
+
137
+ ```ts
138
+ const pdf = await client.download(`/received-invoices/${id}/show/document`)
139
+ const url = URL.createObjectURL(pdf)
140
+ ```
141
+
142
+ ⚠️ **No uses `get()` para esto.** Lee la respuesta con `response.text()`, así
143
+ que un PDF llega entero de tamaño y no se abre — sin ningún error que mirar.
144
+
95
145
  ## Recibir webhooks
96
146
 
97
147
  `verifyWebhook` comprueba la firma `PIMIA-WEBHOOK-v1` y te devuelve el evento
package/dist/api.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * This file was auto-generated by openapi-typescript.
3
3
  * Do not make direct changes to the file.
4
+ *
5
+ * Generado con `npm run generate:types` (scripts/generate-types.mjs), que
6
+ * añade UN transform al generador: `format: binary` sale como `Blob` y no
7
+ * como `string`. El porqué, en ese fichero.
4
8
  */
5
9
  export interface paths {
6
10
  "/absences/balance": {
@@ -4043,7 +4047,7 @@ export interface components {
4043
4047
  * Format: binary
4044
4048
  * @description Maximum file size: 20000 kilobytes.
4045
4049
  */
4046
- admin_avatar?: string | null;
4050
+ admin_avatar?: Blob | null;
4047
4051
  avatar?: string | null;
4048
4052
  };
4049
4053
  /** BankAccount */
@@ -5067,7 +5071,7 @@ export interface components {
5067
5071
  * Format: binary
5068
5072
  * @description Maximum file size: 20000 kilobytes.
5069
5073
  */
5070
- attachment_receipt?: string | null;
5074
+ attachment_receipt?: Blob | null;
5071
5075
  /** @description Valores de campo personalizado del recurso. `id` es el de la definición, que se descubre en `GET /custom-fields` (catálogo `meta`, legible con cualquier token); las definiciones las crea el dueño del tenant desde su panel. Se devuelven en la clave `fields` del recurso. En `multipart/form-data` viaja como cadena JSON: `[{"id":3,"value":"REF-42"}]`. */
5072
5076
  customFields?: string;
5073
5077
  /** @description Borra el recibo adjunto del gasto. Solo surte efecto en la actualización; en `multipart/form-data` viaja como `1` o `0`. */
@@ -7778,7 +7782,7 @@ export interface operations {
7778
7782
  * Format: binary
7779
7783
  * @description Maximum file size: 10240 kilobytes.
7780
7784
  */
7781
- file: string;
7785
+ file: Blob;
7782
7786
  bank_account_id: number;
7783
7787
  /** @enum {string} */
7784
7788
  format: "norma43" | "csv" | "auto";
@@ -10925,7 +10929,7 @@ export interface operations {
10925
10929
  [name: string]: unknown;
10926
10930
  };
10927
10931
  content: {
10928
- "application/octet-stream": string;
10932
+ "application/octet-stream": Blob;
10929
10933
  };
10930
10934
  };
10931
10935
  404: components["responses"]["ModelNotFoundException"];
@@ -10948,7 +10952,7 @@ export interface operations {
10948
10952
  * Format: binary
10949
10953
  * @description Maximum file size: 5120 kilobytes.
10950
10954
  */
10951
- letterhead: string;
10955
+ letterhead: Blob;
10952
10956
  };
10953
10957
  };
10954
10958
  };
@@ -11522,7 +11526,7 @@ export interface operations {
11522
11526
  * Format: binary
11523
11527
  * @description Maximum file size: 5120 kilobytes.
11524
11528
  */
11525
- file: string;
11529
+ file: Blob;
11526
11530
  };
11527
11531
  };
11528
11532
  };
@@ -13044,7 +13048,7 @@ export interface operations {
13044
13048
  * Format: binary
13045
13049
  * @description Maximum file size: 10240 kilobytes.
13046
13050
  */
13047
- certificate: string;
13051
+ certificate: Blob;
13048
13052
  password: string;
13049
13053
  };
13050
13054
  };
@@ -13279,7 +13283,7 @@ export interface operations {
13279
13283
  [name: string]: unknown;
13280
13284
  };
13281
13285
  content: {
13282
- "application/octet-stream": string;
13286
+ "application/octet-stream": Blob;
13283
13287
  };
13284
13288
  };
13285
13289
  404: components["responses"]["ModelNotFoundException"];
@@ -13302,7 +13306,7 @@ export interface operations {
13302
13306
  * Format: binary
13303
13307
  * @description Maximum file size: 10240 kilobytes.
13304
13308
  */
13305
- document: string;
13309
+ document: Blob;
13306
13310
  };
13307
13311
  };
13308
13312
  };
package/dist/api.js CHANGED
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * This file was auto-generated by openapi-typescript.
3
3
  * Do not make direct changes to the file.
4
+ *
5
+ * Generado con `npm run generate:types` (scripts/generate-types.mjs), que
6
+ * añade UN transform al generador: `format: binary` sale como `Blob` y no
7
+ * como `string`. El porqué, en ese fichero.
4
8
  */
5
9
  export {};
package/dist/client.d.ts CHANGED
@@ -64,8 +64,43 @@ export interface RequestOptions {
64
64
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
65
65
  /** Query string. Los `undefined`/`null` se omiten; los arrays se repiten. */
66
66
  query?: Record<string, string | number | boolean | undefined | null | Array<string | number>>;
67
- /** Cuerpo JSON. */
67
+ /**
68
+ * El cuerpo de la petición.
69
+ *
70
+ * Por defecto se manda como **JSON**, que es lo que pide casi toda la API.
71
+ *
72
+ * Diez operaciones del contrato son `multipart/form-data` —el justificante
73
+ * de un gasto, el documento de una factura recibida, importar un extracto
74
+ * bancario, el membrete de una plantilla, el certificado de firma…—, y para
75
+ * ésas se pasa un {@link FormData}: el cliente lo manda **tal cual** y **no
76
+ * le pone `content-type`**, para que el runtime escriba el suyo con su
77
+ * `boundary`. {@link toFormData} lo arma con las conversiones que el
78
+ * servidor espera.
79
+ *
80
+ * También pasan sin tocar `Blob`, `URLSearchParams`, `ArrayBuffer` y las
81
+ * vistas de `ArrayBuffer`.
82
+ *
83
+ * ⛔ Un `ReadableStream` **no**, y es a propósito: este cliente reintenta
84
+ * ante un 401 (tras refrescar) y ante un 429, y un stream ya consumido no se
85
+ * puede volver a mandar — el reintento fallaría con un error que no se
86
+ * parece en nada a su causa. Los cinco de arriba se pueden releer.
87
+ */
68
88
  body?: unknown;
89
+ /**
90
+ * Cómo leer una respuesta **correcta**.
91
+ *
92
+ * `'json'` (el defecto) es lo de siempre. `'blob'` es para las dos
93
+ * operaciones que devuelven un fichero (`application/octet-stream`):
94
+ * descargar el membrete de una plantilla y el documento escaneado de una
95
+ * factura recibida.
96
+ *
97
+ * ⚠️ Sin esto, un PDF se lee con `response.text()` y **se corrompe en
98
+ * silencio**: el fichero «llega», pesa lo suyo y no se abre.
99
+ *
100
+ * Los errores se siguen leyendo como JSON aunque pidas `'blob'` — cuando la
101
+ * API falla contesta su sobre de error, no el fichero.
102
+ */
103
+ responseType?: 'json' | 'blob';
69
104
  headers?: Record<string, string>;
70
105
  signal?: AbortSignal;
71
106
  /**
@@ -531,6 +566,25 @@ export declare class PimiaClient {
531
566
  put<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
532
567
  patch<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
533
568
  delete<T = unknown>(path: string, options?: ReadOptions): Promise<T>;
569
+ /**
570
+ * Descarga un fichero de la API y te lo da como `Blob`.
571
+ *
572
+ * Son dos operaciones: el membrete de una plantilla
573
+ * (`GET /invoice-templates/{id}/letterhead`) y el documento escaneado de una
574
+ * factura recibida (`GET /received-invoices/{id}/show/document`).
575
+ *
576
+ * Existe porque `get()` **corrompe un binario sin decirlo**: lee la
577
+ * respuesta con `response.text()`, y un PDF pasado por ahí llega entero de
578
+ * tamaño y no se abre. Ese es el peor final posible para una descarga, así
579
+ * que la forma correcta tiene nombre propio en vez de ser una bandera que
580
+ * hay que acordarse de poner.
581
+ *
582
+ * ```ts
583
+ * const pdf = await client.download(`/received-invoices/${id}/show/document`)
584
+ * const url = URL.createObjectURL(pdf)
585
+ * ```
586
+ */
587
+ download(path: string, query?: RequestOptions['query'], options?: ReadOptions): Promise<Blob>;
534
588
  /**
535
589
  * Petición cruda contra `/api/v1`. `path` puede llevar el prefijo o no:
536
590
  * `/invoices` y `/api/v1/invoices` son lo mismo.
@@ -565,4 +619,35 @@ export declare class PimiaClient {
565
619
  private captureRateLimit;
566
620
  private retryDelay;
567
621
  }
622
+ /**
623
+ * Arma el `FormData` de una operación multipart con las conversiones que el
624
+ * servidor de Pimia espera, que **no** son las que hace `FormData` sola.
625
+ *
626
+ * Tres reglas, y las tres salen del contrato, no de la costumbre:
627
+ *
628
+ * - **Los booleanos viajan como `1` y `0`.** Lo dice el propio spec en
629
+ * `ExpenseRequest.is_attachment_receipt_removed`: «en `multipart/form-data`
630
+ * viaja como `1` o `0`». Un `String(false)` daría `"false"`, que PHP lee
631
+ * como verdadero.
632
+ * - **Los objetos y arrays viajan como JSON en una cadena.** También del
633
+ * spec, en `ExpenseRequest.customFields`: «viaja como cadena JSON:
634
+ * `[{"id":3,"value":"REF-42"}]`».
635
+ * - **`null` y `undefined` se omiten**, en vez de mandar `"null"`. Un campo
636
+ * ausente es un campo ausente; la cadena `"null"` es un valor.
637
+ *
638
+ * Un `Blob` o un `File` se añaden tal cual. Con un `File` el runtime manda ya
639
+ * su nombre; con un `Blob` suelto se puede dar uno pasando `[blob, 'x.pdf']`,
640
+ * que es la forma que el tercer argumento de `append` admite.
641
+ *
642
+ * ```ts
643
+ * await client.post('/expenses', toFormData({
644
+ * expense_date: '2026-08-24',
645
+ * expense_category_id: 3,
646
+ * amount: 12100,
647
+ * attachment_receipt: ficheroPdf,
648
+ * customFields: [{ id: 3, value: 'REF-42' }],
649
+ * }))
650
+ * ```
651
+ */
652
+ export declare function toFormData(fields: Record<string, unknown | [Blob, string]>): FormData;
568
653
  export {};
package/dist/client.js CHANGED
@@ -127,6 +127,32 @@ export class PimiaClient {
127
127
  delete(path, options) {
128
128
  return this.request(path, { ...options, method: 'DELETE' });
129
129
  }
130
+ /**
131
+ * Descarga un fichero de la API y te lo da como `Blob`.
132
+ *
133
+ * Son dos operaciones: el membrete de una plantilla
134
+ * (`GET /invoice-templates/{id}/letterhead`) y el documento escaneado de una
135
+ * factura recibida (`GET /received-invoices/{id}/show/document`).
136
+ *
137
+ * Existe porque `get()` **corrompe un binario sin decirlo**: lee la
138
+ * respuesta con `response.text()`, y un PDF pasado por ahí llega entero de
139
+ * tamaño y no se abre. Ese es el peor final posible para una descarga, así
140
+ * que la forma correcta tiene nombre propio en vez de ser una bandera que
141
+ * hay que acordarse de poner.
142
+ *
143
+ * ```ts
144
+ * const pdf = await client.download(`/received-invoices/${id}/show/document`)
145
+ * const url = URL.createObjectURL(pdf)
146
+ * ```
147
+ */
148
+ download(path, query, options) {
149
+ return this.request(path, {
150
+ ...options,
151
+ method: 'GET',
152
+ query,
153
+ responseType: 'blob',
154
+ });
155
+ }
130
156
  /**
131
157
  * Petición cruda contra `/api/v1`. `path` puede llevar el prefijo o no:
132
158
  * `/invoices` y `/api/v1/invoices` son lo mismo.
@@ -152,6 +178,13 @@ export class PimiaClient {
152
178
  * ```
153
179
  */
154
180
  async requestWithMeta(path, options = {}) {
181
+ /* El cuerpo se clasifica UNA vez, fuera del bucle: lo que se manda no
182
+ cambia entre el intento y su reintento, y decidirlo dentro invitaría a
183
+ que algún día dejaran de coincidir. */
184
+ const cuerpoNativo = esCuerpoNativo(options.body);
185
+ if (cuerpoNativo) {
186
+ exigirSinContentType(options.body, { ...this.extraHeaders, ...options.headers });
187
+ }
155
188
  let tokens = await this.currentTokens();
156
189
  if (isExpired(tokens, this.skew)) {
157
190
  tokens = await this.refreshTokens(tokens);
@@ -162,8 +195,17 @@ export class PimiaClient {
162
195
  const response = await this.doFetch(this.urlFor(path, options.query), {
163
196
  method: options.method ?? 'GET',
164
197
  headers: {
165
- accept: 'application/json',
166
- ...(options.body === undefined ? {} : { 'content-type': 'application/json' }),
198
+ /* Una descarga no pide JSON: si se dejara `application/json` fijo, un
199
+ servidor que negocie el tipo tendría derecho a contestar 406 —o a
200
+ mandar un JSON de error donde se esperaba el fichero. */
201
+ accept: options.responseType === 'blob' ? '*/*' : 'application/json',
202
+ /* Un cuerpo nativo trae su propio tipo: el runtime le pone
203
+ `multipart/form-data` CON su `boundary`, o el de un `Blob`, o
204
+ `application/x-www-form-urlencoded`. Escribirlo aquí a mano se lo
205
+ quitaría, y sin `boundary` el servidor no puede parsear nada. */
206
+ ...(options.body === undefined || cuerpoNativo
207
+ ? {}
208
+ : { 'content-type': 'application/json' }),
167
209
  ...this.extraHeaders,
168
210
  ...options.headers,
169
211
  // Después de `options.headers` para que la opción con nombre mande
@@ -174,13 +216,23 @@ export class PimiaClient {
174
216
  : { 'idempotency-key': options.idempotencyKey }),
175
217
  authorization: `Bearer ${tokens.accessToken}`,
176
218
  },
177
- body: options.body === undefined ? undefined : JSON.stringify(options.body),
219
+ body: options.body === undefined
220
+ ? undefined
221
+ : cuerpoNativo
222
+ ? options.body
223
+ : JSON.stringify(options.body),
178
224
  signal: options.signal,
179
225
  });
180
226
  this.captureRateLimit(response);
181
227
  if (response.ok) {
182
228
  return {
183
- data: (await parseBody(response)),
229
+ /* Una descarga se devuelve como `Blob` SIN pasar por `parseBody`,
230
+ que hace `response.text()`: un PDF leído como texto se corrompe en
231
+ la primera secuencia que no sea UTF-8 válido, y lo hace en
232
+ silencio — el fichero «llega» y no se abre. */
233
+ data: (options.responseType === 'blob'
234
+ ? await response.blob()
235
+ : await parseBody(response)),
184
236
  meta: {
185
237
  status: response.status,
186
238
  // Presente solo cuando Pimia reproduce; su ausencia significa
@@ -287,6 +339,110 @@ export class PimiaClient {
287
339
  return Math.min(base, this.maxRetryDelayMs);
288
340
  }
289
341
  }
342
+ /**
343
+ * ¿Es un cuerpo que el runtime serializa por su cuenta?
344
+ *
345
+ * Los cinco de la lista tienen dos cosas en común, y las dos importan: `fetch`
346
+ * sabe convertirlos y **se pueden releer**. Lo segundo es lo que decide quién
347
+ * entra: este cliente reintenta ante un 401 (después de refrescar) y ante un
348
+ * 429, así que un cuerpo de un solo uso —un `ReadableStream`— reventaría en el
349
+ * reintento con un «body already used» que no se parece en nada a su causa.
350
+ *
351
+ * Los `typeof … !== 'undefined'` no son celo: este paquete corre en Node y en
352
+ * el navegador, y aunque Node 20 los trae todos, un runtime recortado que no
353
+ * tenga `FormData` debe fallar en el `instanceof`, no al evaluarlo.
354
+ */
355
+ function esCuerpoNativo(body) {
356
+ if (body === undefined || body === null)
357
+ return false;
358
+ return ((typeof FormData !== 'undefined' && body instanceof FormData) ||
359
+ (typeof Blob !== 'undefined' && body instanceof Blob) ||
360
+ (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) ||
361
+ body instanceof ArrayBuffer ||
362
+ ArrayBuffer.isView(body));
363
+ }
364
+ /**
365
+ * Un `FormData` con un `content-type` puesto a mano **no se manda**: se avisa.
366
+ *
367
+ * La cabecera de un multipart lleva el `boundary` que separa las partes, y lo
368
+ * genera el runtime al serializar. Escribir `content-type:
369
+ * multipart/form-data` a mano se lo quita, y entonces el servidor recibe un
370
+ * cuerpo que no puede parsear: contesta un 422 sobre un campo obligatorio que
371
+ * el cliente **sí mandó**, y el rastro no lleva a ninguna parte.
372
+ *
373
+ * Es un error de quien llama, no de la API, así que se lanza aquí y no se
374
+ * intenta arreglar por su cuenta: quitarle la cabecera en silencio dejaría en
375
+ * pie la creencia de que hacía falta.
376
+ */
377
+ function exigirSinContentType(body, headers) {
378
+ if (typeof FormData === 'undefined' || !(body instanceof FormData))
379
+ return;
380
+ const puesta = Object.keys(headers).find((k) => k.toLowerCase() === 'content-type');
381
+ if (puesta === undefined)
382
+ return;
383
+ throw new TypeError('No le pongas `content-type` a un cuerpo FormData: el runtime escribe el suyo ' +
384
+ 'con el `boundary` que separa las partes, y una cabecera a mano se lo quita ' +
385
+ `(el servidor respondería 422 sobre un campo que sí mandaste). Quita \`${puesta}\` ` +
386
+ 'de las cabeceras de esta petición.');
387
+ }
388
+ /**
389
+ * Arma el `FormData` de una operación multipart con las conversiones que el
390
+ * servidor de Pimia espera, que **no** son las que hace `FormData` sola.
391
+ *
392
+ * Tres reglas, y las tres salen del contrato, no de la costumbre:
393
+ *
394
+ * - **Los booleanos viajan como `1` y `0`.** Lo dice el propio spec en
395
+ * `ExpenseRequest.is_attachment_receipt_removed`: «en `multipart/form-data`
396
+ * viaja como `1` o `0`». Un `String(false)` daría `"false"`, que PHP lee
397
+ * como verdadero.
398
+ * - **Los objetos y arrays viajan como JSON en una cadena.** También del
399
+ * spec, en `ExpenseRequest.customFields`: «viaja como cadena JSON:
400
+ * `[{"id":3,"value":"REF-42"}]`».
401
+ * - **`null` y `undefined` se omiten**, en vez de mandar `"null"`. Un campo
402
+ * ausente es un campo ausente; la cadena `"null"` es un valor.
403
+ *
404
+ * Un `Blob` o un `File` se añaden tal cual. Con un `File` el runtime manda ya
405
+ * su nombre; con un `Blob` suelto se puede dar uno pasando `[blob, 'x.pdf']`,
406
+ * que es la forma que el tercer argumento de `append` admite.
407
+ *
408
+ * ```ts
409
+ * await client.post('/expenses', toFormData({
410
+ * expense_date: '2026-08-24',
411
+ * expense_category_id: 3,
412
+ * amount: 12100,
413
+ * attachment_receipt: ficheroPdf,
414
+ * customFields: [{ id: 3, value: 'REF-42' }],
415
+ * }))
416
+ * ```
417
+ */
418
+ export function toFormData(fields) {
419
+ const form = new FormData();
420
+ for (const [name, value] of Object.entries(fields)) {
421
+ if (value === undefined || value === null)
422
+ continue;
423
+ if (Array.isArray(value) && value.length === 2 && esBlob(value[0]) && typeof value[1] === 'string') {
424
+ form.append(name, value[0], value[1]);
425
+ continue;
426
+ }
427
+ if (esBlob(value)) {
428
+ form.append(name, value);
429
+ continue;
430
+ }
431
+ if (typeof value === 'boolean') {
432
+ form.append(name, value ? '1' : '0');
433
+ continue;
434
+ }
435
+ if (typeof value === 'object') {
436
+ form.append(name, JSON.stringify(value));
437
+ continue;
438
+ }
439
+ form.append(name, String(value));
440
+ }
441
+ return form;
442
+ }
443
+ function esBlob(value) {
444
+ return typeof Blob !== 'undefined' && value instanceof Blob;
445
+ }
290
446
  function retryAfterSeconds(response) {
291
447
  const header = response.headers.get('retry-after');
292
448
  if (header === null)
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * por README.md; el contrato completo de endpoints está en el OpenAPI del que
6
6
  * salen los tipos de `./api`.
7
7
  */
8
- export { PimiaClient } from './client.js';
8
+ export { PimiaClient, toFormData } from './client.js';
9
9
  export type { CustomerRequest, CustomerResource, EstimateResource, EstimatesRequest, InvoiceResource, InvoicesRequest, PimiaClientOptions, RateLimit, ReadOptions, RequestOptions, ResourceEnvelope, ResponseMeta, ResponseWithMeta, WriteOptions, } from './client.js';
10
10
  export { OAuth, createPkceChallenge, createState } from './oauth.js';
11
11
  export type { AuthorizationServerMetadata, AuthorizeUrlOptions, OAuthConfig, PkceChallenge, } from './oauth.js';
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * por README.md; el contrato completo de endpoints está en el OpenAPI del que
6
6
  * salen los tipos de `./api`.
7
7
  */
8
- export { PimiaClient } from './client.js';
8
+ export { PimiaClient, toFormData } from './client.js';
9
9
  export { OAuth, createPkceChallenge, createState } from './oauth.js';
10
10
  export { MemoryTokenStore, isExpired, tokenSetFromResponse } from './tokens.js';
11
11
  export { DuplicateExternalRefError, ForbiddenError, MissingScopeError, NotAuthenticatedError, NotFoundError, OAuthError, PimiaApiError, PimiaError, RateLimitError, UnauthorizedError, ValidationError, } from './errors.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pimia/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Cliente TypeScript de la API de Pimia para apps de partner: OAuth con PKCE, rotación de refresh persistida, reintentos de rate limit y tipos generados del OpenAPI.",
5
5
  "license": "MIT",
6
6
  "author": "Pimia (https://pimia.es)",
@@ -40,7 +40,7 @@
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.json",
42
42
  "typecheck": "tsc -p tsconfig.json --noEmit",
43
- "generate:types": "openapi-typescript ../spec/pimia-api-v1.json -o src/api.ts",
43
+ "generate:types": "node scripts/generate-types.mjs",
44
44
  "test": "node --test test/*.test.js",
45
45
  "prepublishOnly": "npm run build"
46
46
  },