@neetru/sdk 3.0.2 → 3.1.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +554 -530
  2. package/dist/auth.cjs +219 -54
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.d.cts +1 -2
  5. package/dist/auth.d.ts +1 -2
  6. package/dist/auth.mjs +219 -54
  7. package/dist/auth.mjs.map +1 -1
  8. package/dist/catalog.cjs +50 -2
  9. package/dist/catalog.cjs.map +1 -1
  10. package/dist/catalog.d.cts +1 -2
  11. package/dist/catalog.d.ts +1 -2
  12. package/dist/catalog.mjs +50 -2
  13. package/dist/catalog.mjs.map +1 -1
  14. package/dist/checkout.cjs +52 -3
  15. package/dist/checkout.cjs.map +1 -1
  16. package/dist/checkout.d.cts +1 -2
  17. package/dist/checkout.d.ts +1 -2
  18. package/dist/checkout.mjs +52 -3
  19. package/dist/checkout.mjs.map +1 -1
  20. package/dist/db-react.cjs +18 -6
  21. package/dist/db-react.cjs.map +1 -1
  22. package/dist/db-react.d.cts +35 -13
  23. package/dist/db-react.d.ts +35 -13
  24. package/dist/db-react.mjs +18 -6
  25. package/dist/db-react.mjs.map +1 -1
  26. package/dist/db.cjs +164 -56
  27. package/dist/db.cjs.map +1 -1
  28. package/dist/db.d.cts +1 -2
  29. package/dist/db.d.ts +1 -2
  30. package/dist/db.mjs +164 -56
  31. package/dist/db.mjs.map +1 -1
  32. package/dist/entitlements.cjs +50 -2
  33. package/dist/entitlements.cjs.map +1 -1
  34. package/dist/entitlements.d.cts +1 -2
  35. package/dist/entitlements.d.ts +1 -2
  36. package/dist/entitlements.mjs +50 -2
  37. package/dist/entitlements.mjs.map +1 -1
  38. package/dist/errors.cjs.map +1 -1
  39. package/dist/errors.d.cts +3 -1
  40. package/dist/errors.d.ts +3 -1
  41. package/dist/errors.mjs.map +1 -1
  42. package/dist/firestore-compat.cjs +411 -0
  43. package/dist/firestore-compat.cjs.map +1 -0
  44. package/dist/firestore-compat.d.cts +319 -0
  45. package/dist/firestore-compat.d.ts +319 -0
  46. package/dist/firestore-compat.mjs +383 -0
  47. package/dist/firestore-compat.mjs.map +1 -0
  48. package/dist/index.cjs +266 -66
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +9 -4
  51. package/dist/index.d.ts +9 -4
  52. package/dist/index.mjs +265 -67
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/mocks.cjs.map +1 -1
  55. package/dist/mocks.d.cts +1 -2
  56. package/dist/mocks.d.ts +1 -2
  57. package/dist/mocks.mjs.map +1 -1
  58. package/dist/notifications.cjs +50 -2
  59. package/dist/notifications.cjs.map +1 -1
  60. package/dist/notifications.d.cts +1 -2
  61. package/dist/notifications.d.ts +1 -2
  62. package/dist/notifications.mjs +50 -2
  63. package/dist/notifications.mjs.map +1 -1
  64. package/dist/react.cjs +268 -2
  65. package/dist/react.cjs.map +1 -1
  66. package/dist/react.d.cts +135 -3
  67. package/dist/react.d.ts +135 -3
  68. package/dist/react.mjs +266 -3
  69. package/dist/react.mjs.map +1 -1
  70. package/dist/support.cjs +50 -2
  71. package/dist/support.cjs.map +1 -1
  72. package/dist/support.d.cts +1 -2
  73. package/dist/support.d.ts +1 -2
  74. package/dist/support.mjs +50 -2
  75. package/dist/support.mjs.map +1 -1
  76. package/dist/telemetry.cjs +65 -4
  77. package/dist/telemetry.cjs.map +1 -1
  78. package/dist/telemetry.d.cts +1 -2
  79. package/dist/telemetry.d.ts +1 -2
  80. package/dist/telemetry.mjs +65 -4
  81. package/dist/telemetry.mjs.map +1 -1
  82. package/dist/{types-D7zVkO3n.d.ts → types-B3XFHD4A.d.ts} +640 -2
  83. package/dist/{types-Dc4bjrrt.d.cts → types-DePdSU3P.d.cts} +640 -2
  84. package/dist/usage.cjs +50 -2
  85. package/dist/usage.cjs.map +1 -1
  86. package/dist/usage.d.cts +1 -2
  87. package/dist/usage.d.ts +1 -2
  88. package/dist/usage.mjs +50 -2
  89. package/dist/usage.mjs.map +1 -1
  90. package/dist/webhooks.cjs +83 -2
  91. package/dist/webhooks.cjs.map +1 -1
  92. package/dist/webhooks.d.cts +1 -2
  93. package/dist/webhooks.d.ts +1 -2
  94. package/dist/webhooks.mjs +83 -3
  95. package/dist/webhooks.mjs.map +1 -1
  96. package/package.json +156 -151
  97. package/dist/collection-ref-BDdfD87k.d.cts +0 -581
  98. package/dist/collection-ref-BDdfD87k.d.ts +0 -581
