@vivoa/partner-sdk 0.1.0 → 0.2.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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ - fix: `provisionStore` now throws `VivoaProvisioningFailedError` on `'unknown'` runtime instead of returning a broken store silently.
6
+ - fix: `FakeVivoa.activate` now returns 409 on merchants in `'unknown'` state, matching platform behavior.
7
+ - fix: `SDK_VERSION` extracted to `src/version.ts` — User-Agent header stays in sync with `package.json` on every bump.
8
+ - fix: `WebhookConfig.events` typed as `MerchantEventType[]` instead of `string[]`.
9
+ - fix: `listAll` throws after 10 000 pages instead of looping forever on a misbehaving server.
10
+ - test: contract suite covers `suspend → reactivate` lifecycle.
11
+
3
12
  ## 0.1.0
4
13
 
5
14
  Primera versión.
package/README.md CHANGED
@@ -1,18 +1,28 @@
1
1
  # @vivoa/partner-sdk
2
2
 
3
+ **Embed full commerce stores in your product — provisioned via API, branded as yours.**
4
+
5
+ Your users launch their store through your platform. Vivoa runs the infrastructure. They never see Vivoa.
6
+
3
7
  ```bash
4
- pnpm add @vivoa/partner-sdk # o npm i / yarn add
8
+ pnpm add @vivoa/partner-sdk
5
9
  ```
6
10
 
