# Changelog

All notable changes to `@neetru/sdk` will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.1.0] - 2026-06-05 — **feat: subpath `firestore-compat` + hooks de dados React (frente-B P0)**

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.

### Added

- **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()`).

  - 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).
  - 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`.

- **Hooks de dados React em `@neetru/sdk/react`** — `useCollection`, `useDoc`, `useUser` → `{ data, isLoading, error, isStale, lastUpdated }`.

  - 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`.
  - `useUser`: seed síncrono do estado + timeout de seed (8s) anti-spinner-infinito; aceita `NeetruClient`, `{ auth }` ou `AuthNamespace` direto. Safe em StrictMode.
  - **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).

- **`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).

### Tests / CI

- **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).
- Suite cresceu de 771 → **827 tests** (+56 de `firestore-compat` + `react-data-hooks`, +4 de `useSyncState` wiring, + guards SDK-QW-5).

## [3.0.2] - 2026-06-02 — **fix: sentinels de escrita server-side (serverTimestamp + increment) — bug_05aedb8b**

### Fixed / Added

Fecha 3 lacunas do DB de documentos (`client.db.collection()`):

- **`serverTimestamp()`** — sentinel de timestamp resolvido pelo **servidor**. Antes, `createdAt`/`updatedAt` de produto usavam o relógio do CLIENTE (spoofável + clock-skew). Agora:

  ```ts
  import { serverTimestamp } from '@neetru/sdk/db';
  await orders.add({ total: 99, createdAt: serverTimestamp() });
  ```

  O marcador atravessa o transporte como `{ "__neetru__": "serverTimestamp" }` e o Core o resolve em `FieldValue.serverTimestamp()` no momento da escrita.

- **`increment(n)`** — incremento **atômico** de campo numérico (antes `update()` era só merge/replace, sujeito a lost-update sob concorrência):

  ```ts
  import { increment } from '@neetru/sdk/db';
  await counters.update('global', { hits: increment(1) });
  await wallet.update(uid, { balance: increment(-50) });
  ```

  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).

- **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.

### Novos exports

- De `@neetru/sdk` e `@neetru/sdk/db`: `serverTimestamp`, `increment`, `isFieldSentinel`, `isServerTimestampSentinel`, `isIncrementSentinel`, `NEETRU_SENTINEL_KEY`.
- Tipos: `FieldSentinel`, `ServerTimestampSentinel`, `IncrementSentinel`, `WithFieldSentinels`.

### Segurança

- 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.

> Requer Core com o resolver de sentinels (`src/lib/datastore-sentinels.ts`) nos endpoints `/api/sdk/v1/datastore/*`. Ambos os repos mudam neste fix.

## [3.0.1] - 2026-05-29 — **fix: sincronizar VERSION + docstrings + contract-staging códigos de erro**

### Fixed

