# SDK @capibara-dev/sdk

> Le SDK TypeScript officiel : types du Kit, du manifeste, des scopes et des façades générés depuis les schémas de la plateforme, defineApp + ui, harnais de test local, client API, OAuth PKCE et webhooks — zéro dépendance.
> Catégorie : Référence · mis à jour le 2026-09-04 · plateforme v1

## Statut : disponible (v0.3.0, registre npm)

```bash
npm install -D @capibara-dev/sdk
```

Sans accès au registre npm, le tarball servi par le portail fait la même
chose (`curl -O https://capibara.fr/dev-sdk/capibara-dev-sdk-0.3.0.tgz &&
npm install -D ./capibara-dev-sdk-0.3.0.tgz`). Zéro dépendance, compatible
Node ≥ 18, navigateurs et workers (`fetch` + WebCrypto). Les starters de la
[CLI](/dev/docs/cli) l'installent d'office.

Dans la sandbox, `@capibara-dev/sdk` est **fourni par la plateforme** (même
API, résolu par le bundler) : le paquet npm sert au **typage**, aux **tests
locaux** et au **backend externe**.

---

## Généré depuis les schémas de la plateforme

Le cœur du paquet (`generated.ts`) est produit par la plateforme à partir
des mêmes schémas Zod qui valident vos manifestes, vos documents du Kit et
les appels de façades. Un test anti-dérive dans la CI de Capibara garantit
qu'il correspond exactement au validateur — vous n'aurez jamais un type qui
accepte ce que le portail refuse.

| Export | Contenu |
|---|---|
| `KitNode`, `KitStackNode`, `KitTableNode`, `KitFieldNode`… | Un type par composant du [Kit](/dev/docs/kit) (38), plus `KitRefNode`, `KitEachNode`, `KitFallbackNode`. |
| `KitStackProps`, `KitButtonProps`… + `KitUiBuilders` | Les propriétés de chaque constructeur `ui.<composant>` — l'autocomplétion connaît chaque prop, chaque énumération. |
| `APP_SCOPES`, `AppScope`, `APP_SCOPE_DEFS` | Le catalogue des [scopes feuilles](/dev/docs/permissions) acceptés dans `permissions`, avec leur phrase de consentement. |
| `AppApi`, `APP_FACADES`, `APP_FACADE_NAMES`, `CrmContactsListInput`… | Les [façades](/dev/docs/facades) typées : `ctx.capibara.crm.contacts.list()` côté hébergé, `client.install(id).crm.contacts.list()` côté externe, avec leurs entrées et sorties. |
| `APP_EVENTS`, `AppEventType` | Le catalogue fermé des événements (`events` du manifeste, [webhooks](/dev/docs/webhooks)). |
| `ADDON_CAPABILITIES`, `ADDON_CATEGORIES`, `APP_SURFACES`, `PANEL_SUBJECTS`, `DECLARATIVE_WIDGET_TEMPLATES` | Les listes fermées du [manifeste](/dev/docs/manifeste). |

---

## Le code hébergé : `defineApp` + `ui`

```ts
import { defineApp, ui, toast, type AppContext } from '@capibara-dev/sdk';

export default defineApp({
  pages: {
    home: async ({ ctx }) => ui.doc(ui.stack({ gap: 'md' }, [
      ui.pageHeader({ title: 'Bonjour ' + (ctx.viewer?.name ?? 'vous') }),
      ui.kpi({ label: 'Compteur', value: (await ctx.kv.get<number>('count')) ?? 0 }),
      ui.button({ label: '+1', action: 'count.add' }),
    ])),
  },
  actions: {
    'count.add': async ({ ctx }) => {
      const n = ((await ctx.kv.get<number>('count')) ?? 0) + 1;
      await ctx.kv.put('count', n);
      return { ...toast('Compteur : ' + n, 'success'), refresh: true };
    },
  },
});
```

