@vivoa/partner-sdk 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.
Files changed (106) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +162 -0
  3. package/dist/cjs/adapters/fetch-transport.d.ts +7 -0
  4. package/dist/cjs/adapters/fetch-transport.js +34 -0
  5. package/dist/cjs/adapters/in-memory/fake-vivoa.d.ts +93 -0
  6. package/dist/cjs/adapters/in-memory/fake-vivoa.js +468 -0
  7. package/dist/cjs/adapters/manual-clock.d.ts +9 -0
  8. package/dist/cjs/adapters/manual-clock.js +20 -0
  9. package/dist/cjs/adapters/node-crypto.d.ts +2 -0
  10. package/dist/cjs/adapters/node-crypto.js +13 -0
  11. package/dist/cjs/adapters/system-clock.d.ts +2 -0
  12. package/dist/cjs/adapters/system-clock.js +7 -0
  13. package/dist/cjs/application/partner-client.d.ts +45 -0
  14. package/dist/cjs/application/partner-client.js +55 -0
  15. package/dist/cjs/application/resources/fleet.d.ts +7 -0
  16. package/dist/cjs/application/resources/fleet.js +13 -0
  17. package/dist/cjs/application/resources/me.d.ts +13 -0
  18. package/dist/cjs/application/resources/me.js +23 -0
  19. package/dist/cjs/application/resources/merchants.d.ts +37 -0
  20. package/dist/cjs/application/resources/merchants.js +92 -0
  21. package/dist/cjs/application/resources/webhooks.d.ts +16 -0
  22. package/dist/cjs/application/resources/webhooks.js +30 -0
  23. package/dist/cjs/application/signed-http.d.ts +41 -0
  24. package/dist/cjs/application/signed-http.js +105 -0
  25. package/dist/cjs/application/use-cases/provision-store.d.ts +50 -0
  26. package/dist/cjs/application/use-cases/provision-store.js +103 -0
  27. package/dist/cjs/domain/errors.d.ts +72 -0
  28. package/dist/cjs/domain/errors.js +111 -0
  29. package/dist/cjs/domain/merchant.d.ts +74 -0
  30. package/dist/cjs/domain/merchant.js +28 -0
  31. package/dist/cjs/domain/operator.d.ts +32 -0
  32. package/dist/cjs/domain/operator.js +6 -0
  33. package/dist/cjs/domain/signature.d.ts +21 -0
  34. package/dist/cjs/domain/signature.js +23 -0
  35. package/dist/cjs/domain/webhook.d.ts +48 -0
  36. package/dist/cjs/domain/webhook.js +10 -0
  37. package/dist/cjs/index.d.ts +25 -0
  38. package/dist/cjs/index.js +61 -0
  39. package/dist/cjs/package.json +1 -0
  40. package/dist/cjs/ports/clock.port.d.ts +5 -0
  41. package/dist/cjs/ports/clock.port.js +2 -0
  42. package/dist/cjs/ports/crypto.port.d.ts +7 -0
  43. package/dist/cjs/ports/crypto.port.js +2 -0
  44. package/dist/cjs/ports/http-transport.port.d.ts +25 -0
  45. package/dist/cjs/ports/http-transport.port.js +2 -0
  46. package/dist/cjs/ports/index.d.ts +5 -0
  47. package/dist/cjs/ports/index.js +5 -0
  48. package/dist/cjs/ports/logger.port.d.ts +6 -0
  49. package/dist/cjs/ports/logger.port.js +7 -0
  50. package/dist/cjs/testing.d.ts +12 -0
  51. package/dist/cjs/testing.js +16 -0
  52. package/dist/cjs/webhooks/verify-webhook.d.ts +21 -0
  53. package/dist/cjs/webhooks/verify-webhook.js +36 -0
  54. package/dist/esm/adapters/fetch-transport.d.ts +7 -0
  55. package/dist/esm/adapters/fetch-transport.js +30 -0
  56. package/dist/esm/adapters/in-memory/fake-vivoa.d.ts +93 -0
  57. package/dist/esm/adapters/in-memory/fake-vivoa.js +464 -0
  58. package/dist/esm/adapters/manual-clock.d.ts +9 -0
  59. package/dist/esm/adapters/manual-clock.js +16 -0
  60. package/dist/esm/adapters/node-crypto.d.ts +2 -0
  61. package/dist/esm/adapters/node-crypto.js +10 -0
  62. package/dist/esm/adapters/system-clock.d.ts +2 -0
  63. package/dist/esm/adapters/system-clock.js +4 -0
  64. package/dist/esm/application/partner-client.d.ts +45 -0
  65. package/dist/esm/application/partner-client.js +51 -0
  66. package/dist/esm/application/resources/fleet.d.ts +7 -0
  67. package/dist/esm/application/resources/fleet.js +9 -0
  68. package/dist/esm/application/resources/me.d.ts +13 -0
  69. package/dist/esm/application/resources/me.js +19 -0
  70. package/dist/esm/application/resources/merchants.d.ts +37 -0
  71. package/dist/esm/application/resources/merchants.js +87 -0
  72. package/dist/esm/application/resources/webhooks.d.ts +16 -0
  73. package/dist/esm/application/resources/webhooks.js +26 -0
  74. package/dist/esm/application/signed-http.d.ts +41 -0
  75. package/dist/esm/application/signed-http.js +101 -0
  76. package/dist/esm/application/use-cases/provision-store.d.ts +50 -0
  77. package/dist/esm/application/use-cases/provision-store.js +99 -0
  78. package/dist/esm/cli.d.ts +2 -0
  79. package/dist/esm/cli.js +106 -0
  80. package/dist/esm/domain/errors.d.ts +72 -0
  81. package/dist/esm/domain/errors.js +92 -0
  82. package/dist/esm/domain/merchant.d.ts +74 -0
  83. package/dist/esm/domain/merchant.js +23 -0
  84. package/dist/esm/domain/operator.d.ts +32 -0
  85. package/dist/esm/domain/operator.js +3 -0
  86. package/dist/esm/domain/signature.d.ts +21 -0
  87. package/dist/esm/domain/signature.js +19 -0
  88. package/dist/esm/domain/webhook.d.ts +48 -0
  89. package/dist/esm/domain/webhook.js +7 -0
  90. package/dist/esm/index.d.ts +25 -0
  91. package/dist/esm/index.js +33 -0
  92. package/dist/esm/ports/clock.port.d.ts +5 -0
  93. package/dist/esm/ports/clock.port.js +1 -0
  94. package/dist/esm/ports/crypto.port.d.ts +7 -0
  95. package/dist/esm/ports/crypto.port.js +1 -0
  96. package/dist/esm/ports/http-transport.port.d.ts +25 -0
  97. package/dist/esm/ports/http-transport.port.js +1 -0
  98. package/dist/esm/ports/index.d.ts +5 -0
  99. package/dist/esm/ports/index.js +1 -0
  100. package/dist/esm/ports/logger.port.d.ts +6 -0
  101. package/dist/esm/ports/logger.port.js +4 -0
  102. package/dist/esm/testing.d.ts +12 -0
  103. package/dist/esm/testing.js +11 -0
  104. package/dist/esm/webhooks/verify-webhook.d.ts +21 -0
  105. package/dist/esm/webhooks/verify-webhook.js +33 -0
  106. package/package.json +56 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Primera versión.