package/CHANGELOG.md CHANGED
@@ -1,530 +1,554 @@
1
- # Changelog
2
-
3
- All notable changes to `@neetru/sdk` will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [3.0.2] - 2026-06-02 — **fix: sentinels de escrita server-side (serverTimestamp + increment) — bug_05aedb8b**
9
-
10
- ### Fixed / Added
11
-
12
- Fecha 3 lacunas do DB de documentos (`client.db.collection()`):
13
-
14
- - **`serverTimestamp()`** — sentinel de timestamp resolvido pelo **servidor**. Antes, `createdAt`/`updatedAt` de produto usavam o relógio do CLIENTE (spoofável + clock-skew). Agora:
15
-
16
- ```ts
17
- import { serverTimestamp } from '@neetru/sdk/db';
18
- await orders.add({ total: 99, createdAt: serverTimestamp() });
19
- ```
20
-
21
- O marcador atravessa o transporte como `{ "__neetru__": "serverTimestamp" }` e o Core o resolve em `FieldValue.serverTimestamp()` no momento da escrita.
22
-
23
- - **`increment(n)`** incremento **atômico** de campo numérico (antes `update()` era merge/replace, sujeito a lost-update sob concorrência):
24
-
25
- ```ts
26
- import { increment } from '@neetru/sdk/db';
27
- await counters.update('global', { hits: increment(1) });
28
- await wallet.update(uid, { balance: increment(-50) });
29
- ```
30
-
31
- Resolvido server-side via `FieldValue.increment(n)`. Aplicação otimista local: `serverTimestamp()` → `Date.now()` aproximado; `increment(n)` → soma local sobre o valor atual (valor canônico vem do servidor no sync).
32
-
33
- - **Tipos alinhados ao runtime** — `add`/`set`/`update`/`batch` e `DbDocRef.set/update` agora aceitam `WithFieldSentinels<T>`: cada campo pode receber seu tipo OU um sentinel, sem erro de tipo. O `.d.ts` é gerado da mesma fonte do runtime (tsup), eliminando divergência declaração↔runtime.
34
-
35
- ### Novos exports
36
-
37
- - De `@neetru/sdk` e `@neetru/sdk/db`: `serverTimestamp`, `increment`, `isFieldSentinel`, `isServerTimestampSentinel`, `isIncrementSentinel`, `NEETRU_SENTINEL_KEY`.
38
- - Tipos: `FieldSentinel`, `ServerTimestampSentinel`, `IncrementSentinel`, `WithFieldSentinels`.
39
-
40
- ### Segurança
41
-
42
- - Sentinels só são resolvidos em campos **top-level** (espelha o Core). Marcador desconhecido, `operand` não-numérico, ou sentinel aninhado → o Core rejeita com `400 validation_failed` (fail-closed) e **nunca grava** o objeto cru.
43
-
44
- > Requer Core com o resolver de sentinels (`src/lib/datastore-sentinels.ts`) nos endpoints `/api/sdk/v1/datastore/*`. Ambos os repos mudam neste fix.
45
-
46
- ## [3.0.1] - 2026-05-29 — **fix: sincronizar VERSION + docstrings + contract-staging códigos de erro**
47
-
48
- ### Fixed
49
-
50
- - `src/index.ts` — `VERSION` constante sincronizada com `package.json` (`'3.0.0'` → `'3.0.1'`). Consumers que fazem `import { VERSION } from '@neetru/sdk'` recebiam `'3.0.0'` enquanto `npm list` mostrava `3.0.1` (LOW-NEW-SDK-01).
51
- - `src/telemetry.ts` JSDoc na linha 6 corrigido: path legacy `/api/v1/sdk/telemetry/event` → canônico `/api/sdk/v1/telemetry/event`. O código sempre usou o path correto; apenas a documentação estava stale (LOW-NEW-SDK-02).
52
- - `src/__tests__/telemetry.test.ts` comentário de cabeçalho atualizado para não dizer "(legado)" (LOW-NEW-SDK-02).
53
- - `src/__tests__/contract-staging.test.ts` — `'http_error'` removido de todos os arrays `toContain`. `NeetruErrorCode` não inclui `'http_error'`; respostas HTTP não mapeáveis caem em `'unknown'`. Substituído por `'unknown'` em todos os 7 sites (LOW-NEW-SDK-03).
54
-
55
- ### Nota
56
-
57
- Este patch corresponde ao commit `4c40f01` (inline realtime-protocol types) que não atualizou `VERSION` nem o CHANGELOG.
58
-
59
- ## [3.0.0] - 2026-05-27 — **MAJOR: contract hardening + Universal runtime + paths canonical**
60
-
61
- > Major rewrite resolvendo 5 CRITICAL + 6 HIGH do review `CHEFE_SDK_REVIEW_2026-05-27`.
62
- > SDK 2.3.6 estava com **4 dos 11 namespaces broken** contra Core LIVE (`usage`, `webhooks`, `notifications`, `support`) — 13 endpoints batendo 404/400. Tests passavam validando contrato errado (mock fetch aceita qualquer URL).
63
- >
64
- > **Justificativa do MAJOR**: paths/body shapes mudaram em 4 namespaces; 2 métodos removidos (`usage.track`, `usage.getQuota`); `verifyWebhookSignature` virou async. Toda CLI release/SaaS inaugural depende disso primeiro.
65
-
66
- ### BREAKING CHANGES
67
-
68
- #### 1. `usage` namespace 100% rewrite (CRITICAL-SDK-01)
69
-
70
- - **REMOVIDO** `usage.track(event, props)`Core não aceitava esse shape; era vaporware testado contra mock. Use `usage.report(resource, qty, opts)`.
71
- - **REMOVIDO** `usage.getQuota(metric)` — endpoint `/usage/quota` nunca existiu no Core. `usage.report()` devolve `{value, limit, remaining}` inline em todo POST.
72
- - Paths corrigidos:
73
- - `/sdk/v1/usage/record` → `/api/sdk/v1/usage/record` (canonical)
74
- - `/sdk/v1/entitlements` → `/api/sdk/v1/entitlements` (canonical)
75
- - `usage.report()` agora envia `Idempotency-Key: <uuid v4>` header automaticamente.
76
- - `usage.check()` retorna novo field `behavior: "readonly" | null` (inferido de `reason === "limit_exceeded"`).
77
-
78
- #### 2. `webhooks` namespace — `productId` obrigatório (CRITICAL-SDK-03)
79
-
80
- - `RegisterWebhookInput.productId` adicionado. Default resolve do `config.productId`; lança `validation_failed` se ambos ausentes.
81
- - `list(options?)`, `unregister(id, options?)`, `test(id, options?)` agora aceitam `{ productId }` (Core exige query param em todos os verbos).
82
- - `webhooks.test()` envia `Idempotency-Key` header.
83
-
84
- #### 3. `notifications` namespace — `productId` obrigatório (CRITICAL-SDK-04)
85
-
86
- - `SendNotificationInput.productId` adicionado (Core valida via Zod `SendNotificationSchema`).
87
- - `ListNotificationsOptions.productId` adicionado.
88
- - `NotificationScopeOptions` novo type para `markRead`/`dismiss`.
89
- - `notifications.send()` envia `Idempotency-Key` header.
90
-
91
- #### 4. `support` namespace — endpoint canonical (CRITICAL-SDK-02)
92
-
93
- - Endpoint mudou de `/api/v1/products/{slug}/tickets` (vaporware — **nunca existiu no Core**) para `/api/sdk/v1/support/tickets` (canonical SDK path).
94
- - `productId` obrigatório (default `config.productId`).
95
- - `createTicket()` envia `Idempotency-Key` header.
96
- - `listMyTickets(options?)` aceita `{ productSlug }` override.
97
- - **Pré-requisito Core**: endpoint `/api/sdk/v1/support/tickets` precisa ser construído (vide `docs/CORE_CHANGES_REQUIRED.md` §1).
98
-
99
- #### 5. `verifyWebhookSignature` é async + Universal (HIGH-SDK-03)
100
-
101
- - **Antes (2.x)**: `(payload, sig, secret) => boolean` — usava `require('node:crypto')` que quebrava em bundle browser/Edge ESM puro. README anunciava "Universal" mas mentia.
102
- - **Agora (3.0)**: `(payload, sig, secret) => Promise<boolean>` via `WebCrypto.subtle.sign` HMAC-SHA-256 + constant-time XOR compare.
103
- - Funciona em Node ≥20, browsers modernos, Vercel Edge Functions, Cloudflare Workers.
104
- - Lança `NeetruError('runtime_unsupported')` se WebCrypto ausente (distingue "ambiente não suporta" de "assinatura inválida").
105
-
106
- #### 6. `telemetry.event` / `telemetry.track` path canonical (CRITICAL-SDK-05)
107
-
108
- - `POST /api/v1/sdk/telemetry/event` `POST /api/sdk/v1/telemetry/event` (canonical SDK pattern).
109
- - Pré-requisito Core: alias/handler para o canonical path (vide `docs/CORE_CHANGES_REQUIRED.md` §3).
110
-
111
- #### 7. `baseUrl` allowlist fail-closed (HIGH-SDK-05)
112
-
113
- - `createNeetruClient` agora chama `validateBaseUrl` antes de aceitar `config.baseUrl`.
114
- - Allowlist padrão: HTTPS `*.neetru.com` + `localhost`/`127.0.0.1`/`*.test`/`*.localhost`.
115
- - `NeetruClientConfig.allowedHosts: string[]` novoadicione hosts custom (suffix match).
116
- - Bypass: `NEETRU_ALLOW_INSECURE_BASEURL=1` (env) ou `globalThis.NEETRU_ALLOW_INSECURE_BASEURL='1'`. Warning no console quando ativo.
117
- - Cenário ataque fechado: dev configura `NEETRU_BASE_URL=http://attacker.example/` no `.env`, agora lança `invalid_config` em vez de vazar Bearer.
118
-
119
- #### 8. Retries default por método (HIGH-SDK-01)
120
-
121
- - **GET/HEAD**: 2 retries default (= 3 tentativas) — idempotente, seguro.
122
- - **POST/PUT/PATCH/DELETE**: **0 retries default** — mutations podem duplicar side effects (ex: `FieldValue.increment` no `usage/record`).
123
- - Caller pode override: `httpRequest(config, { method: 'POST', ..., retries: 2 })`.
124
- - Combinado com `Idempotency-Key` automático em mutations: dup-write rate → zero.
125
-
126
- #### 9. Cleanup de stubs deprecated
127
-
128
- - `initNeetru` (deprecated desde v0.2, no-op em v2.0) **definitivamente removido**.
129
- - `NeetruConfig` type alias **removido** (use `NeetruClientConfig`).
130
-
131
- #### 10. `NeetruErrorCode` ganhou `runtime_unsupported`
132
-
133
- - Lançado por `verifyWebhookSignature` quando WebCrypto ausente. Não é breaking se você usa só `instanceof NeetruError`; é breaking se você fez `switch` no union type (TS error em case missing).
134
-
135
- ### ADDED
136
-
137
- - `src/idempotency.ts` `generateIdempotencyKey()` via `crypto.randomUUID()` (Node ≥20, browsers, Edge).
138
- - `src/base-url-allowlist.ts` `validateBaseUrl(url, allowedHosts)` fail-closed.
139
- - `HttpRequestOptions.idempotencyKey: boolean | string` — injeta header automaticamente em todo POST de mutation.
140
- - `client.usage.check().behavior: 'readonly' | null` sinal para `<EntitlementGate mode='readonly'>` decidir UI.
141
- - `NeetruClientConfig.allowedHosts` extension point para hosts custom (staging/proxies).
142
- - `MockUsage.__getReports()` substitui `__getRecords()` (legacy alias mantido).
143
- - Test suite: `src/__tests__/base-url-allowlist.test.ts` (16 tests), `src/__tests__/idempotency.test.ts` (9 tests).
144
- - `docs/CORE_CHANGES_REQUIRED.md` — catálogo de 5 endpoints Core a criar/modificar para 3.0 funcionar end-to-end.
145
-
146
- ### FIXED
147
-
148
- - `<EntitlementGate>` v1.1 não morre mais em silent `allowed:false` paths corrigidos no `usage.check` (era dependência transitiva, fixed via CRITICAL-SDK-01).
149
- - README v2.x prometia "Universal: browser/Node/Edge" mas `verifyWebhookSignature` só funcionava em Node. 3.0 cumpre.
150
-
151
- ### Migration guide v2.x → v3.0
152
-
153
- ```ts
154
- // ❌ Antes (v2.x)
155
- await client.usage.track('api_call', { route: '/x' });
156
- const quota = await client.usage.getQuota('api_calls');
157
-
158
- await client.webhooks.register({
159
- url: 'https://x.com/h',
160
- events: ['subscription.activated'],
161
- });
162
-
163
- await client.notifications.send({
164
- userId: 'u1',
165
- kind: 'order.received',
166
- title: 'Pedido',
167
- });
168
-
169
- await client.support.createTicket({
170
- subject: 'help',
171
- message: 'x',
172
- });
173
-
174
- // ✅ Depois (v3.0)
175
- const client = createNeetruClient({
176
- apiKey: 'nrt_...',
177
- productId: 'gestovendas', // 🆕 default p/ todos os namespaces
178
- tenantId: 't-acme', // 🆕 default p/ usage
179
- });
180
-
181
- const result = await client.usage.report('api_call', 1);
182
- // result.value, result.limit, result.remaining inline — não precisa getQuota separado.
183
-
184
- const allowed = await client.usage.check('api_call');
185
- // allowed.behavior === 'readonly' quando over limit (use em <EntitlementGate>)
186
-
187
- await client.webhooks.register({
188
- // productId default config.productId, ou explícito:
189
- url: 'https://x.com/h',
190
- events: ['subscription.activated'],
191
- });
192
-
193
- await client.notifications.send({
194
- userId: 'u1',
195
- kind: 'order.received',
196
- title: 'Pedido',
197
- });
198
-
199
- await client.support.createTicket({
200
- subject: 'help',
201
- message: 'x',
202
- });
203
- ```
204
-
205
- ### Verificação de assinatura de webhook — agora async
206
-
207
- ```ts
208
- // Antes (v2.x) — sync, quebrava em Edge
209
- import { verifyWebhookSignature } from '@neetru/sdk/webhooks';
210
- const ok = verifyWebhookSignature(rawPayload, sigHeader, secret);
211
-
212
- // Depois (v3.0) async, Universal
213
- const ok = await verifyWebhookSignature(rawPayload, sigHeader, secret);
214
- ```
215
-
216
- ### Endpoints Core que precisam ser criados/modificados antes do publish 3.0
217
-
218
- Vide `docs/CORE_CHANGES_REQUIRED.md` completo:
219
-
220
- 1. **CRITICAL** — `POST/GET /api/sdk/v1/support/tickets` (customer-facing) — 3-4h.
221
- 2. **HIGH** — Idempotency-Key dedup em endpoints de mutation — 4-6h.
222
- 3. **HIGH** — `POST /api/sdk/v1/telemetry/event` (canonical) — 1-2h ou alias middleware.
223
- 4. **LOW** — `behavior` field em `/api/sdk/v1/entitlements` response — 30min.
224
- 5. **LOW** — Verificar envelope `/api/sdk/v1/datastore/{collection}` — 30min check.
225
-
226
- ### Tests
227
-
228
- - **719 tests passing** (era 668 em 2.3.6). Tsc clean. Build clean.
229
- - Contract tests reescritos para validar paths canonical + body shapes + headers (Idempotency-Key UUID v4 regex match).
230
-
231
- ## [2.3.6] - 2026-05-26 — **fix(bundle): webpackIgnore em TODOS os dynamic imports node-only — bug_168368ae [CRITICAL]**
232
-
233
- ### Corrigido
234
-
235
- - **bug_168368aedb664edba1ca1f1945b568ac (CRITICAL) — fix 2.3.5 foi parcial: `webpackIgnore` aplicado só em `createSqlClientFromConfig`, mas `createPgPool` + `createDrizzleHandle` (helpers separados no mesmo arquivo) + os 3 dynamic imports em `MockDb.sql()` (`pg-mem`, `drizzle-orm/node-postgres`, `@electric-sql/pglite`, `drizzle-orm/pglite`) ainda estavam sem o magic comment.**
236
- Resultado: webpack do consumer Next.js seguia o module graph daqueles paths e ainda tentava bundle `pg` → `Module not found: tls/fs` no client.
237
- Fix 2.3.6: `/* webpackIgnore: true */` em **todos** os 7 spots de `await import('pg' | 'drizzle-orm/node-postgres' | 'pg-mem' | '@electric-sql/pglite' | 'drizzle-orm/pglite')` em `sql-client.ts` + `mocks.ts`.
238
-
239
- ### Testes (antirregressão atualizado)
240
-
241
- - **`no-node-deps-in-browser-bundle.regression.test.ts`** expandido: agora pega `import("X")` dynamic SEM `webpackIgnore` (regex negative lookahead). Antes só cobria `from "X"` static. Adicionado 4 patterns: pg, drizzle-orm/node-postgres, pg-mem, @electric-sql/pglite.
242
-
243
- ## [2.3.5] - 2026-05-26 — **fix(bundle): node deps fora do browser entry — bug_0f836014 [CRITICAL]**
244
-
245
- ### Corrigido
246
-
247
- - **bug_0f836014055747858adfa8ab0e8cbb47 (CRITICAL) SDK 2.3.4 publicou com `require('pg')` + `require('drizzle-orm/node-postgres')` literais no `dist/index.mjs`.**
248
- Webpack do consumer Next.js seguia o module graph e tentava bundle pg → puxa fs → `Module not found: Can't resolve 'fs'` no client bundle. Bloqueava `npm run build` de qualquer projeto Next.js que importa `createNeetruClient`.
249
- Fix: dynamic import com `/* webpackIgnore: true */` em `createSqlClientFromConfig`. Instrui webpack a NÃO processar — runtime Node resolve. `sql()` é server-only por contrato.
250
- Resultado: `dist/index.mjs` zero imports literais de pg/drizzle/fs/node:*. Verified via teste antirregressão novo.
251
-
252
- ### Testes (antirregressão)
253
-
254
- - **`src/__tests__/regressions/no-node-deps-in-browser-bundle.regression.test.ts`** (novo) — varre 13 entry bundles e falha se encontrar require/import estático de pg/drizzle-orm/node-postgres/pg-mem/@electric-sql/pglite/fs/node:*. Roda APÓS `npm run build`. Pega regressão dessa classe.
255
-
256
- ## [2.3.4] - 2026-05-26 — **fix(mocks): MockDb.sql() migra pra pglite + pg-mem fallback — bug_6bae8674 [HIGH]**
257
-
258
- ### Corrigido
259
-
260
- - **bug_6bae8674461f4600809e2f9de13c6043 (HIGH) fix anterior (2.3.3) teve efeito INVERSO em `MockDb.sql()`.**
261
- O fix de 2.3.3 injetava `getTypeParser` em `pool.types` para resolver o erro "Not supported: getTypeParser is not supported".
262
- Porém `pg-mem` tem um guard interno: `if (query.types?.getTypeParser) throw NotSupported`.
263
- `drizzle-orm/node-postgres/session.js:113` passa `rawQuery.types = pool.types` em TODA query — ao adicionar
264
- `getTypeParser` ao pool, drizzle passou-o para o pg-mem que o rejeitou imediatamente.
265
- Efeito líquido: qualquer `INSERT`/`SELECT`/`UPDATE` continuava quebrando com o mesmo erro.
266
-
267
- **Fix 2.3.4duas soluções em cascata:**
268
-
269
- 1. **Solução A (preferida): `@electric-sql/pglite` + `drizzle-orm/pglite`** — WASM-Postgres nativo
270
- sem adapter pg-mem. Sem conflito `getTypeParser`, sem monkey-patch. `PGlite.create()` retorna
271
- instância pronta, DDL via `pglite.query(ddl)`, ORM via `drizzle(pglite, { schema })`. Cast
272
- `PgliteDatabaseNodePgDatabase` via `unknown` (interfaces Drizzle compatíveis em superfície
273
- mas com tipos de resultado internos distintos).
274
-
275
- 2. **Solução B (fallback pg-mem)**: strip interceptor — intercepta `pool.query` e deleta
276
- `types.getTypeParser` do objeto de query ANTES do pg-mem ver. Guard não dispara, query passa
277
- limpa. Compatível com consumers que só têm pg-mem nas devDeps.
278
-
279
- **`@electric-sql/pglite` adicionado como `peerDependency` optional** (`^0.4.0`) + `devDependency`
280
- interno (`^0.4.6`).
281
-
282
- **8 novos testes** em `src/__tests__/mocks-sql-pglite.test.ts`: criação sem erro, INSERT sem throw,
283
- SELECT após INSERT, UPDATE, múltiplos INSERTs, sem initSql, close idempotente, transaction().
284
- Todos passando.
285
-
286
- **655 testes totais** (era 647 em 2.3.3). `tsc --noEmit` clean. Build clean.
287
-
288
- ## [2.3.3] - 2026-05-26 **fix(mocks): pool.types.getTypeParser identity parser bug_44bcc99f [HIGH]**
289
-
290
- ### Corrigido
291
-
292
- - **bug_44bcc99f9d3140d78abd8f7810d1afa4 (HIGH) — `MockDb.sql()` queries Drizzle falhavam em runtime.**
293
- Followup do `bug_a7f2ae25` (SDK 2.3.2): `sql()` retornava handle Drizzle, mas qualquer query (`INSERT`, `SELECT`, etc) jogava `Error: Not supported: getTypeParser is not supported`. `pg-mem` não implementa `pool.types.getTypeParser(oid)` que `drizzle-orm/node-postgres/session.js` chama em TODA query.
294
- Fix: monkey-patch identity parser no Pool retornado por `pg-mem.adapters.createPg()` antes de passar pro Drizzle. Types retornam o valor sem transformação (suficiente pra tests in-memory; produção usa `pg` real com parsers nativos).
295
- 3 linhas. Non-breaking. Tests E2E Drizzle in-memory destravados.
296
-
297
- ## [2.3.2] - 2026-05-26 **fix(mocks): MockDb.sql() implementado in-memory via pg-mem — bug_a7f2ae25 [HIGH]**
298
-
299
- ### Corrigido
300
-
301
- - **bug_a7f2ae25fda54129b52b6dd55aaa6050 (HIGH) `MockDb.sql()` lançava `db_unavailable` em vez de implementar Drizzle in-memory.**
302
- Bloqueava vitest server-side dos consumers (pdv-agiliza Fase 4 migration). Política do projeto proíbe workaround instalar `pg-mem` no consumer.
303
- Fix: `MockDb.sql(schema, options)` agora carrega `pg-mem` lazy + cria `NodePgDatabase<TSchema>` Drizzle apontando pra DB in-memory. Suporta `options.initSql: string | string[]` pra rodar DDL no setup (CREATE TABLE etc).
304
- `pg-mem` adicionado como `peerDependency optional` (com `peerDependenciesMeta.optional=true`) + `devDependency` interno. Consumers que querem mock SQL: `npm install -D pg-mem drizzle-orm pg`.
305
- **Não-breaking:** signature `sql(schema, options?)` mantida. SqlOptions ganhou `initSql?` opcional (ignorado por lease real em produção). Comportamento default — `sql()` sem `initSql` — funciona em ambos modos.
306
-
307
- ## [2.3.1] - 2026-05-26 **fix(deps): drizzle-orm + pg como peerDependencies — bug_42ae9f80 [CRITICAL]**
308
-
309
- ### Corrigido
310
-
311
- - **bug_42ae9f80b2884bd583c5246771f7730c (CRITICAL) — `drizzle-orm` em `dependencies` causava type collision com versão do consumer.**
312
- Quando o projeto consumer instalava `drizzle-orm` direto e tentava `client.db.sql(schema)`, o TypeScript via 2 cópias diferentes (uma nested em `node_modules/@neetru/sdk/node_modules/drizzle-orm`, outra no consumer) e reclamava: `'Property config is protected but type Column<...> is not a class derived from Column<...>'` + `'shouldInlineParams private'`. Bloqueava Fases 2-8 da migração pdv-agiliza pra `vm-postgres-single`.
313
- Fix: movidas `drizzle-orm` e `pg` de `dependencies` pra `peerDependencies` (com `peerDependenciesMeta.optional: true`). Consumer instala 1 versão, todos os tipos convergem. Devkit mantém `drizzle-orm` + `pg` em `devDependencies` pra build/test internos.
314
- **Breaking expected on consumer side:** se o projeto consumer NÃO tinha `drizzle-orm` em deps mas usava `client.db.sql()`, agora precisa `npm install drizzle-orm pg`. Consumers que só usam transport REST/Firestore não são afetados (peerDeps optional).
315
-
316
- ## [2.3.0] - 2026-05-25 **feat(db-config): renomeia engine→transport com deprecation — bug_ed9da7fd [HIGH]**
317
-
318
- Fix do bug `bug_ed9da7fd11194eac87f0eb9c75e5fad9` (HIGH) colisão de terminologia: `engine` no SDK
319
- configurava o transporte SDK↔Core (REST/Firestore/WebSocket), mas `engine` no admin database (CLI
320
- `neetru admin database create --engine=...`) configura o storage engine (firestore-instance,
321
- cloud-sql-postgres, vm-postgres-single…). Devs colocavam `firestore-instance` no SDK e quebravam.
322
-
323
- ### Added
324
-
325
- - **`NeetruDbTransport`**novo tipo público canônico para o campo de transporte SDK↔Core.
326
- Valores: `'rest' | 'firestore' | 'nosql-vm'` (idênticos aos de `NeetruDbEngine`).
327
-
328
- - **`NeetruDbOptions.transport`** novo campo canônico que substitui `engine`.
329
- ```ts
330
- // v2.3+ (preferido):
331
- createNeetruClient({ db: { transport: 'rest' } });
332
- createNeetruClient({ db: { transport: 'firestore' } });
333
- createNeetruClient({ db: { transport: 'nosql-vm', realtimeGatewayUrl: '...' } });
334
- ```
335
-
336
- ### Deprecated
337
-
338
- - **`NeetruDbOptions.engine`** depreciado em v2.3, será removido em v3.0.
339
- Continua funcionando: se `transport` estiver ausente e `engine` presente, o valor de `engine`
340
- é usado como transport com `console.warn` de deprecação.
341
-
342
- ```ts
343
- // v2.x (depreciado emite console.warn):
344
- createNeetruClient({ db: { engine: 'firestore' } });
345
- // [neetru/sdk] NeetruDbOptions.engine está depreciado...
346
- ```
347
-
348
- - **`NeetruDbEngine`** — depreciado em v2.3, será removido em v3.0.
349
- Agora é alias de `NeetruDbTransport`. Use `NeetruDbTransport` em código novo.
350
-
351
- ### Behavior
352
-
353
- - `transport` e `engine` podem coexistir: `transport` sempre prevalece, sem warn.
354
- - Sem `transport` nem `engine`: default `'rest'`, sem warn (comportamento idêntico ao v2.x).
355
- - A relação 1:N entre storage engines e transports é documentada no JSDoc de `NeetruDbTransport`:
356
- qualquer storage engine (cloud-sql-postgres, vm-postgres-single…) pode ser acessado com
357
- `transport: 'rest'`; `transport: 'firestore'` e `transport: 'nosql-vm'` são para bancos
358
- documentais com realtime nativo.
359
-
360
- ### Tests
361
-
362
- - 5 testes novos em `src/__tests__/db-real.test.ts`:
363
- `transport=firestore`, `transport=nosql-vm`, `transport=rest`, colisão `transport+engine`
364
- (transport prevalece, sem warn), default sem campos (sem warn).
365
- - Testes de regressão: `engine=firestore` e `engine=nosql-vm` legados ainda passam + emitem warn.
366
-
367
- ## [2.2.1] - 2026-05-25 **fix(offline-docs): fallback MemoryStore em Node — bug_ba287a CRITICAL**
368
-
369
- Fix do bug `bug_ba287aefdf6c4327b026d588a239ad06` (CRITICAL) — `client.db.collection().set/add/update`
370
- lançava `ReferenceError: indexedDB is not defined` em Node.js (reportado por pdv-agiliza prod).
371
-
372
- ### Fixed
373
-
374
- - **`createOfflineDocumentsNamespace` — auto-detect Node runtime e usa `MemoryStore`**.
375
- A função agora verifica `typeof indexedDB === 'undefined' || typeof window === 'undefined'`
376
- antes de instanciar `LocalStore` (que depende de `idb` / IndexedDB). Em Node/Bun/Edge Workers
377
- sem IDB, cai automaticamente em `MemoryStore` implementação in-process com a mesma API,
378
- sem nenhuma flag explícita necessária. Ops de documentos (`set/add/update/remove/get/list`,
379
- `batch`, `onSnapshot`, `onDoc`) funcionam normalmente em Node; dados não persistem entre
380
- reinicializações do processo (comportamento esperado em SSR/server-side).
381
-
382
- ### Added
383
-
384
- - **`src/db/offline/memory-store.ts`** — `MemoryStore`: implementação in-memory do contrato
385
- público de `LocalStore`. Cobre todas as 5 stores (documents, mutations, query\_cache,
386
- sync\_meta, conflict\_log) com Maps/Arrays em memória.
387
-
388
- ### Tests
389
-
390
- - **`src/__tests__/offline-docs-node-compat.test.ts`** — 5 casos de regressão:
391
- verifica que a stack offline não lança `ReferenceError` em ambiente sem IDB,
392
- e que `set/add/update` + `get` roundtrips funcionam in-memory.
393
-
394
- ## [2.2.0] - 2026-05-25 **auth.verifyToken validação server-side de id_token via JWKS [feature]**
395
-
396
- Fix do bug `bug_a7ef1702680044e3994b74bdccecb2ab` — resolve P0-1 audit security pdv-agiliza.
397
-
398
- ### Added
399
-
400
- - **`client.auth.verifyToken(token, options?): Promise<NeetruUser>`** valida um id_token JWT
401
- emitido por `auth.neetru.com` server-side, sem round-trip de rede por request. Usa JWKS local
402
- cacheado (default TTL 1h, configurável via `options.jwksCacheTtlMs`).
403
-
404
- Erros tipados novos (`NeetruErrorCode`):
405
- - `token_expired` — `exp` no passado
406
- - `token_invalid_signature` — assinatura não verificada pela JWKS
407
- - `token_invalid_audience` — `aud` não bate com o `clientId` extraído do `apiKey`
408
- - `token_invalid_issuer` — `iss` não é `https://auth.neetru.com`
409
-
410
- Dependência nova: `jose` (Vercel/Cloudflare-friendly, sem Node nativo).
411
-
412
- - **`MockAuth.verifyToken(token)`** — decodifica JWT sem verificar assinatura (útil em testes).
413
- Aceita `__mockVerifyToken(fn)` pra injetar falhas determinísticas.
414
-
415
- - **`VerifyTokenOptions`** exportado em `index.ts` pra consumers tiparem as options.
416
-
417
- ### Usage
418
-
419
- ```ts
420
- // Next.js route handler
421
- import { createNeetruClient } from '@neetru/sdk';
422
- const client = createNeetruClient({ apiKey: 'nrt_a1b2c3_...', env: 'prod' });
423
-
424
- export async function POST(req: Request) {
425
- const token = req.cookies.get('neetru-session')?.value ?? '';
426
- try {
427
- const user = await client.auth.verifyToken(token);
428
- // user.uid, user.email, user.isStaff, user.tenantId disponíveis
429
- } catch (err) {
430
- return new Response('Unauthorized', { status: 401 });
431
- }
432
- }
433
- ```
434
-
435
- ## [Unreleased]
436
-
437
- ### Planned (post-2.2)
438
- - size-limit budget no CI (<12KB gz core com db namespace).
439
- - CDN distribution (`cdn.neetru.com/sdk/v2/`).
440
- - Endpoint batch `/api/v1/sdk/telemetry/batch` (substitui drenagem N×1 do `track()` por 1 request).
441
-
442
- ## [2.1.1] - 2026-05-25 — **DbDocRef identity + DbBatchOp discriminated union [patch]**
443
-
444
- Dois fixes LOW reportados por pdv-agiliza (tickets `bug_97b3f0488d1c49cf8c5f94e5d544289b` + `bug_6aab9adc182640218652a65cdcb71b63`).
445
- Sem breaking changes todos os usos de `doc()` existentes continuam funcionando.
446
-
447
- ### Fixed
448
-
449
- - **`DbDocRef.id / .path / .collection`** (fix #13, ticket `bug_97b3f0488d1c49cf8c5f94e5d544289b`):
450
- `DbDocRef` agora expõe três propriedades readonly de identidade:
451
- - `id: string` ID do documento dentro da coleção.
452
- - `path: string` Caminho completo `"colecao/docId"`.
453
- - `collection: string` — Nome da coleção parent.
454
- Análogo ao `DocumentReference.id/.path` do Firestore Web SDK. Elimina a necessidade
455
- de adaptadores externos que reattachavam `collection + id` manualmente.
456
- Implementado em `DbCollectionRefImpl.doc()` (offline), `createLazyCollectionRef.doc()`
457
- (client-db) e `MockDb.doc()` (mocks).
458
-
459
- - **`DbBatchOp` discriminated union** (fix #14, ticket `bug_6aab9adc182640218652a65cdcb71b63`):
460
- `DbBatchOp` foi convertido de `interface` opaca (campo `op: 'add'|'set'|'update'|'remove'`
461
- com `id?`/`data?` opcionais) para **discriminated union** pelo campo `kind`:
462
- ```ts
463
- type DbBatchOp<T = Record<string, unknown>> =
464
- | { kind: 'add'; data: Omit<T, 'id'> }
465
- | { kind: 'set'; id: string; data: Omit<T, 'id'> }
466
- | { kind: 'update'; id: string; data: Partial<Omit<T, 'id'>> }
467
- | { kind: 'remove'; id: string };
468
- ```
469
- O `.d.ts` público agora expõe claramente quais campos são obrigatórios por variante.
470
- O campo `collection` foi removido da operação (sempre opera na coleção do `DbCollectionRef`).
471
-
472
- ### Migration (2.0.0 → 2.1.1)
473
-
474
- Callers que usavam `batch()` devem trocar `op` por `kind` e remover o campo `collection`:
475
-
476
- ```ts
477
- // antes (2.0.0)
478
- col.batch([
479
- { op: 'add', collection: 'orders', data: { total: 10 } },
480
- { op: 'set', collection: 'orders', id: 'x', data: { total: 20 } },
481
- { op: 'update', collection: 'orders', id: 'y', data: { total: 30 } },
482
- { op: 'remove', collection: 'orders', id: 'z' },
483
- ]);
484
-
485
- // depois (2.1.1)
486
- col.batch([
487
- { kind: 'add', data: { total: 10 } },
488
- { kind: 'set', id: 'x', data: { total: 20 } },
489
- { kind: 'update', id: 'y', data: { total: 30 } },
490
- { kind: 'remove', id: 'z' },
491
- ]);
492
- ```
493
-
494
- ## [2.0.0] - 2026-05-22 **client.db NeetruDb (Documentos engine-aware + SQL) [M2]**
495
-
496
- Major bump breaking changes no namespace `db`. Liga todos os building blocks M2 na superfície pública.
497
-
498
- ### Breaking Changes
499
-
500
- - **`client.db`** troca de `DbNamespace` (v0.3 REST simples) para `NeetruDb` (v2.0 offline-first engine-aware).
501
- - `collection().list()` agora retorna `DbListResult<T>` (com `docs`, `nextCursor`, `fromCache`, `stale`, `hasPendingWrites`, `changes`) em vez de `T[]`.
502
- - `collection().get(id)` retorna `DbGetResult<T> | null` em vez de `T | null`.
503
- - `db_unavailable` não significa mais "sem rede" ciclo de vida do banco. Offline é transparente.
504
- - Novos métodos obrigatórios na superfície: `sql()`, `syncState`, `flush()`, `clearCache()`, `getConflicts()`, `onSyncStateChanged()`, `batch()`, `onDoc()`, `onSnapshot()`, `doc()`.
505
- - **`db.sql(schema)` lança `NeetruDbError('db_unavailable')` em `NEETRU_ENV=dev`** sem Postgres local, sem acesso SQL. Use `neetru dev` para subir o container.
506
- - **`initNeetru`** mantido como stub funcional mas não mapeia mais `apiUrl → baseUrl`. Remover em v3.0.
507
- - **`MockDb`** foi atualizado para implementar `NeetruDb` (v2.0) em vez de `DbNamespace` (v0.3).
508
-
509
- ### Added
510
-
511
- - **`client.db.collection<T>(name)`** — retorna `DbCollectionRef<T>` offline-first, engine-aware. Funciona sem rede. Sync automático quando conectado.
512
- - CRUD: `add`, `set`, `update`, `remove`, `get`, `list` (com query, cursor, orderBy, where, limit).
513
- - Batch: `batch(ops)` operações atômicas (client-side).
514
- - Realtime: `onSnapshot(query, cb)`, `onDoc(id, cb)` — subscriptions permanentes, delivery imediato do cache.
515
- - Doc ref: `doc(id).get()`, `.set()`, `.update()`, `.remove()`, `.onSnapshot()`.
516
- - **`client.db.sql<TSchema>(schema)`** — lease SQL. Retorna `NeetruSqlClient<TSchema>` com `orm` (Drizzle `NodePgDatabase`) + `transaction()` + `close()`.
517
- - **`client.db.syncState`** — estado de sincronização observável (`status`, `pendingWrites`, `lastSyncedAt`, `isLeaderTab`).
518
- - **`client.db.onSyncStateChanged(cb)`** subscrição de mudanças de sync state.
519
- - **`client.db.flush()`** — força ciclo de sync imediato.
520
- - **`client.db.clearCache(collections?)`**limpa o cache local (IndexedDB).
521
- - **`client.db.getConflicts(collection?)`** — lista conflitos LWW pendentes.
522
- - **Engine detection** via `NeetruDbOptions.engine`: `'firestore'` | `'nosql-vm'` | `'rest'` (default MVP).
523
- - **Subpath `@neetru/sdk/db/react`** — hooks React: `useCollection`, `useDoc` (exporta via `dist/db-react.{mjs,cjs}`).
524
- - **Novos tipos públicos exportados**: `NeetruDb`, `NeetruDbEngine`, `NeetruDbOptions`, `NeetruSqlClient`, `DbSqlLease`, `DbCollectionRef`, `DbDocRef`, `DbQuery`, `DbDoc`, `DbListResult`, `DbGetResult`, `DbBatchOp`, `DbChangeType`, `NeetruDbError`, `NeetruDbErrorCode`.
525
- - **`createNeetruDb(config, opts)`** factory async (para uso fora do `createNeetruClient`).
526
- - **`createNeetruDbSync(config, opts)`** factory sync com lazy init IndexedDB (usada internamente em `createNeetruClient`).
527
-
528
- ### Changed
529
-
530
- - **`NeetruClientConfig.db`** — aceita `NeetruDbOptions` (engine, dbId, collections, dbName, etc.).
1
+ # Changelog
2
+
3
+ All notable changes to `@neetru/sdk` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [3.1.0] - 2026-06-05 — **feat: subpath `firestore-compat` + hooks de dados React (frente-B P0)**
9
+
10
+ Release **minor** só adições, nenhum export removido nem assinatura alterada (a suite de snapshot SDK-QW-5 confirma que a superfície pública 3.0 segue intacta). Upgrade direto de qualquer 3.0.x.
11
+
12
+ ### Added
13
+
14
+ - **Subpath novo `@neetru/sdk/firestore-compat`** — camada de compatibilidade (migração) que espelha a superfície modular do Firebase Firestore que os produtos já usavam: `getFirestore`, `collection`, `doc`, `query`, `where`, `orderBy`, `limit`, `getDoc`, `getDocs`, `setDoc`, `updateDoc`, `deleteDoc`, `addDoc`, `onSnapshot`, `writeBatch`, `runTransaction`, `serverTimestamp`, `Timestamp`, `FieldValue` (`arrayUnion`/`arrayRemove`/`increment`/`delete`), `deleteField`. `DocumentSnapshot`/`QuerySnapshot` emulam `.exists()`/`.data()`/`.docs[]`/`.forEach()` com `id` em `.id` (nunca duplicado dentro de `data()`).
15
+
16
+ - Backing 100% por `client.db` (`NeetruDb`). Aceita `NeetruClient`, `client.db` ou o handle `Firestore` opaco. `serverTimestamp()`/`increment()` delegam pros sentinels server-side do SDK (não mais ISO spoofável do cliente).
17
+ - Mata os ~600–1000 LOC de shims caseiros que cada SaaS reescrevia (desenhado a partir dos shims reais de `neetru-gestovendas` e `neetru-pdvagiliza`). Marcado **claramente como aid de migração** — não compete com a API nativa `NeetruDb`.
18
+
19
+ - **Hooks de dados React em `@neetru/sdk/react`** — `useCollection`, `useDoc`, `useUser` → `{ data, isLoading, error, isStale, lastUpdated }`.
20
+
21
+ - Consomem refs do `firestore-compat`; realtime nativo via `client.db.onSnapshot` com fallback de polling visibility-aware (pausa em `document.hidden`). `id` embutido em cada item sem duplicar dentro de `data`.
22
+ - `useUser`: seed síncrono do estado + timeout de seed (8s) anti-spinner-infinito; aceita `NeetruClient`, `{ auth }` ou `AuthNamespace` direto. Safe em StrictMode.
23
+ - **Não colidem** com os hooks offline-first de `@neetru/sdk/db/react` (assinaturas e retornos distintos). O bundle `@neetru/sdk/react` segue leve (~11KB, sem offline stack).
24
+
25
+ - **`useSyncState(client)`** (`@neetru/sdk/db/react`) — agora aceita o `NeetruClient` inteiro OU a `SyncStateSource` direta (resolve `client.db` automaticamente, forma oficial do `02-sdk.md §3.6`). Retrocompat 100%: passar `client.db`/namespace offline cru segue funcionando. Novo type export `UseSyncStateInput` (adição não-breaking).
26
+
27
+ ### Tests / CI
28
+
29
+ - **VERSION drift guard + snapshot da superfície pública (SDK-QW-5)** — `src/__tests__/version-and-export-surface.test.ts` trava `VERSION` (`src/index.ts`) ≡ `package.json` version e falha o build se qualquer export público de valor sumir (remoção = breaking → exige major bump consciente; adição é permitida).
30
+ - Suite cresceu de 771 → **827 tests** (+56 de `firestore-compat` + `react-data-hooks`, +4 de `useSyncState` wiring, + guards SDK-QW-5).
31
+
32
+ ## [3.0.2] - 2026-06-02 — **fix: sentinels de escrita server-side (serverTimestamp + increment) — bug_05aedb8b**
33
+
34
+ ### Fixed / Added
35
+
36
+ Fecha 3 lacunas do DB de documentos (`client.db.collection()`):
37
+
38
+ - **`serverTimestamp()`** sentinel de timestamp resolvido pelo **servidor**. Antes, `createdAt`/`updatedAt` de produto usavam o relógio do CLIENTE (spoofável + clock-skew). Agora:
39
+
40
+ ```ts
41
+ import { serverTimestamp } from '@neetru/sdk/db';
42
+ await orders.add({ total: 99, createdAt: serverTimestamp() });
43
+ ```
44
+
45
+ O marcador atravessa o transporte como `{ "__neetru__": "serverTimestamp" }` e o Core o resolve em `FieldValue.serverTimestamp()` no momento da escrita.
46
+
47
+ - **`increment(n)`** — incremento **atômico** de campo numérico (antes `update()` era só merge/replace, sujeito a lost-update sob concorrência):
48
+
49
+ ```ts
50
+ import { increment } from '@neetru/sdk/db';
51
+ await counters.update('global', { hits: increment(1) });
52
+ await wallet.update(uid, { balance: increment(-50) });
53
+ ```
54
+
55
+ Resolvido server-side via `FieldValue.increment(n)`. Aplicação otimista local: `serverTimestamp()` → `Date.now()` aproximado; `increment(n)` → soma local sobre o valor atual (valor canônico vem do servidor no sync).
56
+
57
+ - **Tipos alinhados ao runtime** `add`/`set`/`update`/`batch` e `DbDocRef.set/update` agora aceitam `WithFieldSentinels<T>`: cada campo pode receber seu tipo OU um sentinel, sem erro de tipo. O `.d.ts` é gerado da mesma fonte do runtime (tsup), eliminando divergência declaração↔runtime.
58
+
59
+ ### Novos exports
60
+
61
+ - De `@neetru/sdk` e `@neetru/sdk/db`: `serverTimestamp`, `increment`, `isFieldSentinel`, `isServerTimestampSentinel`, `isIncrementSentinel`, `NEETRU_SENTINEL_KEY`.
62
+ - Tipos: `FieldSentinel`, `ServerTimestampSentinel`, `IncrementSentinel`, `WithFieldSentinels`.
63
+
64
+ ### Segurança
65
+
66
+ - Sentinels só são resolvidos em campos **top-level** (espelha o Core). Marcador desconhecido, `operand` não-numérico, ou sentinel aninhado → o Core rejeita com `400 validation_failed` (fail-closed) e **nunca grava** o objeto cru.
67
+
68
+ > Requer Core com o resolver de sentinels (`src/lib/datastore-sentinels.ts`) nos endpoints `/api/sdk/v1/datastore/*`. Ambos os repos mudam neste fix.
69
+
70
+ ## [3.0.1] - 2026-05-29 **fix: sincronizar VERSION + docstrings + contract-staging códigos de erro**
71
+
72
+ ### Fixed
73
+
74
+ - `src/index.ts` — `VERSION` constante sincronizada com `package.json` (`'3.0.0'` `'3.0.1'`). Consumers que fazem `import { VERSION } from '@neetru/sdk'` recebiam `'3.0.0'` enquanto `npm list` mostrava `3.0.1` (LOW-NEW-SDK-01).
75
+ - `src/telemetry.ts` JSDoc na linha 6 corrigido: path legacy `/api/v1/sdk/telemetry/event` → canônico `/api/sdk/v1/telemetry/event`. O código sempre usou o path correto; apenas a documentação estava stale (LOW-NEW-SDK-02).
76
+ - `src/__tests__/telemetry.test.ts` comentário de cabeçalho atualizado para não dizer "(legado)" (LOW-NEW-SDK-02).
77
+ - `src/__tests__/contract-staging.test.ts` — `'http_error'` removido de todos os arrays `toContain`. `NeetruErrorCode` não inclui `'http_error'`; respostas HTTP não mapeáveis caem em `'unknown'`. Substituído por `'unknown'` em todos os 7 sites (LOW-NEW-SDK-03).
78
+
79
+ ### Nota
80
+
81
+ Este patch corresponde ao commit `4c40f01` (inline realtime-protocol types) que não atualizou `VERSION` nem o CHANGELOG.
82
+
83
+ ## [3.0.0] - 2026-05-27 — **MAJOR: contract hardening + Universal runtime + paths canonical**
84
+
85
+ > Major rewrite resolvendo 5 CRITICAL + 6 HIGH do review `CHEFE_SDK_REVIEW_2026-05-27`.
86
+ > SDK 2.3.6 estava com **4 dos 11 namespaces broken** contra Core LIVE (`usage`, `webhooks`, `notifications`, `support`) — 13 endpoints batendo 404/400. Tests passavam validando contrato errado (mock fetch aceita qualquer URL).
87
+ >
88
+ > **Justificativa do MAJOR**: paths/body shapes mudaram em 4 namespaces; 2 métodos removidos (`usage.track`, `usage.getQuota`); `verifyWebhookSignature` virou async. Toda CLI release/SaaS inaugural depende disso primeiro.
89
+
90
+ ### BREAKING CHANGES
91
+
92
+ #### 1. `usage` namespace 100% rewrite (CRITICAL-SDK-01)
93
+
94
+ - **REMOVIDO** `usage.track(event, props)` Core não aceitava esse shape; era vaporware testado contra mock. Use `usage.report(resource, qty, opts)`.
95
+ - **REMOVIDO** `usage.getQuota(metric)` endpoint `/usage/quota` nunca existiu no Core. `usage.report()` devolve `{value, limit, remaining}` inline em todo POST.
96
+ - Paths corrigidos:
97
+ - `/sdk/v1/usage/record` `/api/sdk/v1/usage/record` (canonical)
98
+ - `/sdk/v1/entitlements` → `/api/sdk/v1/entitlements` (canonical)
99
+ - `usage.report()` agora envia `Idempotency-Key: <uuid v4>` header automaticamente.
100
+ - `usage.check()` retorna novo field `behavior: "readonly" | null` (inferido de `reason === "limit_exceeded"`).
101
+
102
+ #### 2. `webhooks` namespace `productId` obrigatório (CRITICAL-SDK-03)
103
+
104
+ - `RegisterWebhookInput.productId` adicionado. Default resolve do `config.productId`; lança `validation_failed` se ambos ausentes.
105
+ - `list(options?)`, `unregister(id, options?)`, `test(id, options?)` agora aceitam `{ productId }` (Core exige query param em todos os verbos).
106
+ - `webhooks.test()` envia `Idempotency-Key` header.
107
+
108
+ #### 3. `notifications` namespace `productId` obrigatório (CRITICAL-SDK-04)
109
+
110
+ - `SendNotificationInput.productId` adicionado (Core valida via Zod `SendNotificationSchema`).
111
+ - `ListNotificationsOptions.productId` adicionado.
112
+ - `NotificationScopeOptions` novo type para `markRead`/`dismiss`.
113
+ - `notifications.send()` envia `Idempotency-Key` header.
114
+
115
+ #### 4. `support` namespaceendpoint canonical (CRITICAL-SDK-02)
116
+
117
+ - Endpoint mudou de `/api/v1/products/{slug}/tickets` (vaporware **nunca existiu no Core**) para `/api/sdk/v1/support/tickets` (canonical SDK path).
118
+ - `productId` obrigatório (default `config.productId`).
119
+ - `createTicket()` envia `Idempotency-Key` header.
120
+ - `listMyTickets(options?)` aceita `{ productSlug }` override.
121
+ - **Pré-requisito Core**: endpoint `/api/sdk/v1/support/tickets` precisa ser construído (vide `docs/CORE_CHANGES_REQUIRED.md` §1).
122
+
123
+ #### 5. `verifyWebhookSignature` é async + Universal (HIGH-SDK-03)
124
+
125
+ - **Antes (2.x)**: `(payload, sig, secret) => boolean` — usava `require('node:crypto')` que quebrava em bundle browser/Edge ESM puro. README anunciava "Universal" mas mentia.
126
+ - **Agora (3.0)**: `(payload, sig, secret) => Promise<boolean>` via `WebCrypto.subtle.sign` HMAC-SHA-256 + constant-time XOR compare.
127
+ - Funciona em Node ≥20, browsers modernos, Vercel Edge Functions, Cloudflare Workers.
128
+ - Lança `NeetruError('runtime_unsupported')` se WebCrypto ausente (distingue "ambiente não suporta" de "assinatura inválida").
129
+
130
+ #### 6. `telemetry.event` / `telemetry.track` path canonical (CRITICAL-SDK-05)
131
+
132
+ - `POST /api/v1/sdk/telemetry/event` → `POST /api/sdk/v1/telemetry/event` (canonical SDK pattern).
133
+ - Pré-requisito Core: alias/handler para o canonical path (vide `docs/CORE_CHANGES_REQUIRED.md` §3).
134
+
135
+ #### 7. `baseUrl` allowlist fail-closed (HIGH-SDK-05)
136
+
137
+ - `createNeetruClient` agora chama `validateBaseUrl` antes de aceitar `config.baseUrl`.
138
+ - Allowlist padrão: HTTPS `*.neetru.com` + `localhost`/`127.0.0.1`/`*.test`/`*.localhost`.
139
+ - `NeetruClientConfig.allowedHosts: string[]` novo adicione hosts custom (suffix match).
140
+ - Bypass: `NEETRU_ALLOW_INSECURE_BASEURL=1` (env) ou `globalThis.NEETRU_ALLOW_INSECURE_BASEURL='1'`. Warning no console quando ativo.
141
+ - Cenário ataque fechado: dev configura `NEETRU_BASE_URL=http://attacker.example/` no `.env`, agora lança `invalid_config` em vez de vazar Bearer.
142
+
143
+ #### 8. Retries default por método (HIGH-SDK-01)
144
+
145
+ - **GET/HEAD**: 2 retries default (= 3 tentativas) — idempotente, seguro.
146
+ - **POST/PUT/PATCH/DELETE**: **0 retries default** — mutations podem duplicar side effects (ex: `FieldValue.increment` no `usage/record`).
147
+ - Caller pode override: `httpRequest(config, { method: 'POST', ..., retries: 2 })`.
148
+ - Combinado com `Idempotency-Key` automático em mutations: dup-write rate zero.
149
+
150
+ #### 9. Cleanup de stubs deprecated
151
+
152
+ - `initNeetru` (deprecated desde v0.2, no-op em v2.0) **definitivamente removido**.
153
+ - `NeetruConfig` type alias **removido** (use `NeetruClientConfig`).
154
+
155
+ #### 10. `NeetruErrorCode` ganhou `runtime_unsupported`
156
+
157
+ - Lançado por `verifyWebhookSignature` quando WebCrypto ausente. Não é breaking se você usa só `instanceof NeetruError`; é breaking se você fez `switch` no union type (TS error em case missing).
158
+
159
+ ### ADDED
160
+
161
+ - `src/idempotency.ts` — `generateIdempotencyKey()` via `crypto.randomUUID()` (Node ≥20, browsers, Edge).
162
+ - `src/base-url-allowlist.ts` — `validateBaseUrl(url, allowedHosts)` fail-closed.
163
+ - `HttpRequestOptions.idempotencyKey: boolean | string` — injeta header automaticamente em todo POST de mutation.
164
+ - `client.usage.check().behavior: 'readonly' | null` — sinal para `<EntitlementGate mode='readonly'>` decidir UI.
165
+ - `NeetruClientConfig.allowedHosts` — extension point para hosts custom (staging/proxies).
166
+ - `MockUsage.__getReports()` substitui `__getRecords()` (legacy alias mantido).
167
+ - Test suite: `src/__tests__/base-url-allowlist.test.ts` (16 tests), `src/__tests__/idempotency.test.ts` (9 tests).
168
+ - `docs/CORE_CHANGES_REQUIRED.md` — catálogo de 5 endpoints Core a criar/modificar para 3.0 funcionar end-to-end.
169
+
170
+ ### FIXED
171
+
172
+ - `<EntitlementGate>` v1.1 não morre mais em silent `allowed:false` — paths corrigidos no `usage.check` (era dependência transitiva, fixed via CRITICAL-SDK-01).
173
+ - README v2.x prometia "Universal: browser/Node/Edge" mas `verifyWebhookSignature` só funcionava em Node. 3.0 cumpre.
174
+
175
+ ### Migration guide v2.x → v3.0
176
+
177
+ ```ts
178
+ // Antes (v2.x)
179
+ await client.usage.track('api_call', { route: '/x' });
180
+ const quota = await client.usage.getQuota('api_calls');
181
+
182
+ await client.webhooks.register({
183
+ url: 'https://x.com/h',
184
+ events: ['subscription.activated'],
185
+ });
186
+
187
+ await client.notifications.send({
188
+ userId: 'u1',
189
+ kind: 'order.received',
190
+ title: 'Pedido',
191
+ });
192
+
193
+ await client.support.createTicket({
194
+ subject: 'help',
195
+ message: 'x',
196
+ });
197
+
198
+ // ✅ Depois (v3.0)
199
+ const client = createNeetruClient({
200
+ apiKey: 'nrt_...',
201
+ productId: 'gestovendas', // 🆕 default p/ todos os namespaces
202
+ tenantId: 't-acme', // 🆕 default p/ usage
203
+ });
204
+
205
+ const result = await client.usage.report('api_call', 1);
206
+ // result.value, result.limit, result.remaining inline — não precisa getQuota separado.
207
+
208
+ const allowed = await client.usage.check('api_call');
209
+ // allowed.behavior === 'readonly' quando over limit (use em <EntitlementGate>)
210
+
211
+ await client.webhooks.register({
212
+ // productId default config.productId, ou explícito:
213
+ url: 'https://x.com/h',
214
+ events: ['subscription.activated'],
215
+ });
216
+
217
+ await client.notifications.send({
218
+ userId: 'u1',
219
+ kind: 'order.received',
220
+ title: 'Pedido',
221
+ });
222
+
223
+ await client.support.createTicket({
224
+ subject: 'help',
225
+ message: 'x',
226
+ });
227
+ ```
228
+
229
+ ### Verificação de assinatura de webhook agora async
230
+
231
+ ```ts
232
+ // ❌ Antes (v2.x) — sync, quebrava em Edge
233
+ import { verifyWebhookSignature } from '@neetru/sdk/webhooks';
234
+ const ok = verifyWebhookSignature(rawPayload, sigHeader, secret);
235
+
236
+ // Depois (v3.0) async, Universal
237
+ const ok = await verifyWebhookSignature(rawPayload, sigHeader, secret);
238
+ ```
239
+
240
+ ### Endpoints Core que precisam ser criados/modificados antes do publish 3.0
241
+
242
+ Vide `docs/CORE_CHANGES_REQUIRED.md` completo:
243
+
244
+ 1. **CRITICAL** — `POST/GET /api/sdk/v1/support/tickets` (customer-facing) — 3-4h.
245
+ 2. **HIGH** — Idempotency-Key dedup em endpoints de mutation — 4-6h.
246
+ 3. **HIGH** — `POST /api/sdk/v1/telemetry/event` (canonical) — 1-2h ou alias middleware.
247
+ 4. **LOW** — `behavior` field em `/api/sdk/v1/entitlements` response 30min.
248
+ 5. **LOW** Verificar envelope `/api/sdk/v1/datastore/{collection}` 30min check.
249
+
250
+ ### Tests
251
+
252
+ - **719 tests passing** (era 668 em 2.3.6). Tsc clean. Build clean.
253
+ - Contract tests reescritos para validar paths canonical + body shapes + headers (Idempotency-Key UUID v4 regex match).
254
+
255
+ ## [2.3.6] - 2026-05-26 — **fix(bundle): webpackIgnore em TODOS os dynamic imports node-only — bug_168368ae [CRITICAL]**
256
+
257
+ ### Corrigido
258
+
259
+ - **bug_168368aedb664edba1ca1f1945b568ac (CRITICAL) — fix 2.3.5 foi parcial: `webpackIgnore` aplicado só em `createSqlClientFromConfig`, mas `createPgPool` + `createDrizzleHandle` (helpers separados no mesmo arquivo) + os 3 dynamic imports em `MockDb.sql()` (`pg-mem`, `drizzle-orm/node-postgres`, `@electric-sql/pglite`, `drizzle-orm/pglite`) ainda estavam sem o magic comment.**
260
+ Resultado: webpack do consumer Next.js seguia o module graph daqueles paths e ainda tentava bundle `pg` → `Module not found: tls/fs` no client.
261
+ Fix 2.3.6: `/* webpackIgnore: true */` em **todos** os 7 spots de `await import('pg' | 'drizzle-orm/node-postgres' | 'pg-mem' | '@electric-sql/pglite' | 'drizzle-orm/pglite')` em `sql-client.ts` + `mocks.ts`.
262
+
263
+ ### Testes (antirregressão atualizado)
264
+
265
+ - **`no-node-deps-in-browser-bundle.regression.test.ts`** expandido: agora pega `import("X")` dynamic SEM `webpackIgnore` (regex negative lookahead). Antes só cobria `from "X"` static. Adicionado 4 patterns: pg, drizzle-orm/node-postgres, pg-mem, @electric-sql/pglite.
266
+
267
+ ## [2.3.5] - 2026-05-26 **fix(bundle): node deps fora do browser entry — bug_0f836014 [CRITICAL]**
268
+
269
+ ### Corrigido
270
+
271
+ - **bug_0f836014055747858adfa8ab0e8cbb47 (CRITICAL) SDK 2.3.4 publicou com `require('pg')` + `require('drizzle-orm/node-postgres')` literais no `dist/index.mjs`.**
272
+ Webpack do consumer Next.js seguia o module graph e tentava bundle pg puxa fs → `Module not found: Can't resolve 'fs'` no client bundle. Bloqueava `npm run build` de qualquer projeto Next.js que importa `createNeetruClient`.
273
+ Fix: dynamic import com `/* webpackIgnore: true */` em `createSqlClientFromConfig`. Instrui webpack a NÃO processar — runtime Node resolve. `sql()` é server-only por contrato.
274
+ Resultado: `dist/index.mjs` zero imports literais de pg/drizzle/fs/node:*. Verified via teste antirregressão novo.
275
+
276
+ ### Testes (antirregressão)
277
+
278
+ - **`src/__tests__/regressions/no-node-deps-in-browser-bundle.regression.test.ts`** (novo) — varre 13 entry bundles e falha se encontrar require/import estático de pg/drizzle-orm/node-postgres/pg-mem/@electric-sql/pglite/fs/node:*. Roda APÓS `npm run build`. Pega regressão dessa classe.
279
+
280
+ ## [2.3.4] - 2026-05-26 — **fix(mocks): MockDb.sql() migra pra pglite + pg-mem fallback — bug_6bae8674 [HIGH]**
281
+
282
+ ### Corrigido
283
+
284
+ - **bug_6bae8674461f4600809e2f9de13c6043 (HIGH) — fix anterior (2.3.3) teve efeito INVERSO em `MockDb.sql()`.**
285
+ O fix de 2.3.3 injetava `getTypeParser` em `pool.types` para resolver o erro "Not supported: getTypeParser is not supported".
286
+ Porém `pg-mem` tem um guard interno: `if (query.types?.getTypeParser) throw NotSupported`.
287
+ `drizzle-orm/node-postgres/session.js:113` passa `rawQuery.types = pool.types` em TODA query — ao adicionar
288
+ `getTypeParser` ao pool, drizzle passou-o para o pg-mem que o rejeitou imediatamente.
289
+ Efeito líquido: qualquer `INSERT`/`SELECT`/`UPDATE` continuava quebrando com o mesmo erro.
290
+
291
+ **Fix 2.3.4 — duas soluções em cascata:**
292
+
293
+ 1. **Solução A (preferida): `@electric-sql/pglite` + `drizzle-orm/pglite`** WASM-Postgres nativo
294
+ sem adapter pg-mem. Sem conflito `getTypeParser`, sem monkey-patch. `PGlite.create()` retorna
295
+ instância pronta, DDL via `pglite.query(ddl)`, ORM via `drizzle(pglite, { schema })`. Cast
296
+ `PgliteDatabase → NodePgDatabase` via `unknown` (interfaces Drizzle compatíveis em superfície
297
+ mas com tipos de resultado internos distintos).
298
+
299
+ 2. **Solução B (fallback pg-mem)**: strip interceptor — intercepta `pool.query` e deleta
300
+ `types.getTypeParser` do objeto de query ANTES do pg-mem ver. Guard não dispara, query passa
301
+ limpa. Compatível com consumers que têm pg-mem nas devDeps.
302
+
303
+ **`@electric-sql/pglite` adicionado como `peerDependency` optional** (`^0.4.0`) + `devDependency`
304
+ interno (`^0.4.6`).
305
+
306
+ **8 novos testes** em `src/__tests__/mocks-sql-pglite.test.ts`: criação sem erro, INSERT sem throw,
307
+ SELECT após INSERT, UPDATE, múltiplos INSERTs, sem initSql, close idempotente, transaction().
308
+ Todos passando.
309
+
310
+ **655 testes totais** (era 647 em 2.3.3). `tsc --noEmit` clean. Build clean.
311
+
312
+ ## [2.3.3] - 2026-05-26 **fix(mocks): pool.types.getTypeParser identity parser bug_44bcc99f [HIGH]**
313
+
314
+ ### Corrigido
315
+
316
+ - **bug_44bcc99f9d3140d78abd8f7810d1afa4 (HIGH)`MockDb.sql()` queries Drizzle falhavam em runtime.**
317
+ Followup do `bug_a7f2ae25` (SDK 2.3.2): `sql()` retornava handle Drizzle, mas qualquer query (`INSERT`, `SELECT`, etc) jogava `Error: Not supported: getTypeParser is not supported`. `pg-mem` não implementa `pool.types.getTypeParser(oid)` que `drizzle-orm/node-postgres/session.js` chama em TODA query.
318
+ Fix: monkey-patch identity parser no Pool retornado por `pg-mem.adapters.createPg()` antes de passar pro Drizzle. Types retornam o valor sem transformação (suficiente pra tests in-memory; produção usa `pg` real com parsers nativos).
319
+ 3 linhas. Non-breaking. Tests E2E Drizzle in-memory destravados.
320
+
321
+ ## [2.3.2] - 2026-05-26 — **fix(mocks): MockDb.sql() implementado in-memory via pg-mem bug_a7f2ae25 [HIGH]**
322
+
323
+ ### Corrigido
324
+
325
+ - **bug_a7f2ae25fda54129b52b6dd55aaa6050 (HIGH) `MockDb.sql()` lançava `db_unavailable` em vez de implementar Drizzle in-memory.**
326
+ Bloqueava vitest server-side dos consumers (pdv-agiliza Fase 4 migration). Política do projeto proíbe workaround instalar `pg-mem` no consumer.
327
+ Fix: `MockDb.sql(schema, options)` agora carrega `pg-mem` lazy + cria `NodePgDatabase<TSchema>` Drizzle apontando pra DB in-memory. Suporta `options.initSql: string | string[]` pra rodar DDL no setup (CREATE TABLE etc).
328
+ `pg-mem` adicionado como `peerDependency optional` (com `peerDependenciesMeta.optional=true`) + `devDependency` interno. Consumers que querem mock SQL: `npm install -D pg-mem drizzle-orm pg`.
329
+ **Não-breaking:** signature `sql(schema, options?)` mantida. SqlOptions ganhou `initSql?` opcional (ignorado por lease real em produção). Comportamento default — `sql()` sem `initSql` — funciona em ambos modos.
330
+
331
+ ## [2.3.1] - 2026-05-26 — **fix(deps): drizzle-orm + pg como peerDependencies — bug_42ae9f80 [CRITICAL]**
332
+
333
+ ### Corrigido
334
+
335
+ - **bug_42ae9f80b2884bd583c5246771f7730c (CRITICAL) — `drizzle-orm` em `dependencies` causava type collision com versão do consumer.**
336
+ Quando o projeto consumer instalava `drizzle-orm` direto e tentava `client.db.sql(schema)`, o TypeScript via 2 cópias diferentes (uma nested em `node_modules/@neetru/sdk/node_modules/drizzle-orm`, outra no consumer) e reclamava: `'Property config is protected but type Column<...> is not a class derived from Column<...>'` + `'shouldInlineParams private'`. Bloqueava Fases 2-8 da migração pdv-agiliza pra `vm-postgres-single`.
337
+ Fix: movidas `drizzle-orm` e `pg` de `dependencies` pra `peerDependencies` (com `peerDependenciesMeta.optional: true`). Consumer instala 1 versão, todos os tipos convergem. Devkit mantém `drizzle-orm` + `pg` em `devDependencies` pra build/test internos.
338
+ **Breaking expected on consumer side:** se o projeto consumer NÃO tinha `drizzle-orm` em deps mas usava `client.db.sql()`, agora precisa `npm install drizzle-orm pg`. Consumers que só usam transport REST/Firestore não são afetados (peerDeps optional).
339
+
340
+ ## [2.3.0] - 2026-05-25 — **feat(db-config): renomeia engine→transport com deprecation bug_ed9da7fd [HIGH]**
341
+
342
+ Fix do bug `bug_ed9da7fd11194eac87f0eb9c75e5fad9` (HIGH) — colisão de terminologia: `engine` no SDK
343
+ configurava o transporte SDK↔Core (REST/Firestore/WebSocket), mas `engine` no admin database (CLI
344
+ `neetru admin database create --engine=...`) configura o storage engine (firestore-instance,
345
+ cloud-sql-postgres, vm-postgres-single…). Devs colocavam `firestore-instance` no SDK e quebravam.
346
+
347
+ ### Added
348
+
349
+ - **`NeetruDbTransport`** novo tipo público canônico para o campo de transporte SDK↔Core.
350
+ Valores: `'rest' | 'firestore' | 'nosql-vm'` (idênticos aos de `NeetruDbEngine`).
351
+
352
+ - **`NeetruDbOptions.transport`** — novo campo canônico que substitui `engine`.
353
+ ```ts
354
+ // v2.3+ (preferido):
355
+ createNeetruClient({ db: { transport: 'rest' } });
356
+ createNeetruClient({ db: { transport: 'firestore' } });
357
+ createNeetruClient({ db: { transport: 'nosql-vm', realtimeGatewayUrl: '...' } });
358
+ ```
359
+
360
+ ### Deprecated
361
+
362
+ - **`NeetruDbOptions.engine`** depreciado em v2.3, será removido em v3.0.
363
+ Continua funcionando: se `transport` estiver ausente e `engine` presente, o valor de `engine`
364
+ é usado como transport com `console.warn` de deprecação.
365
+
366
+ ```ts
367
+ // v2.x (depreciadoemite console.warn):
368
+ createNeetruClient({ db: { engine: 'firestore' } });
369
+ // [neetru/sdk] NeetruDbOptions.engine está depreciado...
370
+ ```
371
+
372
+ - **`NeetruDbEngine`** — depreciado em v2.3, será removido em v3.0.
373
+ Agora é alias de `NeetruDbTransport`. Use `NeetruDbTransport` em código novo.
374
+
375
+ ### Behavior
376
+
377
+ - `transport` e `engine` podem coexistir: `transport` sempre prevalece, sem warn.
378
+ - Sem `transport` nem `engine`: default `'rest'`, sem warn (comportamento idêntico ao v2.x).
379
+ - A relação 1:N entre storage engines e transports é documentada no JSDoc de `NeetruDbTransport`:
380
+ qualquer storage engine (cloud-sql-postgres, vm-postgres-single…) pode ser acessado com
381
+ `transport: 'rest'`; `transport: 'firestore'` e `transport: 'nosql-vm'` são para bancos
382
+ documentais com realtime nativo.
383
+
384
+ ### Tests
385
+
386
+ - 5 testes novos em `src/__tests__/db-real.test.ts`:
387
+ `transport=firestore`, `transport=nosql-vm`, `transport=rest`, colisão `transport+engine`
388
+ (transport prevalece, sem warn), default sem campos (sem warn).
389
+ - Testes de regressão: `engine=firestore` e `engine=nosql-vm` legados ainda passam + emitem warn.
390
+
391
+ ## [2.2.1] - 2026-05-25 — **fix(offline-docs): fallback MemoryStore em Node bug_ba287a CRITICAL**
392
+
393
+ Fix do bug `bug_ba287aefdf6c4327b026d588a239ad06` (CRITICAL) — `client.db.collection().set/add/update`
394
+ lançava `ReferenceError: indexedDB is not defined` em Node.js (reportado por pdv-agiliza prod).
395
+
396
+ ### Fixed
397
+
398
+ - **`createOfflineDocumentsNamespace` — auto-detect Node runtime e usa `MemoryStore`**.
399
+ A função agora verifica `typeof indexedDB === 'undefined' || typeof window === 'undefined'`
400
+ antes de instanciar `LocalStore` (que depende de `idb` / IndexedDB). Em Node/Bun/Edge Workers
401
+ sem IDB, cai automaticamente em `MemoryStore` implementação in-process com a mesma API,
402
+ sem nenhuma flag explícita necessária. Ops de documentos (`set/add/update/remove/get/list`,
403
+ `batch`, `onSnapshot`, `onDoc`) funcionam normalmente em Node; dados não persistem entre
404
+ reinicializações do processo (comportamento esperado em SSR/server-side).
405
+
406
+ ### Added
407
+
408
+ - **`src/db/offline/memory-store.ts`** — `MemoryStore`: implementação in-memory do contrato
409
+ público de `LocalStore`. Cobre todas as 5 stores (documents, mutations, query\_cache,
410
+ sync\_meta, conflict\_log) com Maps/Arrays em memória.
411
+
412
+ ### Tests
413
+
414
+ - **`src/__tests__/offline-docs-node-compat.test.ts`** — 5 casos de regressão:
415
+ verifica que a stack offline não lança `ReferenceError` em ambiente sem IDB,
416
+ e que `set/add/update` + `get` roundtrips funcionam in-memory.
417
+
418
+ ## [2.2.0] - 2026-05-25 — **auth.verifyToken — validação server-side de id_token via JWKS [feature]**
419
+
420
+ Fix do bug `bug_a7ef1702680044e3994b74bdccecb2ab` — resolve P0-1 audit security pdv-agiliza.
421
+
422
+ ### Added
423
+
424
+ - **`client.auth.verifyToken(token, options?): Promise<NeetruUser>`** — valida um id_token JWT
425
+ emitido por `auth.neetru.com` server-side, sem round-trip de rede por request. Usa JWKS local
426
+ cacheado (default TTL 1h, configurável via `options.jwksCacheTtlMs`).
427
+
428
+ Erros tipados novos (`NeetruErrorCode`):
429
+ - `token_expired` `exp` no passado
430
+ - `token_invalid_signature` assinatura não verificada pela JWKS
431
+ - `token_invalid_audience` — `aud` não bate com o `clientId` extraído do `apiKey`
432
+ - `token_invalid_issuer` — `iss` não é `https://auth.neetru.com`
433
+
434
+ Dependência nova: `jose` (Vercel/Cloudflare-friendly, sem Node nativo).
435
+
436
+ - **`MockAuth.verifyToken(token)`** — decodifica JWT sem verificar assinatura (útil em testes).
437
+ Aceita `__mockVerifyToken(fn)` pra injetar falhas determinísticas.
438
+
439
+ - **`VerifyTokenOptions`** exportado em `index.ts` pra consumers tiparem as options.
440
+
441
+ ### Usage
442
+
443
+ ```ts
444
+ // Next.js route handler
445
+ import { createNeetruClient } from '@neetru/sdk';
446
+ const client = createNeetruClient({ apiKey: 'nrt_a1b2c3_...', env: 'prod' });
447
+
448
+ export async function POST(req: Request) {
449
+ const token = req.cookies.get('neetru-session')?.value ?? '';
450
+ try {
451
+ const user = await client.auth.verifyToken(token);
452
+ // user.uid, user.email, user.isStaff, user.tenantId disponíveis
453
+ } catch (err) {
454
+ return new Response('Unauthorized', { status: 401 });
455
+ }
456
+ }
457
+ ```
458
+
459
+ ## [Unreleased]
460
+
461
+ ### Planned (post-2.2)
462
+ - size-limit budget no CI (<12KB gz core com db namespace).
463
+ - CDN distribution (`cdn.neetru.com/sdk/v2/`).
464
+ - Endpoint batch `/api/v1/sdk/telemetry/batch` (substitui drenagem N×1 do `track()` por 1 request).
465
+
466
+ ## [2.1.1] - 2026-05-25 **DbDocRef identity + DbBatchOp discriminated union [patch]**
467
+
468
+ Dois fixes LOW reportados por pdv-agiliza (tickets `bug_97b3f0488d1c49cf8c5f94e5d544289b` + `bug_6aab9adc182640218652a65cdcb71b63`).
469
+ Sem breaking changes todos os usos de `doc()` existentes continuam funcionando.
470
+
471
+ ### Fixed
472
+
473
+ - **`DbDocRef.id / .path / .collection`** (fix #13, ticket `bug_97b3f0488d1c49cf8c5f94e5d544289b`):
474
+ `DbDocRef` agora expõe três propriedades readonly de identidade:
475
+ - `id: string` — ID do documento dentro da coleção.
476
+ - `path: string` — Caminho completo `"colecao/docId"`.
477
+ - `collection: string` — Nome da coleção parent.
478
+ Análogo ao `DocumentReference.id/.path` do Firestore Web SDK. Elimina a necessidade
479
+ de adaptadores externos que reattachavam `collection + id` manualmente.
480
+ Implementado em `DbCollectionRefImpl.doc()` (offline), `createLazyCollectionRef.doc()`
481
+ (client-db) e `MockDb.doc()` (mocks).
482
+
483
+ - **`DbBatchOp` discriminated union** (fix #14, ticket `bug_6aab9adc182640218652a65cdcb71b63`):
484
+ `DbBatchOp` foi convertido de `interface` opaca (campo `op: 'add'|'set'|'update'|'remove'`
485
+ com `id?`/`data?` opcionais) para **discriminated union** pelo campo `kind`:
486
+ ```ts
487
+ type DbBatchOp<T = Record<string, unknown>> =
488
+ | { kind: 'add'; data: Omit<T, 'id'> }
489
+ | { kind: 'set'; id: string; data: Omit<T, 'id'> }
490
+ | { kind: 'update'; id: string; data: Partial<Omit<T, 'id'>> }
491
+ | { kind: 'remove'; id: string };
492
+ ```
493
+ O `.d.ts` público agora expõe claramente quais campos são obrigatórios por variante.
494
+ O campo `collection` foi removido da operação (sempre opera na coleção do `DbCollectionRef`).
495
+
496
+ ### Migration (2.0.0 2.1.1)
497
+
498
+ Callers que usavam `batch()` devem trocar `op` por `kind` e remover o campo `collection`:
499
+
500
+ ```ts
501
+ // antes (2.0.0)
502
+ col.batch([
503
+ { op: 'add', collection: 'orders', data: { total: 10 } },
504
+ { op: 'set', collection: 'orders', id: 'x', data: { total: 20 } },
505
+ { op: 'update', collection: 'orders', id: 'y', data: { total: 30 } },
506
+ { op: 'remove', collection: 'orders', id: 'z' },
507
+ ]);
508
+
509
+ // depois (2.1.1)
510
+ col.batch([
511
+ { kind: 'add', data: { total: 10 } },
512
+ { kind: 'set', id: 'x', data: { total: 20 } },
513
+ { kind: 'update', id: 'y', data: { total: 30 } },
514
+ { kind: 'remove', id: 'z' },
515
+ ]);
516
+ ```
517
+
518
+ ## [2.0.0] - 2026-05-22 — **client.db NeetruDb (Documentos engine-aware + SQL) [M2]**
519
+
520
+ Major bumpbreaking changes no namespace `db`. Liga todos os building blocks M2 na superfície pública.
521
+
522
+ ### Breaking Changes
523
+
524
+ - **`client.db`** troca de `DbNamespace` (v0.3 REST simples) para `NeetruDb` (v2.0 offline-first engine-aware).
525
+ - `collection().list()` agora retorna `DbListResult<T>` (com `docs`, `nextCursor`, `fromCache`, `stale`, `hasPendingWrites`, `changes`) em vez de `T[]`.
526
+ - `collection().get(id)` retorna `DbGetResult<T> | null` em vez de `T | null`.
527
+ - `db_unavailable` não significa mais "sem rede" — só ciclo de vida do banco. Offline é transparente.
528
+ - Novos métodos obrigatórios na superfície: `sql()`, `syncState`, `flush()`, `clearCache()`, `getConflicts()`, `onSyncStateChanged()`, `batch()`, `onDoc()`, `onSnapshot()`, `doc()`.
529
+ - **`db.sql(schema)` lança `NeetruDbError('db_unavailable')` em `NEETRU_ENV=dev`** — sem Postgres local, sem acesso SQL. Use `neetru dev` para subir o container.
530
+ - **`initNeetru`** — mantido como stub funcional mas não mapeia mais `apiUrl baseUrl`. Remover em v3.0.
531
+ - **`MockDb`** foi atualizado para implementar `NeetruDb` (v2.0) em vez de `DbNamespace` (v0.3).
532
+
533
+ ### Added
534
+
535
+ - **`client.db.collection<T>(name)`** — retorna `DbCollectionRef<T>` offline-first, engine-aware. Funciona sem rede. Sync automático quando conectado.
536
+ - CRUD: `add`, `set`, `update`, `remove`, `get`, `list` (com query, cursor, orderBy, where, limit).
537
+ - Batch: `batch(ops)` — operações atômicas (client-side).
538
+ - Realtime: `onSnapshot(query, cb)`, `onDoc(id, cb)` — subscriptions permanentes, delivery imediato do cache.
539
+ - Doc ref: `doc(id).get()`, `.set()`, `.update()`, `.remove()`, `.onSnapshot()`.
540
+ - **`client.db.sql<TSchema>(schema)`** — lease SQL. Retorna `NeetruSqlClient<TSchema>` com `orm` (Drizzle `NodePgDatabase`) + `transaction()` + `close()`.
541
+ - **`client.db.syncState`** — estado de sincronização observável (`status`, `pendingWrites`, `lastSyncedAt`, `isLeaderTab`).
542
+ - **`client.db.onSyncStateChanged(cb)`** — subscrição de mudanças de sync state.
543
+ - **`client.db.flush()`** — força ciclo de sync imediato.
544
+ - **`client.db.clearCache(collections?)`** — limpa o cache local (IndexedDB).
545
+ - **`client.db.getConflicts(collection?)`** — lista conflitos LWW pendentes.
546
+ - **Engine detection** via `NeetruDbOptions.engine`: `'firestore'` | `'nosql-vm'` | `'rest'` (default MVP).
547
+ - **Subpath `@neetru/sdk/db/react`** — hooks React: `useCollection`, `useDoc` (exporta via `dist/db-react.{mjs,cjs}`).
548
+ - **Novos tipos públicos exportados**: `NeetruDb`, `NeetruDbEngine`, `NeetruDbOptions`, `NeetruSqlClient`, `DbSqlLease`, `DbCollectionRef`, `DbDocRef`, `DbQuery`, `DbDoc`, `DbListResult`, `DbGetResult`, `DbBatchOp`, `DbChangeType`, `NeetruDbError`, `NeetruDbErrorCode`.
549
+ - **`createNeetruDb(config, opts)`** — factory async (para uso fora do `createNeetruClient`).
550
+ - **`createNeetruDbSync(config, opts)`** — factory sync com lazy init IndexedDB (usada internamente em `createNeetruClient`).
551
+
552
+ ### Changed
553
+
554
+ - **`NeetruClientConfig.db`** — aceita `NeetruDbOptions` (engine, dbId, collections, dbName, etc.).