- `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).
- `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).
- `src/__tests__/telemetry.test.ts` — comentário de cabeçalho atualizado para não dizer "(legado)" (LOW-NEW-SDK-02).
- `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).

### Nota

Este patch corresponde ao commit `4c40f01` (inline realtime-protocol types) que não atualizou `VERSION` nem o CHANGELOG.

## [3.0.0] - 2026-05-27 — **MAJOR: contract hardening + Universal runtime + paths canonical**

> Major rewrite resolvendo 5 CRITICAL + 6 HIGH do review `CHEFE_SDK_REVIEW_2026-05-27`.
> 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).
>
> **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.

### BREAKING CHANGES

#### 1. `usage` namespace 100% rewrite (CRITICAL-SDK-01)

- **REMOVIDO** `usage.track(event, props)` — Core não aceitava esse shape; era vaporware testado contra mock. Use `usage.report(resource, qty, opts)`.
- **REMOVIDO** `usage.getQuota(metric)` — endpoint `/usage/quota` nunca existiu no Core. `usage.report()` devolve `{value, limit, remaining}` inline em todo POST.
- Paths corrigidos:
  - `/sdk/v1/usage/record` → `/api/sdk/v1/usage/record` (canonical)
  - `/sdk/v1/entitlements` → `/api/sdk/v1/entitlements` (canonical)
- `usage.report()` agora envia `Idempotency-Key: <uuid v4>` header automaticamente.
- `usage.check()` retorna novo field `behavior: "readonly" | null` (inferido de `reason === "limit_exceeded"`).

#### 2. `webhooks` namespace — `productId` obrigatório (CRITICAL-SDK-03)

- `RegisterWebhookInput.productId` adicionado. Default resolve do `config.productId`; lança `validation_failed` se ambos ausentes.
- `list(options?)`, `unregister(id, options?)`, `test(id, options?)` agora aceitam `{ productId }` (Core exige query param em todos os verbos).
- `webhooks.test()` envia `Idempotency-Key` header.

#### 3. `notifications` namespace — `productId` obrigatório (CRITICAL-SDK-04)

- `SendNotificationInput.productId` adicionado (Core valida via Zod `SendNotificationSchema`).
- `ListNotificationsOptions.productId` adicionado.
- `NotificationScopeOptions` novo type para `markRead`/`dismiss`.
- `notifications.send()` envia `Idempotency-Key` header.

#### 4. `support` namespace — endpoint canonical (CRITICAL-SDK-02)

- Endpoint mudou de `/api/v1/products/{slug}/tickets` (vaporware — **nunca existiu no Core**) para `/api/sdk/v1/support/tickets` (canonical SDK path).
- `productId` obrigatório (default `config.productId`).
- `createTicket()` envia `Idempotency-Key` header.
- `listMyTickets(options?)` aceita `{ productSlug }` override.
- **Pré-requisito Core**: endpoint `/api/sdk/v1/support/tickets` precisa ser construído (vide `docs/CORE_CHANGES_REQUIRED.md` §1).

#### 5. `verifyWebhookSignature` é async + Universal (HIGH-SDK-03)

- **Antes (2.x)**: `(payload, sig, secret) => boolean` — usava `require('node:crypto')` que quebrava em bundle browser/Edge ESM puro. README anunciava "Universal" mas mentia.
- **Agora (3.0)**: `(payload, sig, secret) => Promise<boolean>` via `WebCrypto.subtle.sign` HMAC-SHA-256 + constant-time XOR compare.
- Funciona em Node ≥20, browsers modernos, Vercel Edge Functions, Cloudflare Workers.
- Lança `NeetruError('runtime_unsupported')` se WebCrypto ausente (distingue "ambiente não suporta" de "assinatura inválida").

#### 6. `telemetry.event` / `telemetry.track` path canonical (CRITICAL-SDK-05)

- `POST /api/v1/sdk/telemetry/event` → `POST /api/sdk/v1/telemetry/event` (canonical SDK pattern).
- Pré-requisito Core: alias/handler para o canonical path (vide `docs/CORE_CHANGES_REQUIRED.md` §3).

#### 7. `baseUrl` allowlist fail-closed (HIGH-SDK-05)

- `createNeetruClient` agora chama `validateBaseUrl` antes de aceitar `config.baseUrl`.
- Allowlist padrão: HTTPS `*.neetru.com` + `localhost`/`127.0.0.1`/`*.test`/`*.localhost`.
- `NeetruClientConfig.allowedHosts: string[]` novo — adicione hosts custom (suffix match).
- Bypass: `NEETRU_ALLOW_INSECURE_BASEURL=1` (env) ou `globalThis.NEETRU_ALLOW_INSECURE_BASEURL='1'`. Warning no console quando ativo.
- Cenário ataque fechado: dev configura `NEETRU_BASE_URL=http://attacker.example/` no `.env`, agora lança `invalid_config` em vez de vazar Bearer.

#### 8. Retries default por método (HIGH-SDK-01)

- **GET/HEAD**: 2 retries default (= 3 tentativas) — idempotente, seguro.
- **POST/PUT/PATCH/DELETE**: **0 retries default** — mutations podem duplicar side effects (ex: `FieldValue.increment` no `usage/record`).
- Caller pode override: `httpRequest(config, { method: 'POST', ..., retries: 2 })`.
- Combinado com `Idempotency-Key` automático em mutations: dup-write rate → zero.

#### 9. Cleanup de stubs deprecated

- `initNeetru` (deprecated desde v0.2, no-op em v2.0) **definitivamente removido**.
- `NeetruConfig` type alias **removido** (use `NeetruClientConfig`).

#### 10. `NeetruErrorCode` ganhou `runtime_unsupported`

- 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).

### ADDED