7
- SDK oficial de la **Partner API de Vivoa**, para crear y operar tiendas de tus comercios desde tu backend.
11
+ [![npm](https://img.shields.io/npm/v/@vivoa/partner-sdk)](https://www.npmjs.com/package/@vivoa/partner-sdk)
12
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
13
+ [![Node](https://img.shields.io/badge/Node-%E2%89%A522.18-green)](https://nodejs.org/)
14
+
15
+ ---
8
16
 
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.
17
+ ## What you get
14
18
 
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).
19
+ - **One call to provision a store.** `provisionStore()` handles creation, activation, and recovery — idempotent by design. Crash, retry, same store.
20
+ - **Your brand, your API.** Merchants interact with your product. The Vivoa layer is yours to expose however you want.
21
+ - **Production-safe from day one.** Fully typed errors, automatic retries on reads, HMAC-signed requests, webhook verification included.
22
+ - **Test without hitting Vivoa.** The built-in `FakeVivoa` runs the same business rules as production — including real HMAC verification. No brittle mocks.
23
+ - **Zero runtime dependencies.** Uses native `fetch` and `node:crypto`. No supply-chain risk. Drop-in on any runtime.
24
+
25
+ ---
16
26
 
17
27
  ## Quickstart
18
28
 
@@ -20,56 +30,69 @@ Guía de producto: [`docs/PARTNERS.md`](https://github.com/VivoaPlatform/vivoa-p
20
30
  import { createPartnerClient } from '@vivoa/partner-sdk';
21
31
 
22
32
  const vivoa = createPartnerClient({
23
- apiKey: process.env.VIVOA_PARTNER_KEY!,
33
+ apiKey: process.env.VIVOA_PARTNER_KEY!,
24
34
  apiSecret: process.env.VIVOA_PARTNER_SECRET!,
25
- // baseUrl: 'https://api.vivoa.app' (default)
26
35
  });
27
36
 
28
- const me = await vivoa.me.get(); // plan, storesUsed/maxStores, allowedRegions
37
+ // Provision a store for one of your users — idempotent on externalId
38
+ const { merchant, status } = await vivoa.provisionStore({
39
+ externalId: 'cus_9f3a2e10', // your internal reference — customer id, order id, etc.
40
+ email: 'owner@acme.com',
41
+ name: 'Acme',
42
+ countries: ['SV'],
43
+ });
29
44
 
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
45
+ console.log(status.domain); // store is live at this domain
46
+ console.log(merchant.handle); // Vivoa-assigned handle, guaranteed unique
35
47
  ```
36
48
 
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.
49
+ `externalId` is your key. Use it to find the store later (`findByExternalId`), and call `provisionStore` again safely — it won't create a duplicate.
50
+
51
+ ---
38
52
 
39
- `provisionStore` hace lo siguiente:
53
+ ## `provisionStore` — full flow
40
54
 
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.
55
+ One call drives the entire lifecycle:
46
56
 
47
- ## Superficie
57
+ 1. Looks up `externalId` in your fleet — if it exists, resumes from current state.
58
+ 2. Creates the store if it doesn't exist (Vivoa assigns the `handle` from `name`).
59
+ 3. Activates if `not_activated` or `failed`.
60
+ 4. Polls until settled if `creating`.
61
+ 5. Recovers from a dropped `activate` by reading `GET /status`.
62
+ 6. Never silently retries a `failed` — throws `VivoaProvisioningFailedError` so you decide what happens next.
48
63
 
49
- | Recurso | Métodos |
64
+ ---
65
+
66
+ ## API surface
67
+
68
+ | Resource | Methods |
50
69
  |---|---|
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` |
70
+ | `me` | `get()`, `rotateSecret()` |
71
+ | `merchants` | `create`, `list`, `listAll()` (async iterator), `findByExternalId`, `status`, `activate`, `suspend`, `reactivate` |
53
72
  | `webhooks` | `get`, `set`, `remove`, `rotateSecret`, `test`, `deliveries` |
54
73
  | `fleet` | `analytics()` |
55
- | cliente | `provisionStore`, `waitUntilSettled`, `verifyWebhook` |
74
+ | client | `provisionStore`, `waitUntilSettled`, `verifyWebhook` |
75
+
76
+ ---
56
77
 
57
- ## Errores
78
+ ## Errors
58
79
 
59
- Todos los errores extienden `VivoaError`. Los errores HTTP son `VivoaApiError` e incluyen `status`, `method`, `path` y `body`.
80
+ All errors extend `VivoaError`. HTTP errors are `VivoaApiError` with `status`, `method`, `path`, and `body`.
60
81
 
61
- | Clase | Cuándo |
82
+ | Class | When |
62
83
  |---|---|
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 |
84
+ | `VivoaValidationError` | Invalid input caught client-side before any request |
85
+ | `VivoaBadRequestError` (400) | Invalid payload or country outside `allowedRegions` |
86
+ | `VivoaAuthenticationError` (401) | HMAC signature, key, or clock skew (±300 s) |
87
+ | `VivoaForbiddenError` (403) | Inactive account or quota exhausted |
88
+ | `VivoaNotFoundError` (404) | Store not in your fleet |
89
+ | `VivoaConflictError` (409) | `activate` on a store already live or in progress |
90
+ | `VivoaRateLimitError` (429) | Plan request limit exceeded |
91
+ | `VivoaServerError` · `VivoaNetworkError` · `VivoaTimeoutError` · `VivoaProvisioningFailedError` | Server, network, polling, or provisioning failures |
71
92
 
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.
93
+ **Retry policy:** `GET` only, on 429 / 5xx / network errors, with exponential backoff. Writes are not retried at the transport level — but `merchants.create()` and `provisionStore()` are safe to repeat thanks to `externalId` idempotency.
94
+
95
+ ---
73
96
 
74
97
  ## Webhooks
75
98
 
@@ -80,83 +103,66 @@ import { verifyWebhook, VivoaWebhookSignatureError } from '@vivoa/partner-sdk';
80
103
  app.post('/vivoa-hooks', express.raw({ type: 'application/json' }), async (req, res) => {
81
104
  try {
82
105
  const event = await verifyWebhook({
83
- rawBody: req.body, // body CRUDO
106
+ rawBody: req.body,
84
107
  signatureHeader: req.header('x-vivoa-signature'),
85
- signingSecret: process.env.VIVOA_WEBHOOK_SECRET!,
108
+ signingSecret: process.env.VIVOA_WEBHOOK_SECRET!,
86
109
  });
87
- res.sendStatus(204); // responder rápido
88
- queue.push(event); // procesar en segundo plano
110
+ res.sendStatus(204); // respond fast, process async
111
+ queue.push(event);
89
112
  } catch (err) {
90
113
  res.sendStatus(err instanceof VivoaWebhookSignatureError ? 401 : 500);
91
114
  }
92
115
  });
93
116
  ```
94
117
 
95
- ## Testing sin tocar Vivoa
118
+ ---
119
+
120
+ ## Testing
121
+
122
+ `@vivoa/partner-sdk/testing` ships a `FakeVivoa` that runs the same rules as the real server — real HMAC verification, real business logic, zero network.
96
123
 
97
124
  ```ts
98
125
  import { createPartnerClient } from '@vivoa/partner-sdk';
99
126
  import { FakeVivoa, ManualClock } from '@vivoa/partner-sdk/testing';
100
127
 
101
- const clock = new ManualClock();
102
- const vivoa = new FakeVivoa({
128
+ const clock = new ManualClock();
129
+ const fake = new FakeVivoa({
103
130
  clock,
104
- operators: [{ apiKey: 'k', apiSecret: 's', allowedRegions: ['SV'], maxStores: 3 }],
105
- activation: 'slow', // 'live' | 'slow' | 'fail' | 'drop'
106
- onWebhook: (d) => deliveries.push(d),
131
+ operators: [{ apiKey: 'k', apiSecret: 's', allowedRegions: ['SV'], maxStores: 3 }],
132
+ activation: 'slow', // 'live' | 'slow' | 'fail' | 'drop'
133
+ onWebhook: (d) => deliveries.push(d),
107
134
  });
108
- const client = createPartnerClient({ apiKey: 'k', apiSecret: 's', transport: vivoa, clock });
135
+ const client = createPartnerClient({ apiKey: 'k', apiSecret: 's', transport: fake, clock });
109
136
  ```
110
137
 
111
- ## Arquitectura
138
+ The same contract suite runs against `FakeVivoa` and against production. If the fake drifts from the real server, the live run fails on the same assertion.
139
+
140
+ ---
141
+
142
+ ## Architecture
112
143
 
113
144
  ```
114
145
  src/
115
- domain/ tipos, reglas de ciclo de vida, errores, payload canónico (puro)
146
+ domain/ types, lifecycle rules, errors (pure — no I/O)
116
147
  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
148
+ application/ SignedHttpClient · resources/ · provision-store · VivoaPartnerClient
149
+ adapters/ fetch · node-crypto · system clock · ManualClock · FakeVivoa
119
150
  webhooks/ verifyWebhook
120
151
  index.ts composition root (createPartnerClient)
121
152
  cli.ts driving adapter (vivoa-partner)
122
153
  ```
123
154
 
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
155
+ `domain` imports nothing external. `application` imports only `domain` and `ports`. `adapters` implement `ports`. A new runtime needs only a new adapter — the core is untouched.
138
156
 
139
- ```bash
140
- # subir "version" en package.json + CHANGELOG.md
141
- pnpm publish # prepublishOnly: typecheck + test + build + test:dist
142
- ```
157
+ ---
143
158
 
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.
159
+ ## Further reading
145
160
 
146
- ## Desarrollo
161
+ - [Partner program guide](https://github.com/VivoaPlatform/vivoa-platform/blob/main/docs/PARTNERS.md)
162
+ - [Server contract](https://github.com/VivoaPlatform/vivoa-platform/blob/main/packages/backend/src/engine/partner-api/CONTRACT.md)
147
163
 
148
- Requiere Node ≥ 22.18, que ejecuta TypeScript de forma nativa. Los tests corren sin `pnpm install`.
164
+ ---
149
165
 
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
- ```
166
+ ## License
161
167
 
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.
168
+ Private — authorized Vivoa partners only.
@@ -155,7 +155,7 @@ class FakeVivoa {
155
155
  async route(op, method, url, rawBody) {
156
156
  const path = url.pathname.replace(/^\/partner\/v1/, '');
157
157
  const body = rawBody ? JSON.parse(rawBody) : {};
158
- const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate)$/);
158
+ const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket)$/);
159
159
  if (method === 'GET' && path === '/me')
160
160
  return this.profile(op);
161
161
  if (method === 'POST' && path === '/me/rotate-secret') {
@@ -192,6 +192,16 @@ class FakeVivoa {
192
192
  this.emit(op, 'merchant.reactivated', merchant);
193
193
  return { storeId: merchant.id, status: 'active' };
194
194
  }
195
+ if (action === 'admin-ticket') {
196
+ if (merchant.status !== 'active')
197
+ throw new HttpError(403, 'Store is not active');
198
+ const rawPath = typeof body.returnPath === 'string' ? body.returnPath : '/admin';
199
+ const returnPath = rawPath.startsWith('/') && !rawPath.startsWith('//') ? rawPath : '/admin';
200
+ const base = merchant.domain ? `https://${merchant.domain}` : `https://${merchant.handle}.vivoa.test`;
201
+ return {
202
+ redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
203
+ };
204
+ }
195
205
  }
196
206
  }
197
207
  if (method === 'GET' && path === '/fleet/analytics')
@@ -271,10 +281,12 @@ class FakeVivoa {
271
281
  throw new HttpError(500, `Could not allocate a free handle for "${name}"`);
272
282
  }
273
283
  activate(op, m) {
274
- if (m.runtime === 'creating' || m.runtime === 'live' || m.runtime === 'suspended') {
284
+ if (m.runtime === 'creating' || m.runtime === 'live' || m.runtime === 'suspended' || m.runtime === 'unknown') {
275
285
  throw new HttpError(409, m.runtime === 'suspended'
276
286
  ? `Merchant '${m.handle}' is suspended — use reactivate instead`
277
- : `Merchant '${m.handle}' is already ${m.runtime}`);
287
+ : m.runtime === 'unknown'
288
+ ? `Merchant '${m.handle}' is in an unrecoverable state`
289
+ : `Merchant '${m.handle}' is already ${m.runtime}`);
278
290
  }
279
291
  switch (this.activation) {
280
292
  case 'fail':
@@ -10,7 +10,8 @@ import { type ProvisionStoreOptions, type ProvisionStoreResult } from './use-cas
10
10
  import { type VerifyWebhookParams } from '../webhooks/verify-webhook.ts';
11
11
  import type { CreateMerchantInput, MerchantStatus } from '../domain/merchant.ts';
12
12
  import type { WebhookEvent } from '../domain/webhook.ts';
13
- export declare const SDK_VERSION = "0.1.0";
13
+ import { SDK_VERSION } from '../version.ts';
14
+ export { SDK_VERSION };
14
15
  /** Every adapter is injected — the client itself has no I/O of its own. */
15
16
  export interface PartnerClientDeps {
16
17
  baseUrl: string;
@@ -9,7 +9,8 @@ const webhooks_ts_1 = require("./resources/webhooks.js");
9
9
  const fleet_ts_1 = require("./resources/fleet.js");
10
10
  const provision_store_ts_1 = require("./use-cases/provision-store.js");
11
11
  const verify_webhook_ts_1 = require("../webhooks/verify-webhook.js");
12
- exports.SDK_VERSION = '0.1.0';
12
+ const version_ts_1 = require("../version.js");
13
+ Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_ts_1.SDK_VERSION; } });
13
14
  class VivoaPartnerClient {
14
15
  me;
15
16
  merchants;
@@ -28,7 +29,7 @@ class VivoaPartnerClient {
28
29
  logger: deps.logger ?? logger_port_ts_1.silentLogger,
29
30
  timeoutMs: deps.timeoutMs ?? 30_000,
30
31
  maxRetries: deps.maxRetries ?? 2,
31
- userAgent: `vivoa-partner-sdk/${exports.SDK_VERSION}`,
32
+ userAgent: `vivoa-partner-sdk/${version_ts_1.SDK_VERSION}`,
32
33
  });
33
34
  this.crypto = deps.crypto;
34
35
  this.me = new me_ts_1.MeResource(http);
@@ -1,5 +1,5 @@
1
1
  import type { SignedHttpClient } from '../signed-http.ts';
2
- import { type CreateMerchantInput, type ListMerchantsQuery, type Merchant, type MerchantStatus, type Page } from '../../domain/merchant.ts';
2
+ import { type AdminTicketOptions, type CreateMerchantInput, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page } from '../../domain/merchant.ts';
3
3
  /** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
4
4
  export declare const DEFAULT_ACTIVATE_TIMEOUT_MS: number;
5
5
  export declare class MerchantsResource {
@@ -33,5 +33,11 @@ export declare class MerchantsResource {
33
33
  storeId: string;
34
34
  status: 'active';
35
35
  }>;
36
+ /**
37
+ * Mints a single-use SSO URL into this merchant's store admin, signed in AS
38
+ * the store owner. The link expires in ~60s — redirect to it right away, do
39
+ * not store it. 403 if the merchant is not in your fleet or is not active.
40
+ */
41
+ adminTicket(merchantId: string, options?: AdminTicketOptions): Promise<MerchantAdminTicket>;
36
42
  }
37
43
  export declare function validateCreate(input: CreateMerchantInput): void;
@@ -30,12 +30,13 @@ class MerchantsResource {
30
30
  /** Walks every page of the fleet. */
31
31
  async *listAll(query = {}) {
32
32
  const limit = query.limit ?? 100;
33
- for (let page = 1;; page++) {
33
+ for (let page = 1; page <= 10_000; page++) {
34
34
  const res = await this.list({ ...query, page, limit });
35
35
  yield* res.data;
36
36
  if (res.data.length === 0 || page * limit >= res.meta.total)
37
37
  return;
38
38
  }
39
+ throw new Error('listAll: exceeded maximum page limit (10 000)');
39
40
  }
40
41
  /** The merchant created with this reference, or `null` — an exact server-side match. */
41
42
  async findByExternalId(externalId) {
@@ -61,6 +62,16 @@ class MerchantsResource {
61
62
  async reactivate(merchantId) {
62
63
  return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/reactivate`);
63
64
  }
65
+ /**
66
+ * Mints a single-use SSO URL into this merchant's store admin, signed in AS
67
+ * the store owner. The link expires in ~60s — redirect to it right away, do
68
+ * not store it. 403 if the merchant is not in your fleet or is not active.
69
+ */
70
+ async adminTicket(merchantId, options = {}) {
71
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/admin-ticket`, {
72
+ body: options.returnPath ? { returnPath: options.returnPath } : {},
73
+ });
74
+ }
64
75
  }
65
76
  exports.MerchantsResource = MerchantsResource;
66
77
  function encodeId(id) {
@@ -46,8 +46,8 @@ class ProvisionStoreUseCase {
46
46
  });
47
47
  }
48
48
  progress('settled', merchant, status.deployStatus);
49
- if (status.deployStatus === 'failed') {
50
- throw new errors_ts_1.VivoaProvisioningFailedError(merchant.id, 'the platform reported runtime status "failed"');
49
+ if (status.deployStatus === 'failed' || status.deployStatus === 'unknown') {
50
+ throw new errors_ts_1.VivoaProvisioningFailedError(merchant.id, `the platform reported runtime status "${status.deployStatus}"`);
51
51
  }
52
52
  return { merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain }, status, created };
53
53
  }
@@ -49,6 +49,22 @@ export interface CreateMerchantInput {
49
49
  */
50
50
  externalId?: string;
51
51
  }
52
+ /** Options for `merchants.adminTicket()`. */
53
+ export interface AdminTicketOptions {
54
+ /**
55
+ * Same-origin absolute path to land on inside the store admin (defaults to
56
+ * `/admin`). Must start with a single `/` — the platform re-validates and
57
+ * falls back to `/admin` on anything scheme-relative or malformed.
58
+ */
59
+ returnPath?: string;
60
+ }
61
+ /**
62
+ * Single-use SSO handoff into a merchant's store admin. `redirectUrl` is signed
63
+ * in AS the store owner and expires in ~60s — open it immediately, never cache.
64
+ */
65
+ export interface MerchantAdminTicket {
66
+ redirectUrl: string;
67
+ }
52
68
  export interface ListMerchantsQuery {
53
69
  page?: number;
54
70
  /** Max 100. */
@@ -5,7 +5,7 @@ export interface WebhookConfig {
5
5
  configured: boolean;
6
6
  url: string | null;
7
7
  /** [] = all merchant events. */
8
- events: string[];
8
+ events: MerchantEventType[];
9
9
  active: boolean;
10
10
  updatedAt: string | null;
11
11
  }
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "0.2.0";
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_VERSION = void 0;
4
+ exports.SDK_VERSION = '0.2.0';
@@ -152,7 +152,7 @@ export class FakeVivoa {
152
152
  async route(op, method, url, rawBody) {
153
153
  const path = url.pathname.replace(/^\/partner\/v1/, '');
154
154
  const body = rawBody ? JSON.parse(rawBody) : {};
155
- const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate)$/);
155
+ const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket)$/);
156
156
  if (method === 'GET' && path === '/me')
157
157
  return this.profile(op);
158
158
  if (method === 'POST' && path === '/me/rotate-secret') {
@@ -189,6 +189,16 @@ export class FakeVivoa {
189
189
  this.emit(op, 'merchant.reactivated', merchant);
190
190
  return { storeId: merchant.id, status: 'active' };
191
191
  }
192
+ if (action === 'admin-ticket') {
193
+ if (merchant.status !== 'active')
194
+ throw new HttpError(403, 'Store is not active');
195
+ const rawPath = typeof body.returnPath === 'string' ? body.returnPath : '/admin';
196
+ const returnPath = rawPath.startsWith('/') && !rawPath.startsWith('//') ? rawPath : '/admin';
197
+ const base = merchant.domain ? `https://${merchant.domain}` : `https://${merchant.handle}.vivoa.test`;
198
+ return {
199
+ redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
200
+ };
201
+ }
192
202
  }
193
203
  }
