@neetru/sdk 3.6.0 → 3.8.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 (117) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +66 -1
  3. package/dist/ai.cjs +298 -391
  4. package/dist/ai.d.cts +1 -1
  5. package/dist/ai.d.ts +1 -1
  6. package/dist/ai.mjs +298 -391
  7. package/dist/auth.cjs +3717 -5005
  8. package/dist/auth.d.cts +1 -1
  9. package/dist/auth.d.ts +1 -1
  10. package/dist/auth.mjs +3714 -5002
  11. package/dist/build-guard.cjs +101 -119
  12. package/dist/build-guard.mjs +91 -109
  13. package/dist/catalog.cjs +220 -225
  14. package/dist/catalog.d.cts +1 -1
  15. package/dist/catalog.d.ts +1 -1
  16. package/dist/catalog.mjs +220 -225
  17. package/dist/checkout.cjs +307 -323
  18. package/dist/checkout.d.cts +1 -1
  19. package/dist/checkout.d.ts +1 -1
  20. package/dist/checkout.mjs +306 -322
  21. package/dist/db-react.cjs +59 -87
  22. package/dist/db-react.d.cts +1 -1
  23. package/dist/db-react.d.ts +1 -1
  24. package/dist/db-react.mjs +56 -84
  25. package/dist/db.cjs +2128 -3014
  26. package/dist/db.d.cts +1 -1
  27. package/dist/db.d.ts +1 -1
  28. package/dist/db.mjs +2141 -3027
  29. package/dist/entitlements.cjs +19 -20
  30. package/dist/entitlements.d.cts +1 -1
  31. package/dist/entitlements.d.ts +1 -1
  32. package/dist/entitlements.mjs +19 -20
  33. package/dist/errors.cjs +16 -12
  34. package/dist/errors.d.cts +59 -3
  35. package/dist/errors.d.ts +59 -3
  36. package/dist/errors.mjs +15 -12
  37. package/dist/firestore-compat.cjs +241 -297
  38. package/dist/firestore-compat.d.cts +1 -1
  39. package/dist/firestore-compat.d.ts +1 -1
  40. package/dist/firestore-compat.mjs +215 -271
  41. package/dist/index.cjs +3942 -5134
  42. package/dist/index.d.cts +181 -5
  43. package/dist/index.d.ts +181 -5
  44. package/dist/index.mjs +4040 -5239
  45. package/dist/mobile.cjs +459 -344
  46. package/dist/mobile.d.cts +59 -4
  47. package/dist/mobile.d.ts +59 -4
  48. package/dist/mobile.mjs +459 -344
  49. package/dist/mocks.cjs +319 -447
  50. package/dist/mocks.d.cts +1 -1
  51. package/dist/mocks.d.ts +1 -1
  52. package/dist/mocks.mjs +313 -441
  53. package/dist/notifications.cjs +296 -317
  54. package/dist/notifications.d.cts +1 -1
  55. package/dist/notifications.d.ts +1 -1
  56. package/dist/notifications.mjs +295 -316
  57. package/dist/react.cjs +557 -707
  58. package/dist/react.d.cts +1 -1
  59. package/dist/react.d.ts +1 -1
  60. package/dist/react.mjs +547 -697
  61. package/dist/support.cjs +402 -328
  62. package/dist/support.d.cts +29 -3
  63. package/dist/support.d.ts +29 -3
  64. package/dist/support.mjs +401 -328
  65. package/dist/telemetry.cjs +267 -304
  66. package/dist/telemetry.d.cts +1 -1
  67. package/dist/telemetry.d.ts +1 -1
  68. package/dist/telemetry.mjs +267 -304
  69. package/dist/{types-Du_iioJ7.d.cts → types-D-9bjlM0.d.cts} +74 -1
  70. package/dist/{types-BPATkXYu.d.ts → types-bVztFQTI.d.ts} +74 -1
  71. package/dist/usage.cjs +233 -230
  72. package/dist/usage.d.cts +1 -1
  73. package/dist/usage.d.ts +1 -1
  74. package/dist/usage.mjs +233 -230
  75. package/dist/webhooks.cjs +328 -362
  76. package/dist/webhooks.d.cts +1 -1
  77. package/dist/webhooks.d.ts +1 -1
  78. package/dist/webhooks.mjs +325 -359
  79. package/package.json +5 -3
  80. package/dist/ai.cjs.map +0 -1
  81. package/dist/ai.mjs.map +0 -1
  82. package/dist/auth.cjs.map +0 -1
  83. package/dist/auth.mjs.map +0 -1
  84. package/dist/build-guard.cjs.map +0 -1
  85. package/dist/build-guard.mjs.map +0 -1
  86. package/dist/catalog.cjs.map +0 -1
  87. package/dist/catalog.mjs.map +0 -1
  88. package/dist/checkout.cjs.map +0 -1
  89. package/dist/checkout.mjs.map +0 -1
  90. package/dist/db-react.cjs.map +0 -1
  91. package/dist/db-react.mjs.map +0 -1
  92. package/dist/db.cjs.map +0 -1
  93. package/dist/db.mjs.map +0 -1
  94. package/dist/entitlements.cjs.map +0 -1
  95. package/dist/entitlements.mjs.map +0 -1
  96. package/dist/errors.cjs.map +0 -1
  97. package/dist/errors.mjs.map +0 -1
  98. package/dist/firestore-compat.cjs.map +0 -1
  99. package/dist/firestore-compat.mjs.map +0 -1
  100. package/dist/index.cjs.map +0 -1
  101. package/dist/index.mjs.map +0 -1
  102. package/dist/mobile.cjs.map +0 -1
  103. package/dist/mobile.mjs.map +0 -1
  104. package/dist/mocks.cjs.map +0 -1
  105. package/dist/mocks.mjs.map +0 -1
  106. package/dist/notifications.cjs.map +0 -1
  107. package/dist/notifications.mjs.map +0 -1
  108. package/dist/react.cjs.map +0 -1
  109. package/dist/react.mjs.map +0 -1
  110. package/dist/support.cjs.map +0 -1
  111. package/dist/support.mjs.map +0 -1
  112. package/dist/telemetry.cjs.map +0 -1
  113. package/dist/telemetry.mjs.map +0 -1
  114. package/dist/usage.cjs.map +0 -1
  115. package/dist/usage.mjs.map +0 -1
  116. package/dist/webhooks.cjs.map +0 -1
  117. package/dist/webhooks.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,110 @@ All notable changes to `@neetru/sdk` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [3.8.0] - 2026-10-03 — **feat(apps): chave `nrt_` só de servidor, `verifyAccessToken`, mobile integrado, correlation id**