- `src/idempotency.ts` — `generateIdempotencyKey()` via `crypto.randomUUID()` (Node ≥20, browsers, Edge).
- `src/base-url-allowlist.ts` — `validateBaseUrl(url, allowedHosts)` fail-closed.
- `HttpRequestOptions.idempotencyKey: boolean | string` — injeta header automaticamente em todo POST de mutation.
- `client.usage.check().behavior: 'readonly' | null` — sinal para `<EntitlementGate mode='readonly'>` decidir UI.
- `NeetruClientConfig.allowedHosts` — extension point para hosts custom (staging/proxies).
- `MockUsage.__getReports()` substitui `__getRecords()` (legacy alias mantido).
- Test suite: `src/__tests__/base-url-allowlist.test.ts` (16 tests), `src/__tests__/idempotency.test.ts` (9 tests).
- `docs/CORE_CHANGES_REQUIRED.md` — catálogo de 5 endpoints Core a criar/modificar para 3.0 funcionar end-to-end.

### FIXED

- `<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).
- README v2.x prometia "Universal: browser/Node/Edge" mas `verifyWebhookSignature` só funcionava em Node. 3.0 cumpre.

### Migration guide v2.x → v3.0

```ts
// ❌ Antes (v2.x)
await client.usage.track('api_call', { route: '/x' });
const quota = await client.usage.getQuota('api_calls');

await client.webhooks.register({
  url: 'https://x.com/h',
  events: ['subscription.activated'],
});

await client.notifications.send({
  userId: 'u1',
  kind: 'order.received',
  title: 'Pedido',
});

await client.support.createTicket({
  subject: 'help',
  message: 'x',
});

// ✅ Depois (v3.0)
const client = createNeetruClient({
  apiKey: 'nrt_...',
  productId: 'gestovendas',  // 🆕 default p/ todos os namespaces
  tenantId: 't-acme',         // 🆕 default p/ usage
});

const result = await client.usage.report('api_call', 1);
// result.value, result.limit, result.remaining inline — não precisa getQuota separado.

const allowed = await client.usage.check('api_call');
// allowed.behavior === 'readonly' quando over limit (use em <EntitlementGate>)

await client.webhooks.register({
  // productId default config.productId, ou explícito:
  url: 'https://x.com/h',
  events: ['subscription.activated'],
});

await client.notifications.send({
  userId: 'u1',
  kind: 'order.received',
  title: 'Pedido',
});

await client.support.createTicket({
  subject: 'help',
  message: 'x',
});
```

### Verificação de assinatura de webhook — agora async

```ts
// ❌ Antes (v2.x) — sync, quebrava em Edge
import { verifyWebhookSignature } from '@neetru/sdk/webhooks';
const ok = verifyWebhookSignature(rawPayload, sigHeader, secret);

// ✅ Depois (v3.0) — async, Universal
const ok = await verifyWebhookSignature(rawPayload, sigHeader, secret);
```

### Endpoints Core que precisam ser criados/modificados antes do publish 3.0

Vide `docs/CORE_CHANGES_REQUIRED.md` completo:

1. **CRITICAL** — `POST/GET /api/sdk/v1/support/tickets` (customer-facing) — 3-4h.
2. **HIGH** — Idempotency-Key dedup em endpoints de mutation — 4-6h.
3. **HIGH** — `POST /api/sdk/v1/telemetry/event` (canonical) — 1-2h ou alias middleware.
4. **LOW** — `behavior` field em `/api/sdk/v1/entitlements` response — 30min.
5. **LOW** — Verificar envelope `/api/sdk/v1/datastore/{collection}` — 30min check.

### Tests

- **719 tests passing** (era 668 em 2.3.6). Tsc clean. Build clean.
- Contract tests reescritos para validar paths canonical + body shapes + headers (Idempotency-Key UUID v4 regex match).

## [2.3.6] - 2026-05-26 — **fix(bundle): webpackIgnore em TODOS os dynamic imports node-only — bug_168368ae [CRITICAL]**

### Corrigido

- **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.**
  Resultado: webpack do consumer Next.js seguia o module graph daqueles paths e ainda tentava bundle `pg` → `Module not found: tls/fs` no client.
  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`.

### Testes (antirregressão atualizado)