- `ui.<composant>(props, children?)` pour chacun des 38 composants, plus
  `ui.doc(root, { title, data, components, state })`, `ui.ref(component,
  props)` (instance d'un composant réutilisable), `ui.each(path, as, gabarit)`
  (répétition liée aux données) et `ui.fallback(text)` (texte de repli).
- `toast()`, `navigate()`, `json()`, `text()` : les réponses prêtes à
  renvoyer depuis une action ou une route HTTP.
- Types du contrat : `AppDefinition`, `AppContext` (`kv`, `data`, `files`,
  `fetch`, `capibara`, `log`, `followup`…), `RenderArgs`, `ActionArgs`,
  `ActionResult`, `KitDocument`. Tout est décrit dans [Le runtime](/dev/docs/runtime).
- Surfaces du **site public** : `siteBlocks` et `sitePages` — mêmes
  handlers, `viewer` toujours `null`, le visiteur dans `visitor`
  (`AppVisitor`) ; le harnais les rend avec
  `renderSurface(app, 'siteBlock', 'promo', t, { params, visitor })`.

---

## Tester en local : `@capibara-dev/sdk/testing`

Le harnais exécute vos handlers **hors de la plateforme**, avec un `ctx`
simulé en mémoire — sans réseau, sans compte :

```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createTestContext, renderSurface, runAction, findNodes, hasText } from '@capibara-dev/sdk/testing';
import app from '../src/index';

test('la page et le compteur', async () => {
  const t = createTestContext({
    viewer: { name: 'Léa' },                       // null = anonyme (surface publique)
    install: { config: { defaultCity: 'Lyon' } },  // vos réglages / config de widget
    fetch: { 'api.example.com': { json: { ok: true } } },  // un faux par host déclaré
    capibara: { 'crm.contacts.list': async () => ({ items: [], nextCursor: null }) },
    data: { favorites: [{ key: 'lyon', data: { city: 'Lyon' } }] },
  });
  const doc = await renderSurface(app, 'page', 'home', t);   // KitDocument normalisé
  assert.equal(hasText(doc, 'Bonjour Léa'), true);
  assert.equal(findNodes(doc, 'kpi').length, 1);

  const r = await runAction(app, 'count.add', t);            // { toast, refresh, ui… }
  assert.equal(r.refresh, true);
  assert.equal(t.kv.get('count'), 1);
  assert.deepEqual(t.logs, []);                              // ctx.log capturé
});
```

- `createTestContext({ tenant, viewer, visitor, install, subject, state,
  fetch, capibara, kv, data, now })` → `{ ctx, kv, collections, files, logs,
  followups, calls, mails, notifications, calendar, searchDocs, approvals,
  decideApproval, tick }` — `kv`/`collections`/`files` sont des `Map`
  inspectables, `calls` le journal de chaque appel `ctx.*`, `mails` /
  `notifications` / `calendar` / `searchDocs` / `approvals` ce que les
  [moteurs](/dev/docs/moteurs) ont reçu, `decideApproval(id, décision)`
  fabrique l'événement `approval.decided` à rejouer avec `runEvent`. Un
  host non déclaré dans `fetch` ou une façade non simulée lève une erreur
  explicite — comme la plateforme refuserait.
- `renderSurface(app, surface, key, t, { params, subject, state })`,
  `runAction(app, action, t, { form, params, formName, state })`,
  `runEvent(app, { type, data }, t)`, `runSchedule(app, key, t)`,
  `runHttp(app, path, t, { method, query, headers, body })`.
- Assertions : `walkKit`, `findNodes(doc, type)`, `kitStrings`,
  `hasText`, `jsonResponse`, `textResponse`.

Le harnais ne remplace pas la validation du Kit (bornes, a11y) : elle vit
dans [Kit Studio](/dev/kit-studio), `capibara kit check` et, en dernier
ressort, dans le journal de votre installation de test.

---

## Hors de la sandbox : client API, backend externe, webhooks

- **`CapibaraClient`** — client typé de l'[API publique](/dev/docs/api-publique) :
  `Bearer` clé API (`ck_live_…`) ou jeton OAuth, `me()`, `facades()`,
  `install(installId)` → les façades typées de cette installation
  (`X-Capibara-Install`), erreurs `CapibaraApiError` (statut, corps,
  `retryAfterSeconds` sur 429).
- **`createBackend({ app, secret, capibara?, maxSkewSec?, onLog? })`** →
  `{ handle, fetch, dispatch }` — votre serveur sert le MÊME `defineApp`
  (manifeste `backend: { url }`). Tout le détail (signature, protocole,
  `ctx` réduit, tests) est sur [Backend externe](/dev/docs/backend-externe).
- **`verifyBackendSignature(rawBody, header, secret)`** si vous branchez
  vous-même.
- **`createServerHandler({ secret, handlers, maxAgeSec? })`** — un seul endpoint pour vos
  [webhooks sortants](/dev/docs/webhooks) : signature HMAC vérifiée sur le
  corps **brut** (`401` sinon), enveloppe typée `{ id, event, occurredAt,
  tenantId, data }`, dispatch par type d'événement, `500` si votre handler
  lève (Capibara réessaie). `handle(rawBody, headers)` pour Express et
  consorts, `fetch(request)` pour les runtimes à `Request`. `maxAgeSec`
  refuse (`401 stale`) une livraison dont l'en-tête `x-capibara-timestamp`
  est absent ou trop ancien.
- **`verifyWebhookSignature(rawBody, header, secret)`** si vous préférez
  brancher vous-même.
- **Aides OAuth PKCE** — `generateCodeVerifier()`, `codeChallengeS256()`,
  `buildAuthorizeUrl()`, `exchangeAuthorizationCode()`,
  `clientCredentialsToken()` : [Sign in with Capibara](/dev/docs/oauth).

```ts
import { createServerHandler } from '@capibara-dev/sdk';

const webhooks = createServerHandler({
  secret: process.env.CAPIBARA_WEBHOOK_SECRET!,
  handlers: {
    'invoice.paid': async ({ tenantId, data }) => { /* votre traitement, idempotent */ },
  },
});
// Cloudflare Workers / Deno / Bun : export default { fetch: (req) => webhooks.fetch(req) }
// Node/Express : const { status, body } = await webhooks.handle(rawBody, req.headers);
```

---

## Types du manifeste

`AddonManifest`, `AddonCapability`, `AddonPermission` (= `AppScope`),
`AddonPricing`, `AddonMenuEntry`, `AddonWidgetContribution`
(`AddonKitWidget` | `AddonDeclarativeWidget` | `AddonEmbedWidget`),
`AddonPanelContribution`, `AddonSiteBlockContribution`,
`AddonSitePageContribution`, `AddonSchedule`, `AddonCollectionDecl`,
`AddonSettingField` — le miroir typé de [add-on.json](/dev/docs/manifeste).
La validation autoritaire reste celle du portail et de `capibara dev`.

---

## Construire sans le SDK reste possible

Le SDK est un confort, pas un prérequis :

- L'éditeur du portail applique en direct le **même JSON Schema** que les
  types du SDK — vos manifestes sont validés à la même source de vérité.
- Le [Kit](/dev/docs/kit) se décrit en JSON ; `ui.*` ne fait que le
  construire.
- L'[API publique](/dev/docs/api-publique) s'appelle avec n'importe quel
  client HTTP (`fetch`, `curl`…), le flux [OAuth PKCE](/dev/docs/oauth) est
  documenté avec des séquences complètes.

Cette documentation (les pages du hub + les endpoints `llms.txt` décrits
dans [Utiliser cette doc avec votre IA](/dev/docs/llm)) est conçue pour
suffire, avec l'aide d'un assistant IA si vous le souhaitez, à construire une
app complète.