11
+
12
+ Release **minor**. Nenhuma API pública removida; produtos que ainda usam a chave no navegador **continuam funcionando** (só ganham um aviso). Base para produtos com app móvel (primeiro: PDV em React Native/Expo).
13
+
14
+ ### Contexto (decisão do dono, 2026-10-03)
15
+
16
+ - A credencial `nrt_` do produto é **SÓ DE SERVIDOR**. Site e app falam com a API do **próprio produto**; o backend do produto usa o SDK. Navegador/app nunca carregam segredo.
17
+ - Dados de negócio: app → `<produto>.neetru.com/api/v1` → SDK no servidor → Core. Direto ao Core só assuntos de plataforma com o **token do usuário** (login no auth.neetru.com, barra de conta, `/userinfo`).
18
+ - Corrige a premissa da 3.7.0 ("a chave `nrt_` … roda no browser"): isso deixa de ser aceito como desenho.
19
+
20
+ ### Adicionado
21
+
22
+ - **Guarda da chave fora do servidor.** `createNeetruClient` detecta chave (via `apiKey` ou `NEETRU_API_KEY`) rodando em **navegador** (`window` + `document`) ou **React Native** (`navigator.product === 'ReactNative'`):
23
+ - padrão nesta minor: **aviso único** no console, em PT-BR, explicando o caminho certo — **nunca imprime a chave**;
24
+ - `strictServerOnlyKey: true` → falha **FECHADO** com `NeetruError('server_only_key')` (`reason`: `browser` | `react-native`);
25
+ - **na próxima MAJOR (4.0) a falha fechada vira o padrão.** Node/Edge (servidor) não são afetados.
26
+ - `missing_api_key` fora do servidor agora explica que a chamada deve ir pela API do produto.
27
+ - **`verifyAccessToken(token, { audience, requiredScopes?, issuer?, jwksUrl?, clockToleranceSec?, allowClientCredentials? })`** — validação server-side do access_token do auth.neetru.com, no formato real do Core (`src/lib/oidc/tokens.ts`): RS256 via JWKS `…/api/v1/oauth/.well-known/jwks` (cache compartilhado com `auth.verifyToken`), `iss`, `exp` (tolerância 5s), **`aud ∈ audience`** (lista: web + iOS + Android do produto; vazia = `invalid_config`), formato de access token (`jti` + `scope` + `client_id` === `aud` — **id_token é recusado**), usuário final (token de máquina `client:` só com `allowClientCredentials`), escopos exigidos (`insufficient_scope`, 403). Validação local: token revogado vale até expirar (15 min no Core).
28
+ - **`authenticateProductRequest(req, { audience, cookie? })`** — a API do produto aceita `Authorization: Bearer <access_token>` **além** do cookie do site. Bearer inválido lança (nunca cai pro cookie em silêncio). Aceita `Request`, `Headers` ou `req.headers` do Node. Mais `accessTokenErrorToHttp(err)` (401/403 RFC 6750 com `WWW-Authenticate`, 503 se o JWKS cair, 500 sem vazar detalhe), `readBearerToken`, `readCookie`.
29
+ - **`@neetru/sdk/mobile` integrado:**
30
+ - **recusa `apiKey` SEMPRE** (`server_only_key`; no tipo, `apiKey?: never`) e exige `oidcClientId` (client OIDC público do app);
31
+ - guarda o **access_token** do `/token` e expõe `getAccessToken()` (renova 1x se vencido/a <30s);
32
+ - `getUserInfo()` no `/api/v1/oauth/userinfo` do Core (renova 1x num 401; `NeetruAuthRequiredError` sem sessão);
33
+ - `authFetch(path, init)` → API do produto (`productApiUrl`, só `https://` ou localhost) com Bearer + `x-correlation-id`, renova 1x num 401; **outra origem é recusada** (o token nunca vaza pra terceiros);
34
+ - `refreshTokens` **deduplicado** (o Core faz rotation com detecção de replay: dois refresh concorrentes derrubavam a sessão inteira).
35
+ - **`createNeetruClient({ endUserAuth })`** — provedor do token do usuário final **síncrono ou assíncrono** (`getIdToken()` + `refreshTokens()` opcional), usado pelo `support` (`X-Neetru-End-User-Token`) e pela barra de conta (`fetchNeetruAccountBar`). O `MobileAuthNamespace` encaixa direto; no backend do produto pode ser o id_token que o app mandou.
36
+ - **`x-correlation-id` em toda requisição** (`httpRequest` e gateway de IA): UUID por chamada (o do chamador — header ou opção `correlationId` — vence; id inseguro com CR/LF é trocado), o mesmo id nas retentativas, exposto em **`NeetruError.correlationId`** (novo campo; 6º argumento opcional do construtor). Sem `crypto` (RN sem polyfill) cai num gerador não-criptográfico — nunca derruba a chamada.
37
+ - Novos códigos em `NeetruErrorCode`: `server_only_key`, `token_invalid`, `insufficient_scope`.
38
+ - Exportados do barrel: `verifyAccessToken`, `authenticateProductRequest`, `accessTokenErrorToHttp`, `readBearerToken`, `readCookie`, `NEETRU_OIDC_ISSUER`, `detectNonServerRuntime` e os tipos (`VerifyAccessTokenOptions`, `VerifiedAccessToken`, `EndUserTokenProvider`, …).
39
+
40
+ ### Corrigido
41
+
42
+ - **Logout não revogava nada no servidor (web e mobile).** O `signOut` mandava `JSON { token: <id_token> }` pro `/api/v1/oauth/revoke`; o Core exige `application/x-www-form-urlencoded` (400) e id_token não é revogável — o **refresh_token de 30 dias seguia válido** depois do "sair". Agora revoga o **refresh_token** (e, no mobile, também o access_token) no formato do Core (RFC 7009, `token_type_hint` + `client_id` do client público). A sessão local é limpa sempre; se o servidor não confirmar, avisa no console (sem imprimir token).
43
+ - **`client.db` em React Native caía em memória em silêncio** (dados sumiam ao fechar o app). Agora é **fora de escopo com erro claro** (`runtime_unsupported`) em qualquer uso; `mocks.db` explícito continua valendo.
44
+
45
+ ### Mudança de comportamento (ler antes de atualizar)
46
+
47
+ - `@neetru/sdk/mobile` (entrypoint da 3.6.0, ainda sem app em produção): `apiKey` agora lança; sem `oidcClientId` lança (não cai mais no keyId da chave).
48
+ - Navegador com chave: aparece **um** `console.warn` por carregamento de página. Nada quebra.
49
+ - `signOut` web: com refresh_token salvo, faz 1 POST form-urlencoded ao `/revoke` (antes: JSON que dava 400). Sem refresh_token, não chama o servidor.
50
+
51
+ ### Migração dos produtos (PDV, GestoVendas, Zelo)
52
+
53
+ 1. Tirar a chave do bundle do navegador: remover `NEXT_PUBLIC_NEETRU_API_KEY` / `NEXT_PUBLIC_NEETRU_PUBLIC_KEY`; a chave fica só em `NEETRU_API_KEY` no servidor (injetada pelo Core/agente).
54
+ 2. Toda chamada que hoje o navegador faz com o SDK + chave (support, usage, db, ai…) passa por uma rota da API do produto (`/api/v1/...`) que usa `getServerNeetruClient()`; o id_token do usuário vai junto via `endUserAuth`.
55
+ 3. As rotas da API aceitam o app com `authenticateProductRequest(req, { audience: [web, ios, android], cookie })`.
56
+ 4. Ligar `strictServerOnlyKey: true` no client do navegador assim que (1) estiver feito — falha cedo se alguma chave voltar.
57
+ 5. `neetru deploy` (CLI ≥ 2.30.0) passa a **bloquear** o deploy se achar `nrt_…` no pacote público.
58
+
59
+ ### Interno
60
+
61
+ - **Gate de pre-push local** (padrão da skill `neetru-pre-push` — Actions da org sem billing, o gate local É o CI): `scripts/pre-push-gate.mjs` (template da loja, sem dependências) + `.githooks/pre-push` (+ `.gitattributes` forçando LF no hook — com `core.autocrlf=true` o shebang com CRLF quebraria no Windows). Etapas = as do `ci.yml`: `npm run lint` (`tsc --noEmit`) → `npm test` (→ `npm run build` com `NEETRU_PREPUSH_FULL=1`); push direto pra `main` bloqueado (`NEETRU_ALLOW_MAIN_PUSH=1` pra fuga consciente); `NEETRU_SKIP_PREPUSH=1` só em emergência, com motivo no PR. O `prepare` liga `core.hooksPath=.githooks` só quando existe `.git` (arquivo ou diretório — vale em worktree) e nunca falha: não afeta `npm publish` nem quem instala o pacote do registry (lá o `prepare` nem roda). Sem mudança no pacote publicado — não exige release.
62
+
63
+ ## [3.7.0] - 2026-09-29 — **feat(support): identidade do usuário final (`X-Neetru-End-User-Token`) nos tickets**
64
+
65
+ Release **minor** (capacidade nova; nenhuma API pública removida ou renomeada — ver "Mudança de comportamento" abaixo).
66
+
67
+ ### Contexto
68
+
69
+ Auditoria das APIs públicas do Core (2026-09-29, HIGH-2): a chave `nrt_` do produto identifica o **produto**, não a pessoa — e roda no browser. Os tickets de suporte eram "possuídos" pelo dono da chave, igual pra todos os usuários finais, e `GET /api/sdk/v1/support/tickets` listava os tickets de **todo mundo** do produto (screenshots, console logs, URLs). Contrato final do Core (Neetru/neetru-core#513):
70
+
71
+ - `X-Neetru-End-User-Token: <id_token>` (aceita também `Bearer <id_token>`), sempre junto do `Authorization` da chave do produto. **Só id_token**: token com `jti` (access token) → `401 invalid_end_user_token`.
72
+ - **Obrigatório** em `GET /api/sdk/v1/support/tickets` (sem header → `403 end_user_token_required`) e nas mutações `/{id}/contest|satisfaction|satisfaction-attempt|resolved-seen`.
73
+ - **Opcional** no `POST` de criação: com header o ticket fica amarrado ao `endUserId` (= `sub`); sem header nasce sem dono e **não aceita** contest/satisfaction.
74
+ - id_token de client OIDC de **outro produto** → `403 end_user_token_product_mismatch`.
75
+ - Token expirado / usuário inativo → `401 invalid_end_user_token`.
76
+ - A listagem não traz mais `attachments`/`consoleLogs`: `screenshotUrl` vem `null` na lista e `screenshotAttached` diz se há print. `limit` padrão 20, máximo 50.
77
+
78
+ **Dependência de deploy:** esta versão precisa estar publicada **junto/antes** do deploy do Core #513; senão a aba "Meus tickets" dos produtos (ex.: pdv) passa a receber 403.
79
+
80
+ ### Adicionado
81
+
82
+ - `client.support.*` envia **automaticamente** `X-Neetru-End-User-Token` com o **id_token** do usuário logado (`client.auth.getIdToken()`, lido a cada chamada — nunca o access token). Login feito depois de criar o client já vale; `getIdToken` que lança é tratado como "sem usuário".
83
+ - **Renovação automática 1x:** num `401 invalid_end_user_token`, o SDK chama `client.auth.refreshTokens()` (refresh_token rotation) UMA vez e repete a chamada com o id_token novo; se não renovar (ou falhar de novo), lança `NeetruAuthRequiredError`. Numa operação obrigatória sem id_token (expirado localmente), também tenta o refresh 1x antes de desistir.
84
+ - `AuthNamespace.refreshTokens?(): Promise<boolean>` — agora exposto pelo auth OIDC web (já existia internamente e no `@neetru/sdk/mobile`). Opcional no tipo: mocks/implementações próprias podem omitir.
85
+ - **`NeetruAuthRequiredError`** (exportado de `@neetru/sdk`, subclasse de `NeetruError`, `code: 'auth_required'`), com `reason`:
86
+ - `no_signed_in_user` — `listMyTickets`, `contestTicket`, `submitSatisfaction`, `recordSatisfactionAttempt` e `markResolvedSeen` **sem usuário logado** falham ANTES do request, com mensagem clara (em vez do 403 opaco do Core);
87
+ - `end_user_token_required` — o Core respondeu 403 exigindo a identidade;
88
+ - `invalid_end_user_token` — o Core recusou o token e o refresh não resolveu → refazer login.
89
+ - `403 end_user_token_product_mismatch` → `NeetruError` com `code: 'forbidden'` e `reason: 'end_user_token_product_mismatch'`, mensagem explicando que o login é de outro produto (config: `oidcClientId` × `apiKey`/`productId`). Não tenta refresh.
90
+ - `SupportTicket.screenshotAttached?: boolean` (da listagem do Core; fallback: há `screenshotUrl`). `listMyTickets({ limit })` — inteiro 1-50 (omitido = 20 do Core); fora disso `validation_failed` sem request.
91
+ - Novos códigos em `NeetruErrorCode`: `auth_required` e `gone`.
92
+ - **HTTP 410** (`statusToCode(410) === 'gone'`): mensagem útil ("endpoint removido do Core") + o substituto do header `X-Neetru-Replacement` — cobre o legado `/api/v1/sdk/entitlements/check`, que o Core agora responde 410. (O `client.entitlements.check()` do SDK já lançava localmente desde a 3.4.x; o 410 protege quem ainda chama a rota direto.)
93
+
94
+ ### Mudança de comportamento (ler antes de atualizar)
95
+
96
+ - **Mutações de ticket exigem usuário logado no SDK.** `contestTicket`/`submitSatisfaction`/`recordSatisfactionAttempt`/`markResolvedSeen` sem id_token lançam `NeetruAuthRequiredError` sem chamar o Core. Quem chamava isso **server-side só com a chave do produto** (sem usuário final) passa a receber o erro.
97
+ - **Tickets legados sem dono** (criados antes do corte de 2026-09-29, sem `endUserId`): o Core mantém a regra antiga pra eles (posse pela chave do produto) e ignora o header nesse caso — então **continuam mutáveis pelo SDK desde que haja QUALQUER usuário logado** no app (o id_token vai, o Core aplica a regra legada). Não há caminho no SDK pra mutá-los sem usuário logado.
98
+ - **Tickets criados a partir de 2026-09-29 sem header** (sem dono) não aceitam contest/satisfaction no Core — pra o usuário acompanhar o ticket, crie-o com o usuário logado.
99
+ - `listMyTickets()` passa a devolver `screenshotUrl: null` (a lista do Core não traz mais o anexo); use `screenshotAttached`.
100
+
101
+ ### Mantido (compatível)
102
+
103
+ - `createTicket` **sem** usuário logado continua funcionando, sem o header e sem tentar refresh (fluxo de report anônimo / server-side).
104
+ - Outros 403 (`forbidden_product`, `forbidden` de ticket de outro usuário) seguem como `NeetruError` comum.
105
+ - `MockSupport` (env `dev`) mantém o comportamento (ganha `screenshotAttached`).
106
+
107
+ ### Também entra nesta versão (mesclado no #34, ainda não publicado)
108
+
109
+ - Batch de quick-wins do Labs: bundle publicado minificado e sem sourcemap (~6x menor), `@deprecated` do namespace `entitlements` no index, e opt-in `deadlineMs` em `httpRequest` (orçamento total de tempo com retries).
110
+ - **Corrigido junto (regressão do #34, nunca publicada):** o `minify: true` apagava os magic comments `/* webpackIgnore: true */` dos `import("pg")`/`import("drizzle-orm/node-postgres")` dinâmicos → o webpack/Next do produto voltaria a tentar empacotar `pg` no bundle do browser (bug_0f836014; o teste `no-node-deps-in-browser-bundle` falhava 4/13 com `dist/` presente, inclusive na `main`). Agora minifica identificadores+sintaxe sem apagar whitespace/comentários: dist ~2,1 MB (vs 1,5 MB full; 8,5 MB antes do #34).
111
+
8
112
  ## [3.6.0] - 2026-09-24 — **feat(mobile): `@neetru/sdk/mobile` — namespace de auth OIDC+PKCE para React Native/Expo**
9
113
 
10
114
  Release **minor** (capacidade nova, aditiva — subpath novo, nenhuma API existente muda).
package/README.md CHANGED
@@ -50,6 +50,71 @@ if (!check.allowed && check.behavior === 'readonly') {
50
50
  }
51
51
  ```
52
52
 
53
+ > **A credencial `nrt_` é SÓ DE SERVIDOR (3.8.0).** O código acima roda no
54
+ > backend do produto. Site e app nunca carregam a chave: eles chamam a API do
55
+ > próprio produto com o token do usuário, e o backend usa o SDK. Ver
56
+ > [Apps e site sem chave no cliente](#apps-e-site-sem-chave-no-cliente-380).
57
+
58
+ ## Apps e site sem chave no cliente (3.8.0)
59
+
60
+ ```
61
+ app/site ──Authorization: Bearer <access_token>──▶ API do produto ──SDK + nrt_──▶ Core
62
+ ```
63
+
64
+ **Backend do produto** (aceita Bearer do app além do cookie do site):
65
+
66
+ ```ts
67
+ import { authenticateProductRequest, accessTokenErrorToHttp, getServerNeetruClient } from '@neetru/sdk';
68
+
69
+ export async function GET(req: Request) {
70
+ try {
71
+ const auth = await authenticateProductRequest(req, {
72
+ // client_id OIDC do MESMO produto: web + iOS + Android
73
+ audience: [process.env.NEETRU_OIDC_CLIENT_ID_WEB!, process.env.NEETRU_OIDC_CLIENT_ID_IOS!,
74
+ process.env.NEETRU_OIDC_CLIENT_ID_ANDROID!],
75
+ requiredScopes: ['openid'],
76
+ cookie: { name: 'session', verify: verifySessionCookie }, // o caminho que o site já tem
77
+ });
78
+ if (!auth) return Response.json({ error: 'unauthorized' }, { status: 401 });
79
+ const neetru = getServerNeetruClient({ apiKey: process.env.NEETRU_API_KEY }); // servidor
80
+ // ... regra de negócio em nome de auth.token.sub
81
+ } catch (err) {
82
+ const r = accessTokenErrorToHttp(err);
83
+ return Response.json(r.body, { status: r.status, headers: r.headers });
84
+ }
85
+ }
86
+ ```
87
+
88
+ `verifyAccessToken(token, { audience, requiredScopes })` valida assinatura RS256
89
+ (JWKS `auth.neetru.com/api/v1/oauth/.well-known/jwks`, cacheado), `iss`, `exp`,
90
+ `aud ∈ audience` e exige formato de access token (`jti` + `scope` + `client_id`)
91
+ — id_token não serve de credencial de API. Validação local: token revogado vale
92
+ até expirar (15 min no Core); o logout do app revoga o refresh_token.
93
+
94
+ **App React Native/Expo** (`@neetru/sdk/mobile` — recusa `apiKey` sempre):
95
+
96
+ ```ts
97
+ import { createMobileAuthNamespace } from '@neetru/sdk/mobile';
98
+
99
+ const auth = createMobileAuthNamespace({
100
+ oidcClientId: 'oidc-pdv-ios', // client OIDC PÚBLICO do app
101
+ productApiUrl: 'https://pdv.neetru.com/api/v1',
102
+ storage: secureStoreAdapter, // expo-secure-store
103
+ openAuthUrl: (url) => WebBrowser.openAuthSessionAsync(url, redirectUri).then(() => {}),
104
+ });
105
+ await auth.signIn({ redirectUri });
106
+ // ... deep link de volta:
107
+ await auth.completeSignIn(callbackUrl);
108
+ const res = await auth.authFetch('/vendas'); // Bearer access_token, renova 1x num 401
109
+ const me = await auth.getUserInfo(); // /userinfo do Core
110
+ await auth.signOut(); // limpa local + revoga refresh/access no Core
111
+ ```
112
+
113
+ `client.db` em React Native lança erro claro (dados de negócio vão pela API do
114
+ produto). Chave detectada em navegador/RN pelo `createNeetruClient`: aviso único
115
+ nesta minor; `strictServerOnlyKey: true` bloqueia já; **a próxima MAJOR bloqueia
116
+ sempre**.
117
+
53
118
  ## Princípios
54
119
 
55
120
  - **Vendor-neutral** — superfície pública NÃO vaza Firebase/Stripe/etc. Backend Neetru pode mudar no futuro sem reescrever produto.
@@ -57,7 +122,7 @@ if (!check.allowed && check.behavior === 'readonly') {
57
122
  - **Universal** — browser, Node ≥20, Edge runtimes (Vercel Edge, Cloudflare Workers). Usa `fetch` global + WebCrypto.
58
123
  - **Fail-closed segurança** — `baseUrl` allowlist (`*.neetru.com` + localhost) rejeita phishing/MITM. Bypass: `NEETRU_ALLOW_INSECURE_BASEURL=1`.
59
124
  - **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.
60
- - **Tipado** — erros via `NeetruError` com `.code`, `.status`, `.requestId`.
125
+ - **Tipado** — erros via `NeetruError` com `.code`, `.status`, `.requestId`, `.correlationId` (3.8.0: `x-correlation-id` gerado em toda requisição).
61
126
  - **Dev mode** — `NEETRU_ENV=dev` ativa mocks automáticos — zero rede em testes locais.
62
127
 
63
128
  ## Namespaces 3.0