- **`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.

## [2.3.5] - 2026-05-26 — **fix(bundle): node deps fora do browser entry — bug_0f836014 [CRITICAL]**

### Corrigido

- **bug_0f836014055747858adfa8ab0e8cbb47 (CRITICAL) — SDK 2.3.4 publicou com `require('pg')` + `require('drizzle-orm/node-postgres')` literais no `dist/index.mjs`.**
  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`.
  Fix: dynamic import com `/* webpackIgnore: true */` em `createSqlClientFromConfig`. Instrui webpack a NÃO processar — runtime Node resolve. `sql()` é server-only por contrato.
  Resultado: `dist/index.mjs` zero imports literais de pg/drizzle/fs/node:*. Verified via teste antirregressão novo.

### Testes (antirregressão)

- **`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.

## [2.3.4] - 2026-05-26 — **fix(mocks): MockDb.sql() migra pra pglite + pg-mem fallback — bug_6bae8674 [HIGH]**

### Corrigido

- **bug_6bae8674461f4600809e2f9de13c6043 (HIGH) — fix anterior (2.3.3) teve efeito INVERSO em `MockDb.sql()`.**
  O fix de 2.3.3 injetava `getTypeParser` em `pool.types` para resolver o erro "Not supported: getTypeParser is not supported".
  Porém `pg-mem` tem um guard interno: `if (query.types?.getTypeParser) throw NotSupported`.
  `drizzle-orm/node-postgres/session.js:113` passa `rawQuery.types = pool.types` em TODA query — ao adicionar
  `getTypeParser` ao pool, drizzle passou-o para o pg-mem que o rejeitou imediatamente.
  Efeito líquido: qualquer `INSERT`/`SELECT`/`UPDATE` continuava quebrando com o mesmo erro.

  **Fix 2.3.4 — duas soluções em cascata:**

  1. **Solução A (preferida): `@electric-sql/pglite` + `drizzle-orm/pglite`** — WASM-Postgres nativo
     sem adapter pg-mem. Sem conflito `getTypeParser`, sem monkey-patch. `PGlite.create()` retorna
     instância pronta, DDL via `pglite.query(ddl)`, ORM via `drizzle(pglite, { schema })`. Cast
     `PgliteDatabase → NodePgDatabase` via `unknown` (interfaces Drizzle compatíveis em superfície
     mas com tipos de resultado internos distintos).

  2. **Solução B (fallback pg-mem)**: strip interceptor — intercepta `pool.query` e deleta
     `types.getTypeParser` do objeto de query ANTES do pg-mem ver. Guard não dispara, query passa
     limpa. Compatível com consumers que só têm pg-mem nas devDeps.

  **`@electric-sql/pglite` adicionado como `peerDependency` optional** (`^0.4.0`) + `devDependency`
  interno (`^0.4.6`).

  **8 novos testes** em `src/__tests__/mocks-sql-pglite.test.ts`: criação sem erro, INSERT sem throw,
  SELECT após INSERT, UPDATE, múltiplos INSERTs, sem initSql, close idempotente, transaction().
  Todos passando.

  **655 testes totais** (era 647 em 2.3.3). `tsc --noEmit` clean. Build clean.

## [2.3.3] - 2026-05-26 — **fix(mocks): pool.types.getTypeParser identity parser — bug_44bcc99f [HIGH]**

### Corrigido

- **bug_44bcc99f9d3140d78abd8f7810d1afa4 (HIGH) — `MockDb.sql()` queries Drizzle falhavam em runtime.**
  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.
  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).
  3 linhas. Non-breaking. Tests E2E Drizzle in-memory destravados.

## [2.3.2] - 2026-05-26 — **fix(mocks): MockDb.sql() implementado in-memory via pg-mem — bug_a7f2ae25 [HIGH]**

### Corrigido

- **bug_a7f2ae25fda54129b52b6dd55aaa6050 (HIGH) — `MockDb.sql()` lançava `db_unavailable` em vez de implementar Drizzle in-memory.**
  Bloqueava vitest server-side dos consumers (pdv-agiliza Fase 4 migration). Política do projeto proíbe workaround instalar `pg-mem` no consumer.
  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).
  `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`.
  **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.

## [2.3.1] - 2026-05-26 — **fix(deps): drizzle-orm + pg como peerDependencies — bug_42ae9f80 [CRITICAL]**

### Corrigido

- **bug_42ae9f80b2884bd583c5246771f7730c (CRITICAL) — `drizzle-orm` em `dependencies` causava type collision com versão do consumer.**
  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`.
  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.
  **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).

## [2.3.0] - 2026-05-25 — **feat(db-config): renomeia engine→transport com deprecation — bug_ed9da7fd [HIGH]**

