# @neetru/sdk

> Biblioteca runtime oficial pra consumir o ecossistema Neetru a partir de produtos SaaS (Next.js, Node ≥20, browser, Edge runtimes).

<p>
  <img alt="npm" src="https://img.shields.io/npm/v/@neetru/sdk?logo=npm">
  <img alt="downloads" src="https://img.shields.io/npm/dm/@neetru/sdk?logo=npm">
  <img alt="bundle size" src="https://img.shields.io/bundlephobia/minzip/@neetru/sdk">
  <img alt="Node" src="https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white">
  <img alt="license" src="https://img.shields.io/badge/license-MIT-22c55e">
</p>

> ## ⚠ 3.0 é breaking — leia o [CHANGELOG](./CHANGELOG.md) antes de upgrade
>
> SDK 2.x estava com 4 namespaces broken contra Core LIVE. 3.0 corrige paths canonical, exige `productId` em mutations, adiciona `Idempotency-Key` automático, `baseUrl` allowlist fail-closed, e `verifyWebhookSignature` virou Universal async via WebCrypto.
>
> **Métodos removidos**: `usage.track`, `usage.getQuota` (use `usage.report` / `usage.check`).
>
> **Migration guide completo no CHANGELOG.md**.

## Instalação

```bash
npm install @neetru/sdk
```

## Hello world (3.0)

```ts
import { createNeetruClient } from '@neetru/sdk';

const neetru = createNeetruClient({
  apiKey: process.env.NEETRU_API_KEY,    // nrt_<keyId>_<secret>
  productId: 'gestovendas',              // 3.0: default p/ todos namespaces
  tenantId: 't-acme',                    // 3.0: default p/ usage
  env: 'prod',                           // 'dev' = mocks in-memory
});

// Auth
const user = await neetru.auth.signIn();

// Meter consumo (canonical /api/sdk/v1/usage/record)
const r = await neetru.usage.report('api_call', 1);
console.log(r.value, '/', r.limit, '— restam', r.remaining);

// Entitlement check com behavior=readonly
const check = await neetru.usage.check('api_call');
if (!check.allowed && check.behavior === 'readonly') {
  // UI degradada — read-only sem quebrar engajamento
}
```

## Princípios

- **Vendor-neutral** — superfície pública NÃO vaza Firebase/Stripe/etc. Backend Neetru pode mudar no futuro sem reescrever produto.
- **Tree-shakable** — ESM-first, sem side-effects. Importe só os namespaces que usa.
- **Universal** — browser, Node ≥20, Edge runtimes (Vercel Edge, Cloudflare Workers). Usa `fetch` global + WebCrypto.
- **Fail-closed segurança** — `baseUrl` allowlist (`*.neetru.com` + localhost) rejeita phishing/MITM. Bypass: `NEETRU_ALLOW_INSECURE_BASEURL=1`.
- **Idempotency-Key automático** — mutations (`usage.report`, `webhooks.test`, `notifications.send`, `support.createTicket`) injetam UUID v4 header. `retries: 0` default em POSTs para defesa em camada.
- **Tipado** — erros via `NeetruError` com `.code`, `.status`, `.requestId`.
- **Dev mode** — `NEETRU_ENV=dev` ativa mocks automáticos — zero rede em testes locais.

## Namespaces 3.0

| Namespace | O que oferece | Endpoint |
|---|---|---|
| `auth` | OIDC sign-in/out + verifyToken | `auth.neetru.com/api/v1/oauth/*` |
| `catalog` | Produtos públicos (`list` / `get`) | `/api/sdk/v1/catalog` |
| `entitlements` | Verificação boolean simples (legacy compat) | `/api/v1/sdk/entitlements/check` |
| `telemetry` | `event(...)` + `track(...)` + `log(...)` | `/api/sdk/v1/telemetry/{event,log}` |
| `usage` | `report` / `check` (metering + entitlement full) | `/api/sdk/v1/usage/record`, `/api/sdk/v1/entitlements` |
| `support` | `createTicket` / `listMyTickets` | `/api/sdk/v1/support/tickets` ⭐ 3.0 |
| `db` | Coleções tenant-scoped (offline-first v2.0) | `/api/sdk/v1/datastore/*` |
| `checkout` | Stripe Checkout intent | `/api/v1/checkout/intents` |
| `webhooks` | Produtos registram URL pra receber eventos | `/api/sdk/v1/webhooks` |
| `notifications` | Produto envia notification in-app | `/api/sdk/v1/notifications` |

