@neetru/sdk 3.7.0 → 3.8.1
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 +75 -0
- package/README.md +66 -1
- package/dist/ai.cjs +221 -177
- package/dist/ai.d.cts +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.mjs +221 -177
- package/dist/auth.cjs +1051 -868
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.mjs +1048 -865
- package/dist/catalog.cjs +164 -97
- package/dist/catalog.d.cts +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.mjs +164 -97
- package/dist/checkout.cjs +182 -115
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/checkout.mjs +181 -114
- package/dist/db-react.d.cts +1 -1
- package/dist/db-react.d.ts +1 -1
- package/dist/db.cjs +437 -345
- package/dist/db.d.cts +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.mjs +455 -363
- package/dist/entitlements.cjs +17 -10
- package/dist/entitlements.d.cts +1 -1
- package/dist/entitlements.d.ts +1 -1
- package/dist/entitlements.mjs +17 -10
- package/dist/errors.cjs +15 -8
- package/dist/errors.d.cts +24 -3
- package/dist/errors.d.ts +24 -3
- package/dist/errors.mjs +14 -7
- package/dist/firestore-compat.d.cts +1 -1
- package/dist/firestore-compat.d.ts +1 -1
- package/dist/index.cjs +1544 -1190
- package/dist/index.d.cts +177 -3
- package/dist/index.d.ts +177 -3
- package/dist/index.mjs +1676 -1329
- package/dist/mobile.cjs +400 -181
- package/dist/mobile.d.cts +59 -4
- package/dist/mobile.d.ts +59 -4
- package/dist/mobile.mjs +400 -181
- package/dist/mocks.cjs +9 -2
- package/dist/mocks.d.cts +1 -1
- package/dist/mocks.d.ts +1 -1
- package/dist/mocks.mjs +9 -2
- package/dist/notifications.cjs +197 -130
- package/dist/notifications.d.cts +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.mjs +196 -129
- package/dist/react.cjs +303 -282
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs +295 -274
- package/dist/support.cjs +299 -229
- package/dist/support.d.cts +8 -4
- package/dist/support.d.ts +8 -4
- package/dist/support.mjs +298 -228
- package/dist/telemetry.cjs +234 -167
- package/dist/telemetry.d.cts +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.mjs +234 -167
- package/dist/{types-Dx0IrOSF.d.ts → types-BYPk_3HR.d.ts} +104 -17
- package/dist/{types-Bx4kTLmk.d.cts → types-yv3EDKg2.d.cts} +104 -17
- package/dist/usage.cjs +183 -116
- package/dist/usage.d.cts +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/usage.mjs +183 -116
- package/dist/webhooks.cjs +215 -148
- package/dist/webhooks.d.cts +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/dist/webhooks.mjs +212 -145
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,81 @@ 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.1] — 2026-10-11 (re-lease em falha de autenticação + doc de mTLS)
|
|
11
|
+
|
|
12
|
+
### Corrigido
|
|
13
|
+
|
|
14
|
+
- **`client.db.sql()` não se recuperava sozinho depois de um `rotate-credentials`** (bug_b7f5f2cff6d14a2aaa5d5c2068dc9dfd). A rotação (ALTER ROLE + userlist do PgBouncer) mantém vivas as conexões já abertas, mas toda conexão **nova** do pool, ainda com a senha do lease antigo, era recusada na autenticação até o próximo renew (~3 min). O re-lease reativo só cobria erro de rede/TLS, então os produtos recriavam o client na mão e vazavam pools. Agora falha de autenticação também dispara o re-lease: lease novo → pool novo → pool velho drenado com `end()` → a operação é repetida **uma vez**. Se a credencial nova também for recusada, o erro propaga (sem laço).
|
|
15
|
+
- Critério pelo SQLSTATE em `err.code`: `28P01` (senha inválida) e `28000` (autorização inválida). O PgBouncer devolve falha de SCRAM com o código genérico `08P01`; esse caso só conta com a mensagem `SASL authentication failed` / `password authentication failed` (`08P01` sozinho, como `query_wait_timeout`, **não** dispara).
|
|
16
|
+
- **Transações:** o `connect()` que abre a transação também ganha o re-lease + 1 nova tentativa, **só** em falha de autenticação (acontece no startup da conexão, antes do `BEGIN`; o corpo da transação nunca é repetido). Erro de rede/TLS no `connect()` segue sem retry, como antes.
|
|
17
|
+
- Só Postgres: o pool do `client.db.sql()` é `pg.Pool`; não há driver MySQL no SDK, então `ER_ACCESS_DENIED_ERROR` (1045) não se aplica.
|
|
18
|
+
- Sem API pública nova ou removida (`isAuthenticationError` é interno do módulo SQL).
|
|
19
|
+
|
|
20
|
+
### Documentação
|
|
21
|
+
|
|
22
|
+
- **A doc de `DbSqlLease` dizia que o mTLS de cliente "não está implementado no Core" e que `clientCert`/`clientKey` são "SEMPRE `null`". Isso é falso** (bug_e6e3af03f19c406994fe9d05a5f2617a). O texto veio do S2 de 2026-09-20 (`8d39b32`), baseado numa auditoria que já não batia com o código.
|
|
23
|
+
- O Core emite `sslca` + `clientCert` + `clientKey` em **todo** lease SQL (emissão e renovação), sem flag. O cert de cliente tem CN = `leaseId`, EKU `clientAuth` e 15 min de validade; a chave é EC P-256 efêmera. Se a CA não existir, o Core falha com 500 em vez de mandar `null`.
|
|
24
|
+
- O SDK já repassava os três ao `pg.Pool` (`ssl = { ca, cert, key, rejectUnauthorized: true }`). Agora há teste fixando isso nos dois factories.
|
|
25
|
+
- O PgBouncer da VM exige o cert de cliente (`client_tls_sslmode = verify-full` + `client_tls_ca_file` = CA do Core) **e** a senha SCRAM.
|
|
26
|
+
- Resultado: o mTLS está ativo de ponta a ponta. O `certificate expired` (alerta TLS 45) que a renovação e o re-lease curam é do cert de **cliente**, que expira com o lease. Não é o cert do servidor, que vale 365 dias. Comentários de `lease.ts`/`resilient-pool.ts`/`sql-client.ts` e o texto do `console.warn` de renovação falha foram corrigidos.
|
|
27
|
+
- `credentialVersion`: documentado que hoje é o placeholder fixo `1` do Core. A rotação é detectada pela troca de `password`, não por ele.
|
|
28
|
+
- Sem mudança de comportamento nem de tipos públicos (`clientCert`/`clientKey`/`sslca` seguem `string | null`, por tolerância a resposta sem o campo).
|
|
29
|
+
|
|
30
|
+
## [3.8.0] - 2026-10-03 — **feat(apps): chave `nrt_` só de servidor, `verifyAccessToken`, mobile integrado, correlation id**
|
|
31
|
+
|
|
32
|
+
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).
|
|
33
|
+
|
|
34
|
+
### Contexto (decisão do dono, 2026-10-03)
|
|
35
|
+
|
|
36
|
+
- 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.
|
|
37
|
+
- 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`).
|
|
38
|
+
- Corrige a premissa da 3.7.0 ("a chave `nrt_` … roda no browser"): isso deixa de ser aceito como desenho.
|
|
39
|
+
|
|
40
|
+
### Adicionado
|
|
41
|
+
|
|
42
|
+
- **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'`):
|
|
43
|
+
- padrão nesta minor: **aviso único** no console, em PT-BR, explicando o caminho certo — **nunca imprime a chave**;
|
|
44
|
+
- `strictServerOnlyKey: true` → falha **FECHADO** com `NeetruError('server_only_key')` (`reason`: `browser` | `react-native`);
|
|
45
|
+
- **na próxima MAJOR (4.0) a falha fechada vira o padrão.** Node/Edge (servidor) não são afetados.
|
|
46
|
+
- `missing_api_key` fora do servidor agora explica que a chamada deve ir pela API do produto.
|
|
47
|
+
- **`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).
|
|
48
|
+
- **`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`.
|
|
49
|
+
- **`@neetru/sdk/mobile` integrado:**
|
|
50
|
+
- **recusa `apiKey` SEMPRE** (`server_only_key`; no tipo, `apiKey?: never`) e exige `oidcClientId` (client OIDC público do app);
|
|
51
|
+
- guarda o **access_token** do `/token` e expõe `getAccessToken()` (renova 1x se vencido/a <30s);
|
|
52
|
+
- `getUserInfo()` no `/api/v1/oauth/userinfo` do Core (renova 1x num 401; `NeetruAuthRequiredError` sem sessão);
|
|
53
|
+
- `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);
|
|
54
|
+
- `refreshTokens` **deduplicado** (o Core faz rotation com detecção de replay: dois refresh concorrentes derrubavam a sessão inteira).
|
|
55
|
+
- **`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.
|
|
56
|
+
- **`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.
|
|
57
|
+
- Novos códigos em `NeetruErrorCode`: `server_only_key`, `token_invalid`, `insufficient_scope`.
|
|
58
|
+
- Exportados do barrel: `verifyAccessToken`, `authenticateProductRequest`, `accessTokenErrorToHttp`, `readBearerToken`, `readCookie`, `NEETRU_OIDC_ISSUER`, `detectNonServerRuntime` e os tipos (`VerifyAccessTokenOptions`, `VerifiedAccessToken`, `EndUserTokenProvider`, …).
|
|
59
|
+
|
|
60
|
+
### Corrigido
|
|
61
|
+
|
|
62
|
+
- **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).
|
|
63
|
+
- **`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.
|
|
64
|
+
|
|
65
|
+
### Mudança de comportamento (ler antes de atualizar)
|
|
66
|
+
|
|
67
|
+
- `@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).
|
|
68
|
+
- Navegador com chave: aparece **um** `console.warn` por carregamento de página. Nada quebra.
|
|
69
|
+
- `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.
|
|
70
|
+
|
|
71
|
+
### Migração dos produtos (PDV, GestoVendas, Zelo)
|
|
72
|
+
|
|
73
|
+
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).
|
|
74
|
+
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`.
|
|
75
|
+
3. As rotas da API aceitam o app com `authenticateProductRequest(req, { audience: [web, ios, android], cookie })`.
|
|
76
|
+
4. Ligar `strictServerOnlyKey: true` no client do navegador assim que (1) estiver feito — falha cedo se alguma chave voltar.
|
|
77
|
+
5. `neetru deploy` (CLI ≥ 2.30.0) passa a **bloquear** o deploy se achar `nrt_…` no pacote público.
|
|
78
|
+
|
|
79
|
+
### Interno
|
|
80
|
+
|
|
81
|
+
- **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.
|
|
82
|
+
|
|
8
83
|
## [3.7.0] - 2026-09-29 — **feat(support): identidade do usuário final (`X-Neetru-End-User-Token`) nos tickets**
|
|
9
84
|
|
|
10
85
|
Release **minor** (capacidade nova; nenhuma API pública removida ou renomeada — ver "Mudança de comportamento" abaixo).
|
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
|