Fix do bug `bug_ed9da7fd11194eac87f0eb9c75e5fad9` (HIGH) — colisão de terminologia: `engine` no SDK
configurava o transporte SDK↔Core (REST/Firestore/WebSocket), mas `engine` no admin database (CLI
`neetru admin database create --engine=...`) configura o storage engine (firestore-instance,
cloud-sql-postgres, vm-postgres-single…). Devs colocavam `firestore-instance` no SDK e quebravam.

### Added

- **`NeetruDbTransport`** — novo tipo público canônico para o campo de transporte SDK↔Core.
  Valores: `'rest' | 'firestore' | 'nosql-vm'` (idênticos aos de `NeetruDbEngine`).

- **`NeetruDbOptions.transport`** — novo campo canônico que substitui `engine`.
  ```ts
  // v2.3+ (preferido):
  createNeetruClient({ db: { transport: 'rest' } });
  createNeetruClient({ db: { transport: 'firestore' } });
  createNeetruClient({ db: { transport: 'nosql-vm', realtimeGatewayUrl: '...' } });
  ```

### Deprecated

- **`NeetruDbOptions.engine`** — depreciado em v2.3, será removido em v3.0.
  Continua funcionando: se `transport` estiver ausente e `engine` presente, o valor de `engine`
  é usado como transport com `console.warn` de deprecação.

  ```ts
  // v2.x (depreciado — emite console.warn):
  createNeetruClient({ db: { engine: 'firestore' } });
  // ⚠ [neetru/sdk] NeetruDbOptions.engine está depreciado...
  ```

- **`NeetruDbEngine`** — depreciado em v2.3, será removido em v3.0.
  Agora é alias de `NeetruDbTransport`. Use `NeetruDbTransport` em código novo.

### Behavior

- `transport` e `engine` podem coexistir: `transport` sempre prevalece, sem warn.
- Sem `transport` nem `engine`: default `'rest'`, sem warn (comportamento idêntico ao v2.x).
- A relação 1:N entre storage engines e transports é documentada no JSDoc de `NeetruDbTransport`:
  qualquer storage engine (cloud-sql-postgres, vm-postgres-single…) pode ser acessado com
  `transport: 'rest'`; `transport: 'firestore'` e `transport: 'nosql-vm'` são para bancos
  documentais com realtime nativo.

### Tests

- 5 testes novos em `src/__tests__/db-real.test.ts`:
  `transport=firestore`, `transport=nosql-vm`, `transport=rest`, colisão `transport+engine`
  (transport prevalece, sem warn), default sem campos (sem warn).
- Testes de regressão: `engine=firestore` e `engine=nosql-vm` legados ainda passam + emitem warn.

## [2.2.1] - 2026-05-25 — **fix(offline-docs): fallback MemoryStore em Node — bug_ba287a CRITICAL**

Fix do bug `bug_ba287aefdf6c4327b026d588a239ad06` (CRITICAL) — `client.db.collection().set/add/update`
lançava `ReferenceError: indexedDB is not defined` em Node.js (reportado por pdv-agiliza prod).

### Fixed

- **`createOfflineDocumentsNamespace` — auto-detect Node runtime e usa `MemoryStore`**.
  A função agora verifica `typeof indexedDB === 'undefined' || typeof window === 'undefined'`
  antes de instanciar `LocalStore` (que depende de `idb` / IndexedDB). Em Node/Bun/Edge Workers
  sem IDB, cai automaticamente em `MemoryStore` — implementação in-process com a mesma API,
  sem nenhuma flag explícita necessária. Ops de documentos (`set/add/update/remove/get/list`,
  `batch`, `onSnapshot`, `onDoc`) funcionam normalmente em Node; dados não persistem entre
  reinicializações do processo (comportamento esperado em SSR/server-side).

### Added

- **`src/db/offline/memory-store.ts`** — `MemoryStore`: implementação in-memory do contrato
  público de `LocalStore`. Cobre todas as 5 stores (documents, mutations, query\_cache,
  sync\_meta, conflict\_log) com Maps/Arrays em memória.

### Tests

- **`src/__tests__/offline-docs-node-compat.test.ts`** — 5 casos de regressão:
  verifica que a stack offline não lança `ReferenceError` em ambiente sem IDB,
  e que `set/add/update` + `get` roundtrips funcionam in-memory.