194
204
  if (method === 'GET' && path === '/fleet/analytics')
@@ -268,10 +278,12 @@ export class FakeVivoa {
268
278
  throw new HttpError(500, `Could not allocate a free handle for "${name}"`);
269
279
  }
270
280
  activate(op, m) {
271
- if (m.runtime === 'creating' || m.runtime === 'live' || m.runtime === 'suspended') {
281
+ if (m.runtime === 'creating' || m.runtime === 'live' || m.runtime === 'suspended' || m.runtime === 'unknown') {
272
282
  throw new HttpError(409, m.runtime === 'suspended'
273
283
  ? `Merchant '${m.handle}' is suspended — use reactivate instead`
274
- : `Merchant '${m.handle}' is already ${m.runtime}`);
284
+ : m.runtime === 'unknown'
285
+ ? `Merchant '${m.handle}' is in an unrecoverable state`
286
+ : `Merchant '${m.handle}' is already ${m.runtime}`);
275
287
  }
276
288
  switch (this.activation) {
277
289
  case 'fail':
@@ -10,7 +10,8 @@ import { type ProvisionStoreOptions, type ProvisionStoreResult } from './use-cas
10
10
  import { type VerifyWebhookParams } from '../webhooks/verify-webhook.ts';
11
11
  import type { CreateMerchantInput, MerchantStatus } from '../domain/merchant.ts';
12
12
  import type { WebhookEvent } from '../domain/webhook.ts';
13
- export declare const SDK_VERSION = "0.1.0";
13
+ import { SDK_VERSION } from '../version.ts';
14
+ export { SDK_VERSION };
14
15
  /** Every adapter is injected — the client itself has no I/O of its own. */
15
16
  export interface PartnerClientDeps {
16
17
  baseUrl: string;
@@ -6,7 +6,8 @@ import { WebhooksResource } from "./resources/webhooks.js";
6
6
  import { FleetResource } from "./resources/fleet.js";
7
7
  import { ProvisionStoreUseCase, } from "./use-cases/provision-store.js";
8
8
  import { verifyWebhook } from "../webhooks/verify-webhook.js";
9
- export const SDK_VERSION = '0.1.0';
9
+ import { SDK_VERSION } from "../version.js";
10
+ export { SDK_VERSION };
10
11
  export class VivoaPartnerClient {
11
12
  me;
12
13
  merchants;
@@ -1,5 +1,5 @@
1
1
  import type { SignedHttpClient } from '../signed-http.ts';
2
- import { type CreateMerchantInput, type ListMerchantsQuery, type Merchant, type MerchantStatus, type Page } from '../../domain/merchant.ts';
2
+ import { type AdminTicketOptions, type CreateMerchantInput, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page } from '../../domain/merchant.ts';
3
3
  /** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
4
4
  export declare const DEFAULT_ACTIVATE_TIMEOUT_MS: number;
5
5
  export declare class MerchantsResource {
@@ -33,5 +33,11 @@ export declare class MerchantsResource {
33
33
  storeId: string;
34
34
  status: 'active';
35
35
  }>;
36
+ /**
37
+ * Mints a single-use SSO URL into this merchant's store admin, signed in AS
38
+ * the store owner. The link expires in ~60s — redirect to it right away, do
39
+ * not store it. 403 if the merchant is not in your fleet or is not active.
40
+ */
41
+ adminTicket(merchantId: string, options?: AdminTicketOptions): Promise<MerchantAdminTicket>;
36
42
  }
37
43
  export declare function validateCreate(input: CreateMerchantInput): void;
@@ -26,12 +26,13 @@ export class MerchantsResource {
26
26
  /** Walks every page of the fleet. */
27
27
  async *listAll(query = {}) {
28
28
  const limit = query.limit ?? 100;
29
- for (let page = 1;; page++) {
29
+ for (let page = 1; page <= 10_000; page++) {
30
30
  const res = await this.list({ ...query, page, limit });
31
31
  yield* res.data;
32
32
  if (res.data.length === 0 || page * limit >= res.meta.total)
33
33
  return;
34
34
  }
35
+ throw new Error('listAll: exceeded maximum page limit (10 000)');
35
36
  }
36
37
  /** The merchant created with this reference, or `null` — an exact server-side match. */
37
38
  async findByExternalId(externalId) {
@@ -57,6 +58,16 @@ export class MerchantsResource {
57
58
  async reactivate(merchantId) {
58
59
  return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/reactivate`);
59
60
  }
61
+ /**
62
+ * Mints a single-use SSO URL into this merchant's store admin, signed in AS
63
+ * the store owner. The link expires in ~60s — redirect to it right away, do
64
+ * not store it. 403 if the merchant is not in your fleet or is not active.
65
+ */
66
+ async adminTicket(merchantId, options = {}) {
67
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/admin-ticket`, {
68
+ body: options.returnPath ? { returnPath: options.returnPath } : {},
69
+ });
70
+ }
60
71
  }
61
72
  function encodeId(id) {
62
73
  if (!id)
@@ -43,8 +43,8 @@ export class ProvisionStoreUseCase {
43
43
  });
44
44
  }
45
45
  progress('settled', merchant, status.deployStatus);
46
- if (status.deployStatus === 'failed') {
47
- throw new VivoaProvisioningFailedError(merchant.id, 'the platform reported runtime status "failed"');
46
+ if (status.deployStatus === 'failed' || status.deployStatus === 'unknown') {
47
+ throw new VivoaProvisioningFailedError(merchant.id, `the platform reported runtime status "${status.deployStatus}"`);
48
48
  }
49
49
  return { merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain }, status, created };
50
50
  }
package/dist/esm/cli.js CHANGED
@@ -13,6 +13,7 @@ const USAGE = `vivoa-partner <command> [options]
13
13
  merchants list [--search s] [--external-id id] [--page n] [--limit n]
14
14
  merchants status <id>
15
15
  merchants suspend <id> | reactivate <id>
16
+ merchants admin-ticket <id> [--return-path /admin/team]
16
17
  store create --external-id id --name n --email e --countries SV,GT [--industry i] --yes
17
18
  Create (or resume) + activate + wait until live.
18
19
  The platform assigns the handle; --external-id is
@@ -36,6 +37,7 @@ async function main(argv) {
36
37
  industry: { type: 'string' },
37
38
  url: { type: 'string' },
38
39
  events: { type: 'string' },
40
+ 'return-path': { type: 'string' },
39
41
  yes: { type: 'boolean', default: false },
40
42
  help: { type: 'boolean', short: 'h', default: false },
41
43
  },
@@ -69,6 +71,10 @@ async function main(argv) {
69
71
  return client.merchants.suspend(required(arg, '<id>'));
70
72
  case 'merchants reactivate':
71
73
  return client.merchants.reactivate(required(arg, '<id>'));
74
+ case 'merchants admin-ticket':
75
+ return client.merchants.adminTicket(required(arg, '<id>'), {
76
+ returnPath: values['return-path'],
77
+ });
72
78
  case 'store create': {
73
79
  if (!values.yes)
74
80
  throw new VivoaError('store create provisions REAL infrastructure — re-run with --yes');
@@ -49,6 +49,22 @@ export interface CreateMerchantInput {
49
49
  */
50
50
  externalId?: string;
51
51
  }
52
+ /** Options for `merchants.adminTicket()`. */
53
+ export interface AdminTicketOptions {
54
+ /**
55
+ * Same-origin absolute path to land on inside the store admin (defaults to
56
+ * `/admin`). Must start with a single `/` — the platform re-validates and
57
+ * falls back to `/admin` on anything scheme-relative or malformed.
58
+ */
59
+ returnPath?: string;
60
+ }
61
+ /**
62
+ * Single-use SSO handoff into a merchant's store admin. `redirectUrl` is signed
63
+ * in AS the store owner and expires in ~60s — open it immediately, never cache.
64
+ */
65
+ export interface MerchantAdminTicket {
66
+ redirectUrl: string;
67
+ }
52
68
  export interface ListMerchantsQuery {
53
69
  page?: number;
54
70
  /** Max 100. */
@@ -5,7 +5,7 @@ export interface WebhookConfig {
5
5
  configured: boolean;
6
6
  url: string | null;
7
7
  /** [] = all merchant events. */
8
- events: string[];
8
+ events: MerchantEventType[];
9
9
  active: boolean;
10
10
  updatedAt: string | null;
11
11
  }
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "0.2.0";
@@ -0,0 +1 @@
1
+ export const SDK_VERSION = '0.2.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vivoa/partner-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Official SDK for the Vivoa Partner API — create and operate stores for your merchants",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",