## Exemplos por namespace

### Login OIDC — Authorization Code + PKCE (3.1.3)

O SDK implementa o lado-**browser** do fluxo; a troca `code → tokens` acontece no
**backend do produto** (confidential client com `client_id` + `client_secret`
registrados em `oidc_clients/{id}` no Core — modelo Firebase/Facebook).

```ts
// 1) Browser — iniciar login (gera state CSRF + nonce + PKCE S256 e redireciona)
const neetru = createNeetruClient({ oidcClientId: 'meu-app-oidc' });
await neetru.auth.signIn({
  redirectUri: 'https://app.exemplo.com/auth/callback',
  postLoginRedirect: '/dashboard',
});

// 2) Browser — na página de callback: valida state e devolve o material do exchange
const cb = await neetru.auth.handleRedirectCallback();
if (cb) {
  // mande pro SEU backend (nunca exponha client_secret no browser)
  await fetch('/api/auth/exchange', { method: 'POST', body: JSON.stringify(cb) });
  location.assign(cb.postLoginRedirect ?? '/');
}

// 3) Backend do produto — troca o code por tokens no IdP Neetru
//    POST https://auth.neetru.com/api/v1/oauth/token  (x-www-form-urlencoded)
//    Authorization: Basic base64(clientId:clientSecret)
//    grant_type=authorization_code & code & redirect_uri & code_verifier
//    → valide claim `nonce` do id_token === cb.nonce; sete cookie httpOnly.

// 4) Server-side — valide o id_token em qualquer request (JWKS cacheado)
const user = await neetru.auth.verifyToken(idTokenDoCookie);
```

`auth.getIdToken()` expõe o JWT cru se o app optar por armazená-lo em
`localStorage['neetru_id_token']` (alternativa SPA; o padrão recomendado é o
cookie httpOnly do backend).

### Webhooks outbound (v1.2)

```ts
await neetru.webhooks.register({
  url: 'https://meu-produto.com/webhooks/neetru',
  events: ['subscription.activated', 'subscription.cancelled', 'usage.quota_exceeded'],
  secret: 'chave-32-chars-pra-hmac-sha256',
});

const endpoints = await neetru.webhooks.list();
const test = await neetru.webhooks.test(endpoints[0].id);
console.log(test.statusCode, test.durationMs);
```

Eventos recebidos no seu endpoint chegam com:
- `X-Neetru-Signature: sha256=<hmac>` (se `secret` registrado)
- `X-Neetru-Timestamp: <ms>` (replay protection — rejeitar > 5min skew)

#### Verificar assinatura no consumer (3.1.3 — async + Universal)

O HMAC do Core cobre `` `${timestamp}.${rawBody}` `` (o timestamp é o valor de
`X-Neetru-Timestamp`). Prefira `verifyWebhookRequest`, que lê os headers,
valida o skew (5 min) e o HMAC de uma vez:

```ts
import { verifyWebhookRequest } from '@neetru/sdk';

// Next.js Route Handler (Edge OU Node runtime — funciona em ambos)
export async function POST(req: Request) {
  const raw = await req.text();
  const result = await verifyWebhookRequest(raw, req.headers, process.env.NEETRU_WEBHOOK_SECRET!);
  if (!result.ok) return new Response('unauthorized', { status: 401 });

  const event = JSON.parse(raw);
  // ... handle event
  return Response.json({ ok: true });
}
```

Verificação manual com `verifyWebhookSignature` exige passar o timestamp:

```ts
const ok = await verifyWebhookSignature(
  raw,
  req.headers.get('X-Neetru-Signature'),
  process.env.NEETRU_WEBHOOK_SECRET!,
  req.headers.get('X-Neetru-Timestamp') ?? undefined,
);
```

> **Breaking 3.0**: era `boolean` sync usando `node:crypto`. Agora `Promise<boolean>` via WebCrypto.subtle — funciona em Cloudflare Workers, Vercel Edge, browsers e Node ≥20.
> **Fix 3.1.3**: até 3.1.2 o helper verificava o HMAC só sobre o body — entregas reais do Core (assinadas com `timestamp.body`) sempre falhavam. Passe o `timestamp` (ou use `verifyWebhookRequest`).

### Notifications produto → user (v1.2)