## [2.2.0] - 2026-05-25 — **auth.verifyToken — validação server-side de id_token via JWKS [feature]**

Fix do bug `bug_a7ef1702680044e3994b74bdccecb2ab` — resolve P0-1 audit security pdv-agiliza.

### Added

- **`client.auth.verifyToken(token, options?): Promise<NeetruUser>`** — valida um id_token JWT
  emitido por `auth.neetru.com` server-side, sem round-trip de rede por request. Usa JWKS local
  cacheado (default TTL 1h, configurável via `options.jwksCacheTtlMs`).

  Erros tipados novos (`NeetruErrorCode`):
  - `token_expired` — `exp` no passado
  - `token_invalid_signature` — assinatura não verificada pela JWKS
  - `token_invalid_audience` — `aud` não bate com o `clientId` extraído do `apiKey`
  - `token_invalid_issuer` — `iss` não é `https://auth.neetru.com`

  Dependência nova: `jose` (Vercel/Cloudflare-friendly, sem Node nativo).

- **`MockAuth.verifyToken(token)`** — decodifica JWT sem verificar assinatura (útil em testes).
  Aceita `__mockVerifyToken(fn)` pra injetar falhas determinísticas.

- **`VerifyTokenOptions`** — exportado em `index.ts` pra consumers tiparem as options.

### Usage

```ts
// Next.js route handler
import { createNeetruClient } from '@neetru/sdk';
const client = createNeetruClient({ apiKey: 'nrt_a1b2c3_...', env: 'prod' });

export async function POST(req: Request) {
  const token = req.cookies.get('neetru-session')?.value ?? '';
  try {
    const user = await client.auth.verifyToken(token);
    // user.uid, user.email, user.isStaff, user.tenantId disponíveis
  } catch (err) {
    return new Response('Unauthorized', { status: 401 });
  }
}
```

## [Unreleased]

### Planned (post-2.2)
- size-limit budget no CI (<12KB gz core com db namespace).
- CDN distribution (`cdn.neetru.com/sdk/v2/`).
- Endpoint batch `/api/v1/sdk/telemetry/batch` (substitui drenagem N×1 do `track()` por 1 request).

## [2.1.1] - 2026-05-25 — **DbDocRef identity + DbBatchOp discriminated union [patch]**

Dois fixes LOW reportados por pdv-agiliza (tickets `bug_97b3f0488d1c49cf8c5f94e5d544289b` + `bug_6aab9adc182640218652a65cdcb71b63`).
Sem breaking changes — todos os usos de `doc()` existentes continuam funcionando.

### Fixed

