@neetru/sdk
v3.1.12
Published
Neetru SDK 3.0 — biblioteca runtime universal (browser/Node/Edge) para consumir o ecossistema Neetru. Paths canonical /api/sdk/v1/*, productId obrigatório, Idempotency-Key em mutations, WebCrypto signatures, baseUrl allowlist fail-closed.
Readme
@neetru/sdk
Biblioteca runtime oficial pra consumir o ecossistema Neetru a partir de produtos SaaS (Next.js, Node ≥20, browser, Edge runtimes).
⚠ 3.0 é breaking — leia o CHANGELOG antes de upgrade
SDK 2.x estava com 4 namespaces broken contra Core LIVE. 3.0 corrige paths canonical, exige
productIdem mutations, adicionaIdempotency-Keyautomático,baseUrlallowlist fail-closed, everifyWebhookSignaturevirou Universal async via WebCrypto.Métodos removidos:
usage.track,usage.getQuota(useusage.report/usage.check).Migration guide completo no CHANGELOG.md.
Instalação
npm install @neetru/sdkHello world (3.0)
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
fetchglobal + WebCrypto. - Fail-closed segurança —
baseUrlallowlist (*.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: 0default em POSTs para defesa em camada. - Tipado — erros via
NeetruErrorcom.code,.status,.requestId. - Dev mode —
NEETRU_ENV=devativa 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).
// 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)
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>(sesecretregistrado)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:
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:
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
booleansync usandonode:crypto. AgoraPromise<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 comtimestamp.body) sempre falhavam. Passe otimestamp(ou useverifyWebhookRequest).
Notifications produto → user (v1.2)
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
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
// Reporta consumo (resource, qty) — devolve {value, limit, remaining} inline,
// sem precisar de uma chamada de quota separada. Idempotency-Key automático.
const usage = await neetru.usage.report('reports_generated', 1);
// → { value: 16, limit: 100, remaining: 84, status: 'ok' }
// Verifica entitlement de um resource (sem incrementar o contador).
const ent = await neetru.usage.check('reports_per_month');
// → { allowed, limit?, remaining? }Support
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)
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
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)
const neetru = createNeetruClient({ env: 'dev' });
// auth retorna DEV_FIXTURE_USER, usage/db/webhooks/notifications são in-memoryOverride pra testes determinísticos:
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
fetchglobal) - 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:
NEETRU_STAGING_URL=https://api.staging.neetru.com \
NEETRU_STAGING_API_KEY=nrt_<keyId>_<secret> \
npx vitest run src/__tests__/contract-staging.test.tsVariá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
- CLI complementar:
npm install -g @neetru/cli
Licença
MIT © Neetru