```ts
await neetru.notifications.send({
  userId: 'usr_xyz',
  kind: 'order.received',
  severity: 'success',
  title: 'Novo pedido #1234',
  body: 'Pedido de R$ 89,90',
  link: '/orders/1234',
  fingerprint: 'order:1234',           // dedup < 24h
});

const notifs = await neetru.notifications.list('usr_xyz', { onlyUnread: true });
await neetru.notifications.markRead(notifs[0].id);
```

### Entitlements

```ts
const ok = await neetru.entitlements.check('gestovendas', 'ai_recommendations');
const detailed = await neetru.entitlements.detailed('gestovendas', 'ai_recommendations');
// → { allowed, planId, remaining?, limit?, reason }
```

### Usage tracking

```ts
await neetru.usage.track('report_generated', { count: 42, format: 'pdf' });

const quota = await neetru.usage.getQuota('reports_per_month');
// → { used: 15, limit: 100, resetsAt: '...', plan: 'starter' }
```

### Support

```ts
await neetru.support.createTicket({
  subject: 'Bug ao exportar CSV',
  message: 'Quando seleciono >1000 rows o export falha',
  severity: 'high',
});

const myTickets = await neetru.support.listMyTickets({ status: 'open' });
```

### DB (tenant-scoped, vendor-neutral)

```ts
await neetru.db.add('orders', { customerId: 'usr_x', total: 9990 });
const orders = await neetru.db.list('orders', {
  where: [['customerId', '==', 'usr_x']],
  orderBy: 'createdAt',
  limit: 50,
});
```

### Checkout

```ts
const intent = await neetru.checkout.start({
  productId: 'gestovendas',
  planId: 'pro',
  tenantType: 'company',
  tenantId: 'company_xyz',
});
window.location.href = intent.checkoutUrl;
```

## Modo dev (mocks automáticos)

```ts
const neetru = createNeetruClient({ env: 'dev' });
// auth retorna DEV_FIXTURE_USER, usage/db/webhooks/notifications são in-memory
```

Override pra testes determinísticos:

```ts
import { MockAuth, MockUsage } from '@neetru/sdk';

const neetru = createNeetruClient({
  apiKey: 'nrt_test',
  env: 'prod',
  mocks: {
    auth: new MockAuth({ user: { uid: 'test', email: 'a@b' } }),
    usage: new MockUsage(),
  },
});
```

## Versionamento

- **SemVer estrito** — breaking changes só em major.
- v1.0 GA (2026-05-06) — superfície estável de 7 namespaces
- v1.1 (2026-05) — checkout namespace
- v1.2 (2026-05) — webhooks + notifications namespaces

`initNeetru` (API v0.0.1) está deprecated desde v0.2 — funciona até v2.0.

## Stack

- TypeScript 5 ESM-first
- Zero dependências runtime (usa `fetch` global)
- Bundle minzip <10KB (todos namespaces somados, tree-shake-friendly)

## Testes de contrato (staging)

O arquivo `src/__tests__/contract-staging.test.ts` contém testes que exercitam o HTTP real contra um ambiente de staging. Por default, todos os testes são skippados quando `NEETRU_STAGING_URL` não estiver definido.

Para rodar:

```bash
NEETRU_STAGING_URL=https://api.staging.neetru.com \
NEETRU_STAGING_API_KEY=nrt_<keyId>_<secret> \
npx vitest run src/__tests__/contract-staging.test.ts
```

Variáveis de ambiente:

| Variável | Obrigatória | Descrição |
|---|---|---|
| `NEETRU_STAGING_URL` | Sim | URL base da API de staging |
| `NEETRU_STAGING_API_KEY` | Sim | API key no formato `nrt_<keyId>_<secret>` |
| `NEETRU_STAGING_PRODUCT_ID` | Não | productId cadastrado no staging (default: `gestovendas`) |
| `NEETRU_STAGING_TENANT_ID` | Não | tenantId válido no staging (default: `t-acme`) |

Namespaces cobertos: `catalog.list`, `catalog.get`, `entitlements.check`, `entitlements.checkDetailed`, `usage.report`, `usage.check`, `telemetry.event`, `telemetry.log`, `auth.getUser`, `auth.signIn`.

Os testes assertam apenas o shape/tipos da resposta, nunca valores exatos — staging pode ter dados variáveis mas o contrato de formato deve ser estável.

## Mais info

- **Repo:** [github.com/Neetru/neetru-core](https://github.com/Neetru/neetru-core)
- **CLI complementar:** `npm install -g @neetru/cli`

## Licença

MIT © Neetru