- **`DbDocRef.id / .path / .collection`** (fix #13, ticket `bug_97b3f0488d1c49cf8c5f94e5d544289b`):
  `DbDocRef` agora expõe três propriedades readonly de identidade:
  - `id: string` — ID do documento dentro da coleção.
  - `path: string` — Caminho completo `"colecao/docId"`.
  - `collection: string` — Nome da coleção parent.
  Análogo ao `DocumentReference.id/.path` do Firestore Web SDK. Elimina a necessidade
  de adaptadores externos que reattachavam `collection + id` manualmente.
  Implementado em `DbCollectionRefImpl.doc()` (offline), `createLazyCollectionRef.doc()`
  (client-db) e `MockDb.doc()` (mocks).

- **`DbBatchOp` discriminated union** (fix #14, ticket `bug_6aab9adc182640218652a65cdcb71b63`):
  `DbBatchOp` foi convertido de `interface` opaca (campo `op: 'add'|'set'|'update'|'remove'`
  com `id?`/`data?` opcionais) para **discriminated union** pelo campo `kind`:
  ```ts
  type DbBatchOp<T = Record<string, unknown>> =
    | { kind: 'add';    data: Omit<T, 'id'> }
    | { kind: 'set';    id: string; data: Omit<T, 'id'> }
    | { kind: 'update'; id: string; data: Partial<Omit<T, 'id'>> }
    | { kind: 'remove'; id: string };
  ```
  O `.d.ts` público agora expõe claramente quais campos são obrigatórios por variante.
  O campo `collection` foi removido da operação (sempre opera na coleção do `DbCollectionRef`).

### Migration (2.0.0 → 2.1.1)

Callers que usavam `batch()` devem trocar `op` por `kind` e remover o campo `collection`:

```ts
// antes (2.0.0)
col.batch([
  { op: 'add',    collection: 'orders', data: { total: 10 } },
  { op: 'set',    collection: 'orders', id: 'x', data: { total: 20 } },
  { op: 'update', collection: 'orders', id: 'y', data: { total: 30 } },
  { op: 'remove', collection: 'orders', id: 'z' },
]);

// depois (2.1.1)
col.batch([
  { kind: 'add',    data: { total: 10 } },
  { kind: 'set',    id: 'x', data: { total: 20 } },
  { kind: 'update', id: 'y', data: { total: 30 } },
  { kind: 'remove', id: 'z' },
]);
```

## [2.0.0] - 2026-05-22 — **client.db NeetruDb (Documentos engine-aware + SQL) [M2]**

Major bump — breaking changes no namespace `db`. Liga todos os building blocks M2 na superfície pública.

### Breaking Changes

- **`client.db`** troca de `DbNamespace` (v0.3 REST simples) para `NeetruDb` (v2.0 offline-first engine-aware).
  - `collection().list()` agora retorna `DbListResult<T>` (com `docs`, `nextCursor`, `fromCache`, `stale`, `hasPendingWrites`, `changes`) em vez de `T[]`.
  - `collection().get(id)` retorna `DbGetResult<T> | null` em vez de `T | null`.
  - `db_unavailable` não significa mais "sem rede" — só ciclo de vida do banco. Offline é transparente.
  - Novos métodos obrigatórios na superfície: `sql()`, `syncState`, `flush()`, `clearCache()`, `getConflicts()`, `onSyncStateChanged()`, `batch()`, `onDoc()`, `onSnapshot()`, `doc()`.
- **`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.
- **`initNeetru`** — mantido como stub funcional mas não mapeia mais `apiUrl → baseUrl`. Remover em v3.0.
- **`MockDb`** foi atualizado para implementar `NeetruDb` (v2.0) em vez de `DbNamespace` (v0.3).

### Added

- **`client.db.collection<T>(name)`** — retorna `DbCollectionRef<T>` offline-first, engine-aware. Funciona sem rede. Sync automático quando conectado.
  - CRUD: `add`, `set`, `update`, `remove`, `get`, `list` (com query, cursor, orderBy, where, limit).
  - Batch: `batch(ops)` — operações atômicas (client-side).
  - Realtime: `onSnapshot(query, cb)`, `onDoc(id, cb)` — subscriptions permanentes, delivery imediato do cache.
  - Doc ref: `doc(id).get()`, `.set()`, `.update()`, `.remove()`, `.onSnapshot()`.
- **`client.db.sql<TSchema>(schema)`** — lease SQL. Retorna `NeetruSqlClient<TSchema>` com `orm` (Drizzle `NodePgDatabase`) + `transaction()` + `close()`.
- **`client.db.syncState`** — estado de sincronização observável (`status`, `pendingWrites`, `lastSyncedAt`, `isLeaderTab`).
- **`client.db.onSyncStateChanged(cb)`** — subscrição de mudanças de sync state.
- **`client.db.flush()`** — força ciclo de sync imediato.
- **`client.db.clearCache(collections?)`** — limpa o cache local (IndexedDB).
- **`client.db.getConflicts(collection?)`** — lista conflitos LWW pendentes.
- **Engine detection** via `NeetruDbOptions.engine`: `'firestore'` | `'nosql-vm'` | `'rest'` (default MVP).
- **Subpath `@neetru/sdk/db/react`** — hooks React: `useCollection`, `useDoc` (exporta via `dist/db-react.{mjs,cjs}`).
- **Novos tipos públicos exportados**: `NeetruDb`, `NeetruDbEngine`, `NeetruDbOptions`, `NeetruSqlClient`, `DbSqlLease`, `DbCollectionRef`, `DbDocRef`, `DbQuery`, `DbDoc`, `DbListResult`, `DbGetResult`, `DbBatchOp`, `DbChangeType`, `NeetruDbError`, `NeetruDbErrorCode`.
- **`createNeetruDb(config, opts)`** — factory async (para uso fora do `createNeetruClient`).
- **`createNeetruDbSync(config, opts)`** — factory sync com lazy init IndexedDB (usada internamente em `createNeetruClient`).

### Changed

- **`NeetruClientConfig.db`** — aceita `NeetruDbOptions` (engine, dbId, collections, dbName, etc.).