6
+
7
+ - `createPartnerClient()` con recursos `me`, `merchants`, `webhooks` y `fleet`, que cubren la Partner API `/partner/v1`.
8
+ - `provisionStore()`: crea la tienda, la activa y espera a que esté en vivo. Es idempotente por `externalId` (tu propia referencia) — la plataforma asigna el `handle`, así que nunca es algo que el socio elige o que puede chocar.
9
+ - `verifyWebhook()` para `X-Vivoa-Signature`, con ventana contra reenvíos.
10
+ - Errores tipados por código HTTP. Solo se reintentan los `GET`.
11
+ - `@vivoa/partner-sdk/testing`: `FakeVivoa` y `ManualClock`.
12
+ - CLI `vivoa-partner`.
13
+ - Salida dual ESM + CommonJS.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @vivoa/partner-sdk
2
+
3
+ ```bash
4
+ pnpm add @vivoa/partner-sdk # o npm i / yarn add
5
+ ```
6
+
7
+ SDK oficial de la **Partner API de Vivoa**, para crear y operar tiendas de tus comercios desde tu backend.
8
+
9
+ - **Sin dependencias de runtime**: usa `fetch` y `node:crypto`, ambos reemplazables.
10
+ - **Arquitectura hexagonal**: el núcleo no hace I/O. El transporte, la criptografía y el reloj son puertos que puedes cambiar.
11
+ - **El handle lo asigna Vivoa**: no lo eliges tú al crear un merchant — la plataforma lo genera a partir de `name` y garantiza que sea único. Menos que validar, nada que pueda chocar.
12
+ - **Seguro ante reintentos**: `provisionStore()` usa tu propio `externalId` como clave de idempotencia. Si lo vuelves a llamar después de un crash o un timeout, retoma la misma tienda en vez de crear otra.
13
+ - **Testeable sin Vivoa**: `@vivoa/partner-sdk/testing` trae un Vivoa en memoria que verifica la firma HMAC real y aplica las mismas reglas que la plataforma.
14
+
15
+ Guía de producto: [`docs/PARTNERS.md`](https://github.com/VivoaPlatform/vivoa-platform/blob/main/docs/PARTNERS.md). Contrato del servidor: [`engine/partner-api/CONTRACT.md`](https://github.com/VivoaPlatform/vivoa-platform/blob/main/packages/backend/src/engine/partner-api/CONTRACT.md).
16
+
17
+ ## Quickstart
18
+
19
+ ```ts
20
+ import { createPartnerClient } from '@vivoa/partner-sdk';
21
+
22
+ const vivoa = createPartnerClient({
23
+ apiKey: process.env.VIVOA_PARTNER_KEY!,
24
+ apiSecret: process.env.VIVOA_PARTNER_SECRET!,
25
+ // baseUrl: 'https://api.vivoa.app' (default)
26
+ });
27
+
28
+ const me = await vivoa.me.get(); // plan, storesUsed/maxStores, allowedRegions
29
+
30
+ const { merchant, status, created } = await vivoa.provisionStore(
31
+ { email: 'owner@acme.com', name: 'Acme', countries: ['SV'], externalId: 'cus_9f3a2e10' },
32
+ { onProgress: (p) => console.log(p.phase, p.runtime) },
33
+ );
34
+ // merchant.handle → asignado por Vivoa; status.deployStatus === 'live'; status.domain → dominio de la tienda
35
+ ```
36
+
37
+ `externalId` es tu propia referencia (id de cliente, de orden…) — la usas para encontrar la tienda después (`findByExternalId`) y es la llave de idempotencia: `provisionStore` la exige justo por eso.
38
+
39
+ `provisionStore` hace lo siguiente:
40
+
41
+ 1. Busca el `externalId` en tu flota y, si no existe, crea la tienda (Vivoa asigna el `handle`).
42
+ 2. Activa la tienda si está `not_activated` o `failed`.
43
+ 3. Si queda `creating`, consulta el estado hasta que se asiente.
44
+ 4. Si el `activate` devuelve 409 o se cae la conexión, se recupera leyendo `GET /status`.
45
+ 5. Nunca reintenta por su cuenta un `failed`: lanza `VivoaProvisioningFailedError` y la decisión queda en tus manos.
46
+
47
+ ## Superficie
48
+
49
+ | Recurso | Métodos |
50
+ |---|---|
51
+ | `me` | `get()`, `rotateSecret()` (el cliente pasa a usar el secreto nuevo de inmediato) |
52
+ | `merchants` | `create`, `list`, `listAll()` (iterador async), `findByExternalId`, `status`, `activate`, `suspend`, `reactivate` |
53
+ | `webhooks` | `get`, `set`, `remove`, `rotateSecret`, `test`, `deliveries` |
54
+ | `fleet` | `analytics()` |
55
+ | cliente | `provisionStore`, `waitUntilSettled`, `verifyWebhook` |
56
+
57
+ ## Errores
58
+
59
+ Todos los errores extienden `VivoaError`. Los errores HTTP son `VivoaApiError` e incluyen `status`, `method`, `path` y `body`.
60
+
61
+ | Clase | Cuándo |
62
+ |---|---|
63
+ | `VivoaValidationError` | Input inválido, detectado en el cliente antes de enviar nada |
64
+ | `VivoaBadRequestError` (400) | Payload inválido o país fuera de `allowedRegions` |
65
+ | `VivoaAuthenticationError` (401) | Firma, key o reloj (±300 s) |
66
+ | `VivoaForbiddenError` (403) | Cuenta no `active` o cupo lleno |
67
+ | `VivoaNotFoundError` (404) | La tienda no es de tu flota |
68
+ | `VivoaConflictError` (409) | `activate` sobre una tienda que ya está creándose, en vivo o suspendida |
69
+ | `VivoaRateLimitError` (429) | Superaste las requests por minuto del plan |
70
+ | `VivoaServerError` (5xx) · `VivoaNetworkError` · `VivoaTimeoutError` · `VivoaProvisioningFailedError` | Fallos del servidor, de red, de espera o del aprovisionamiento |
71
+
72
+ Solo se reintentan los `GET`, ante 429, 5xx o errores de red, con backoff exponencial. Las escrituras no se reintentan a nivel de transporte — pero `merchants.create()` y `provisionStore()` son seguros de repetir de todas formas: con `externalId`, la plataforma responde con el mismo merchant en vez de un 409 o un duplicado.
73
+
74
+ ## Webhooks
75
+
76
+ ```ts
77
+ import express from 'express';
78
+ import { verifyWebhook, VivoaWebhookSignatureError } from '@vivoa/partner-sdk';
79
+
80
+ app.post('/vivoa-hooks', express.raw({ type: 'application/json' }), async (req, res) => {
81
+ try {
82
+ const event = await verifyWebhook({
83
+ rawBody: req.body, // body CRUDO
84
+ signatureHeader: req.header('x-vivoa-signature'),
85
+ signingSecret: process.env.VIVOA_WEBHOOK_SECRET!,
86
+ });
87
+ res.sendStatus(204); // responder rápido
88
+ queue.push(event); // procesar en segundo plano
89
+ } catch (err) {
90
+ res.sendStatus(err instanceof VivoaWebhookSignatureError ? 401 : 500);
91
+ }
92
+ });
93
+ ```
94
+
95
+ ## Testing sin tocar Vivoa
96
+
97
+ ```ts
98
+ import { createPartnerClient } from '@vivoa/partner-sdk';
99
+ import { FakeVivoa, ManualClock } from '@vivoa/partner-sdk/testing';
100
+
101
+ const clock = new ManualClock();
102
+ const vivoa = new FakeVivoa({
103
+ clock,
104
+ operators: [{ apiKey: 'k', apiSecret: 's', allowedRegions: ['SV'], maxStores: 3 }],
105
+ activation: 'slow', // 'live' | 'slow' | 'fail' | 'drop'
106
+ onWebhook: (d) => deliveries.push(d),
107
+ });
108
+ const client = createPartnerClient({ apiKey: 'k', apiSecret: 's', transport: vivoa, clock });
109
+ ```
110
+
111
+ ## Arquitectura
112
+
113
+ ```
114
+ src/
115
+ domain/ tipos, reglas de ciclo de vida, errores, payload canónico (puro)
116
+ ports/ HttpTransport · CryptoProvider · Clock · SdkLogger
117
+ application/ SignedHttpClient · resources/ · use-cases/provision-store · VivoaPartnerClient
118
+ adapters/ fetch · node-crypto · system/manual clock · in-memory/FakeVivoa
119
+ webhooks/ verifyWebhook
120
+ index.ts composition root (createPartnerClient)
121
+ cli.ts driving adapter (vivoa-partner)
122
+ ```
123
+
124
+ Dependencias permitidas: `domain` no importa nada; `application` importa solo `domain` y `ports`; `adapters` implementan `ports`.
125
+
126
+ Para llevar el SDK a otro runtime basta con un adapter nuevo, por ejemplo WebCrypto para edge o un transporte con métricas. Cuando el backend sume endpoints, se agregan como recursos. En ningún caso hay que tocar el núcleo.
127
+
128
+ ## CLI
129
+
130
+ ```bash
131
+ export VIVOA_PARTNER_KEY=… VIVOA_PARTNER_SECRET=… # solo por env, nunca por flags
132
+ node src/cli.ts me
133
+ node src/cli.ts merchants list --search acme
134
+ node src/cli.ts store create --external-id cus_1 --name Acme --email owner@acme.com --countries SV --yes
135
+ ```
136
+
137
+ ## Publicar
138
+
139
+ ```bash
140
+ # subir "version" en package.json + CHANGELOG.md
141
+ pnpm publish # prepublishOnly: typecheck + test + build + test:dist
142
+ ```
143
+
144
+ O deja que lo haga CI: sube la versión, mergea a `main` y crea un **GitHub Release** (tag `vX.Y.Z` igual al `version` de `package.json`) — `.github/workflows/release.yml` corre el mismo gate y publica con el secreto `NPM_TOKEN` del repo.
145
+
146
+ ## Desarrollo
147
+
148
+ Requiere Node ≥ 22.18, que ejecuta TypeScript de forma nativa. Los tests corren sin `pnpm install`.
149
+
150
+ ```bash
151
+ pnpm test # unit + contrato contra FakeVivoa
152
+ pnpm typecheck # tsc (src + tests)
153
+ pnpm build # dist/esm + dist/cjs (+ .d.ts)
154
+ pnpm test:dist # smoke de los entry points publicados (require + import)
155
+
156
+ # Mismo contrato contra la API real. Por defecto solo lee:
157
+ VIVOA_LIVE=1 VIVOA_PARTNER_KEY=… VIVOA_PARTNER_SECRET=… pnpm test:live
158
+ # Crea y activa UNA tienda sdk-e2e-<ts> (consume cupo e infraestructura real):
159
+ VIVOA_LIVE=1 VIVOA_LIVE_WRITE=1 … pnpm test:live
160
+ ```
161
+
162
+ La suite de contrato (`test/contract/partner-api.contract.ts`) es una sola y corre contra el fake y contra producción. Si el fake se aparta del servidor real, la corrida live falla en la misma aserción.
@@ -0,0 +1,7 @@
1
+ import type { HttpRequest, HttpResponse, HttpTransport } from '../ports/http-transport.port.ts';
2
+ /** Default transport: the runtime's global `fetch` (Node ≥ 18, Deno, Bun, edge). */
3
+ export declare class FetchTransport implements HttpTransport {
4
+ private readonly fetchImpl;
5
+ constructor(fetchImpl?: typeof fetch);
6
+ send(request: HttpRequest): Promise<HttpResponse>;
7
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchTransport = void 0;
4
+ const errors_ts_1 = require("../domain/errors.js");
5
+ /** Default transport: the runtime's global `fetch` (Node ≥ 18, Deno, Bun, edge). */
6
+ class FetchTransport {
7
+ fetchImpl;
8
+ constructor(fetchImpl = globalThis.fetch) {
9
+ if (!fetchImpl)
10
+ throw new Error('FetchTransport: no global fetch available — pass one explicitly');
11
+ this.fetchImpl = fetchImpl;
12
+ }
13
+ async send(request) {
14
+ let res;
15
+ try {
16
+ res = await this.fetchImpl(request.url, {
17
+ method: request.method,
18
+ headers: request.headers,
19
+ body: request.body,
20
+ signal: AbortSignal.timeout(request.timeoutMs),
21
+ });
22
+ }
23
+ catch (err) {
24
+ const reason = err.name === 'TimeoutError' ? `timed out after ${request.timeoutMs} ms` : err.message;
25
+ throw new errors_ts_1.VivoaNetworkError(`${request.method} ${request.url}: ${reason}`, { cause: err });
26
+ }
27
+ const headers = {};
28
+ res.headers.forEach((value, key) => {
29
+ headers[key.toLowerCase()] = value;
30
+ });
31
+ return { status: res.status, headers, body: await res.text() };
32
+ }
33
+ }
34
+ exports.FetchTransport = FetchTransport;
@@ -0,0 +1,93 @@
1
+ import type { HttpRequest, HttpResponse, HttpTransport } from '../../ports/http-transport.port.ts';
2
+ import type { CryptoProvider } from '../../ports/crypto.port.ts';
3
+ import type { Clock } from '../../ports/clock.port.ts';
4
+ import { type MerchantRuntimeStatus } from '../../domain/merchant.ts';
5
+ import type { OperatorProfile, OperatorStatus } from '../../domain/operator.ts';
6
+ /**
7
+ * How `POST /activate` behaves:
8
+ * - `live`: provisions synchronously and returns live (the platform's happy path).
9
+ * - `slow`: returns `creating`; stays creating for `pollsUntilLive` status reads.
10
+ * - `fail`: responds 500 and leaves the merchant `failed` (retry allowed).
11
+ * - `drop`: the connection drops (network error) but provisioning continues → live.
12
+ */
13
+ export type FakeActivationMode = 'live' | 'slow' | 'fail' | 'drop';
14
+ export interface FakeOperatorSeed {
15
+ apiKey: string;
16
+ apiSecret: string;
17
+ name?: string;
18
+ status?: OperatorStatus;
19
+ plan?: string;
20
+ maxStores?: number;
21
+ allowedRegions?: string[];
22
+ rateLimitPerMin?: number;
23
+ }
24
+ export interface FakeWebhookDispatch {
25
+ url: string;
26
+ headers: Record<string, string>;
27
+ body: string;
28
+ }
29
+ export interface FakeVivoaOptions {
30
+ operators?: FakeOperatorSeed[];
31
+ /**
32
+ * Handles already in use platform-wide — seed this to force the auto-assigned
33
+ * handle to fall back to its `-xxxx` suffixed form, the same way a real
34
+ * collision would. A partner never sends a handle (the platform always
35
+ * assigns one), so this can no longer produce a 409 on create.
36
+ */
37
+ takenHandles?: string[];
38
+ activation?: FakeActivationMode;
39
+ pollsUntilLive?: number;
40
+ failureReason?: string;
41
+ clock?: Clock;
42
+ crypto?: CryptoProvider;
43
+ /** Receives every signed webhook the fake "delivers". */
44
+ onWebhook?: (dispatch: FakeWebhookDispatch) => void;
45
+ }
46
+ /**
47
+ * In-memory Vivoa Partner API behind the `HttpTransport` port. It verifies the
48
+ * real HMAC signature and enforces the same rules as the platform
49
+ * (`engine/partner-api`): quota, allowed countries, unique handle, active-only
50
+ * writes, fleet scoping, activation state machine and per-minute rate limit.
51
+ * The shared contract suite (`test/contract`) runs against this fake AND the
52
+ * live API, so the two cannot silently drift.
53
+ */
54
+ export declare class FakeVivoa implements HttpTransport {
55
+ readonly clock: Clock;
56
+ activation: FakeActivationMode;
57
+ pollsUntilLive: number;
58
+ failureReason: string;
59
+ private readonly crypto;
60
+ private readonly onWebhook?;
61
+ private readonly operators;
62
+ private readonly merchants;
63
+ private readonly handles;
64
+ constructor(options?: FakeVivoaOptions);
65
+ addOperator(seed: FakeOperatorSeed): OperatorProfile;
66
+ setOperatorStatus(apiKey: string, status: OperatorStatus): void;
67
+ /** Test helper: flip a merchant's runtime (e.g. simulate ops tearing it down). */
68
+ setRuntime(merchantId: string, runtime: MerchantRuntimeStatus): void;
69
+ send(req: HttpRequest): Promise<HttpResponse>;
70
+ private authenticate;
71
+ private rateLimit;
72
+ private requireActive;
73
+ private route;
74
+ private create;
75
+ /**
76
+ * Mirrors `StoreService.allocateHandle`: the readable slug of `name` first,
77
+ * a random `-xxxx` suffix if it (or a seeded `takenHandles` entry) is
78
+ * already used. The platform assigns this — a partner never sends one.
79
+ */
80
+ private allocateHandle;
81
+ private activate;
82
+ private goLive;
83
+ private statusView;
84
+ private list;
85
+ private analytics;
86
+ private webhookRoute;
87
+ private emit;
88
+ private deliver;
89
+ private fleet;
90
+ private owned;
91
+ private profile;
92
+ private requireOperatorByKey;
93
+ }