@neetru/sdk 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/ai.d.cts +1 -1
  3. package/dist/ai.d.ts +1 -1
  4. package/dist/auth.cjs +179 -2
  5. package/dist/auth.cjs.map +1 -1
  6. package/dist/auth.d.cts +1 -1
  7. package/dist/auth.d.ts +1 -1
  8. package/dist/auth.mjs +179 -2
  9. package/dist/auth.mjs.map +1 -1
  10. package/dist/catalog.d.cts +1 -1
  11. package/dist/catalog.d.ts +1 -1
  12. package/dist/checkout.d.cts +1 -1
  13. package/dist/checkout.d.ts +1 -1
  14. package/dist/db-react.d.cts +1 -1
  15. package/dist/db-react.d.ts +1 -1
  16. package/dist/db.cjs +17 -0
  17. package/dist/db.cjs.map +1 -1
  18. package/dist/db.d.cts +1 -1
  19. package/dist/db.d.ts +1 -1
  20. package/dist/db.mjs +17 -1
  21. package/dist/db.mjs.map +1 -1
  22. package/dist/entitlements.d.cts +1 -1
  23. package/dist/entitlements.d.ts +1 -1
  24. package/dist/firestore-compat.d.cts +1 -1
  25. package/dist/firestore-compat.d.ts +1 -1
  26. package/dist/index.cjs +180 -3
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.mjs +180 -3
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/mocks.cjs +68 -1
  33. package/dist/mocks.cjs.map +1 -1
  34. package/dist/mocks.d.cts +17 -1
  35. package/dist/mocks.d.ts +17 -1
  36. package/dist/mocks.mjs +68 -1
  37. package/dist/mocks.mjs.map +1 -1
  38. package/dist/notifications.d.cts +1 -1
  39. package/dist/notifications.d.ts +1 -1
  40. package/dist/react.d.cts +1 -1
  41. package/dist/react.d.ts +1 -1
  42. package/dist/support.cjs +98 -1
  43. package/dist/support.cjs.map +1 -1
  44. package/dist/support.d.cts +1 -1
  45. package/dist/support.d.ts +1 -1
  46. package/dist/support.mjs +98 -1
  47. package/dist/support.mjs.map +1 -1
  48. package/dist/telemetry.d.cts +1 -1
  49. package/dist/telemetry.d.ts +1 -1
  50. package/dist/{types-DQplLXIk.d.cts → types-BoFSCHF_.d.cts} +117 -2
  51. package/dist/{types-DslphKIM.d.ts → types-DdJyqBy-.d.ts} +117 -2
  52. package/dist/usage.d.cts +1 -1
  53. package/dist/usage.d.ts +1 -1
  54. package/dist/webhooks.d.cts +1 -1
  55. package/dist/webhooks.d.ts +1 -1
  56. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to `@neetru/sdk` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.4.0] - 2026-08-15 — **feat(support): superfície "Product Feedback" — markResolvedSeen/recordSatisfactionAttempt/submitSatisfaction/contestTicket**
9
+
10
+ Release **minor** (API pública aditiva no namespace `client.support`: 4 métodos novos + 9 campos novos em `SupportTicket` + 3 opcionais em `CreateTicketInput`).
11
+
12
+ ### Contexto
13
+
14
+ Camada de dados do fluxo "Product Feedback" (report → resolvido → satisfação → contestação), contrato compartilhado entre Core (endpoints+schema), SDK (`@neetru/sdk` namespace `support`) e `@neetru/product-feedback` (neetru-libs). Ver `CONTRATO_PRODUCT_FEEDBACK_2026-08-15.md`. Decisões do owner: rating CSAT 1-5 (`number | null`); contestação incluída no v1 (4 estados).
15
+
16
+ ### Adicionado
17
+
18
+ - **`createTicket(input)`**: aceita opcionais `pageUrl?: string`, `screenshotBase64?: string`, `consoleLogs?: string` — propagados no body do POST quando presentes.
19
+ - **`SupportTicket`**: campos novos (todos nullable, mapeados defensivamente em `toTicket()`) — `pageUrl`, `screenshotUrl` (= `attachments[0] ?? null`), `resolvedAt`, `resolvedNotifiedAt`, `satisfactionAttempts` (default `0`), `lastSatisfactionAttemptAt`, `satisfiedAt`, `rating`, `satisfactionComment`.
20
+ - **`markResolvedSeen(ticketId)`**: `POST /api/sdk/v1/support/tickets/{id}/resolved-seen` — marca a notificação de resolução como vista (idempotente no Core).
21
+ - **`recordSatisfactionAttempt(ticketId)`**: `POST .../satisfaction-attempt` — registra exibição do modal de satisfação (regra-chave do fluxo: exibir já conta como tentativa, evita reprompt infinito).
22
+ - **`submitSatisfaction({ ticketId, rating, comment?, skipped })`**: `POST .../satisfaction` — envia (ou pula) a avaliação CSAT.
23
+ - **`contestTicket(ticketId, { comment? })`**: `POST .../contest` — reabre um ticket resolvido e zera o ciclo de satisfação/notificação.
24
+ - Validação client-side antes de cada request: `ticketId` non-empty (`validation_failed`); em `submitSatisfaction`, `rating` inteiro 1-5 quando não-`null`; `comment` ≤ 2000 chars.
25
+ - `MockSupport` espelha os 4 métodos novos in-memory (FSM coerente com o contrato) — modo dev/test sem fetch real.
26
+ - Testes novos cobrindo cada método (path, `requireAuth`, validação client-side), propagação dos campos novos em `createTicket`, e mapeamento defensivo em `toTicket()`.
27
+
28
+ ### Corrigido (incluído neste minor — `bug_58811a43`)
29
+
30
+ - **JSDoc de `NeetruDbOptions.dbId`** (`src/db/client-db.ts`): reescrito pra descrever o comportamento real (nomeia o cache offline LOCAL + filtro de subscription `nosql-vm`); explicita o que NÃO faz (não escopa as escritas REST — o escopo é `x-neetru-tenant`; não é usado pelo lease SQL, que usa `SqlOptions.database`). O texto antigo ("necessário para scoping das coleções") enganava.
31
+ - **Aviso dev-only one-time** `warnMissingDbIdOnce` (`createNeetruDb`): `console.warn` único em contexto browser + fora de `prod` quando `dbId` está ausente. **Sem mudança de comportamento de wire.**
32
+
8
33
  ## [3.3.0] - 2026-08-09 — **feat(db-sql): motor de ambientes "suspenso" — `db_suspended` não-retryável + lease inicial LAZY**
9
34
 
10
35
  Release **minor** (API pública aditiva: novo `NeetruDbErrorCode` `db_suspended` + `NeetruError.reason`; `client.db.sql()` deixa de explodir o import quando o ambiente está fora no boot).
package/dist/ai.d.cts CHANGED
@@ -1,3 +1,3 @@
1
- export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, aH as createAiNamespace } from './types-DQplLXIk.cjs';
1
+ export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, aI as createAiNamespace } from './types-BoFSCHF_.cjs';
2
2
  import 'drizzle-orm/node-postgres';
3
3
  import './errors.cjs';
package/dist/ai.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, aH as createAiNamespace } from './types-DslphKIM.js';
1
+ export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, aI as createAiNamespace } from './types-DdJyqBy-.js';
2
2
  import 'drizzle-orm/node-postgres';
3
3
  import './errors.js';
package/dist/auth.cjs CHANGED
@@ -1584,6 +1584,7 @@ function toTicket(raw) {
1584
1584
  if (typeof r.id !== "string") {
1585
1585
  throw new NeetruError("invalid_response", "Ticket missing id");
1586
1586
  }
1587
+ const firstAttachment = Array.isArray(r.attachments) && typeof r.attachments[0] === "string" ? r.attachments[0] : void 0;
1587
1588
  return {
1588
1589
  id: r.id,
1589
1590
  subject: typeof r.subject === "string" ? r.subject : "",
@@ -1592,9 +1593,28 @@ function toTicket(raw) {
1592
1593
  status: VALID_STATUSES.includes(r.status) ? r.status : "open",
1593
1594
  createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
1594
1595
  updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
1595
- productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0
1596
+ productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0,
1597
+ pageUrl: typeof r.pageUrl === "string" ? r.pageUrl : null,
1598
+ screenshotUrl: typeof r.screenshotUrl === "string" ? r.screenshotUrl : firstAttachment ?? null,
1599
+ resolvedAt: typeof r.resolvedAt === "string" ? r.resolvedAt : null,
1600
+ resolvedNotifiedAt: typeof r.resolvedNotifiedAt === "string" ? r.resolvedNotifiedAt : null,
1601
+ satisfactionAttempts: typeof r.satisfactionAttempts === "number" && Number.isFinite(r.satisfactionAttempts) ? r.satisfactionAttempts : 0,
1602
+ lastSatisfactionAttemptAt: typeof r.lastSatisfactionAttemptAt === "string" ? r.lastSatisfactionAttemptAt : null,
1603
+ satisfiedAt: typeof r.satisfiedAt === "string" ? r.satisfiedAt : null,
1604
+ rating: typeof r.rating === "number" && Number.isFinite(r.rating) ? r.rating : null,
1605
+ satisfactionComment: typeof r.satisfactionComment === "string" ? r.satisfactionComment : null
1596
1606
  };
1597
1607
  }
1608
+ function assertTicketId(ticketId) {
1609
+ if (!ticketId || typeof ticketId !== "string") {
1610
+ throw new NeetruError("validation_failed", "ticketId is required");
1611
+ }
1612
+ }
1613
+ function assertValidComment(comment) {
1614
+ if (typeof comment === "string" && comment.length > 2e3) {
1615
+ throw new NeetruError("validation_failed", "comment max 2000 chars");
1616
+ }
1617
+ }
1598
1618
  function resolveProductId(config, ...candidates) {
1599
1619
  for (const c of candidates) {
1600
1620
  if (typeof c === "string" && c.length > 0) {
@@ -1651,6 +1671,15 @@ function createSupportNamespace(config) {
1651
1671
  if (input.metadata && typeof input.metadata === "object") {
1652
1672
  body.metadata = input.metadata;
1653
1673
  }
1674
+ if (typeof input.pageUrl === "string") {
1675
+ body.pageUrl = input.pageUrl;
1676
+ }
1677
+ if (typeof input.screenshotBase64 === "string") {
1678
+ body.screenshotBase64 = input.screenshotBase64;
1679
+ }
1680
+ if (typeof input.consoleLogs === "string") {
1681
+ body.consoleLogs = input.consoleLogs;
1682
+ }
1654
1683
  const raw = await httpRequest(config, {
1655
1684
  method: "POST",
1656
1685
  path: "/api/sdk/v1/support/tickets",
@@ -1676,6 +1705,74 @@ function createSupportNamespace(config) {
1676
1705
  });
1677
1706
  const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
1678
1707
  return list.map(toTicket);
1708
+ },
1709
+ /**
1710
+ * 3.4: marca a notificação de resolução como vista (`resolvedNotifiedAt`,
1711
+ * idempotente no Core). Endpoint canonical:
1712
+ * `POST /api/sdk/v1/support/tickets/{id}/resolved-seen`.
1713
+ */
1714
+ async markResolvedSeen(ticketId) {
1715
+ assertTicketId(ticketId);
1716
+ await httpRequest(config, {
1717
+ method: "POST",
1718
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/resolved-seen`,
1719
+ requireAuth: true
1720
+ });
1721
+ },
1722
+ /**
1723
+ * 3.4: registra uma tentativa de exibição do modal de satisfação
1724
+ * (`satisfactionAttempts += 1`). Chamar no SHOW do modal, não na
1725
+ * resposta. Endpoint canonical:
1726
+ * `POST /api/sdk/v1/support/tickets/{id}/satisfaction-attempt`.
1727
+ */
1728
+ async recordSatisfactionAttempt(ticketId) {
1729
+ assertTicketId(ticketId);
1730
+ await httpRequest(config, {
1731
+ method: "POST",
1732
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/satisfaction-attempt`,
1733
+ requireAuth: true
1734
+ });
1735
+ },
1736
+ /**
1737
+ * 3.4: envia (ou pula) a avaliação de satisfação (CSAT 1-5). Endpoint
1738
+ * canonical: `POST /api/sdk/v1/support/tickets/{id}/satisfaction`.
1739
+ */
1740
+ async submitSatisfaction(input) {
1741
+ if (!input || typeof input !== "object") {
1742
+ throw new NeetruError("validation_failed", "input is required");
1743
+ }
1744
+ assertTicketId(input.ticketId);
1745
+ if (input.rating !== null && input.rating !== void 0) {
1746
+ if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
1747
+ throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
1748
+ }
1749
+ }
1750
+ assertValidComment(input.comment);
1751
+ await httpRequest(config, {
1752
+ method: "POST",
1753
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(input.ticketId)}/satisfaction`,
1754
+ body: {
1755
+ rating: input.rating ?? null,
1756
+ comment: input.comment ?? null,
1757
+ skipped: input.skipped
1758
+ },
1759
+ requireAuth: true
1760
+ });
1761
+ },
1762
+ /**
1763
+ * 3.4: contesta um ticket resolvido — reabre e zera o ciclo de
1764
+ * satisfação/notificação. Endpoint canonical:
1765
+ * `POST /api/sdk/v1/support/tickets/{id}/contest`.
1766
+ */
1767
+ async contestTicket(ticketId, opts) {
1768
+ assertTicketId(ticketId);
1769
+ assertValidComment(opts?.comment);
1770
+ await httpRequest(config, {
1771
+ method: "POST",
1772
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/contest`,
1773
+ body: { comment: opts?.comment ?? void 0 },
1774
+ requireAuth: true
1775
+ });
1679
1776
  }
1680
1777
  };
1681
1778
  }
@@ -5096,7 +5193,20 @@ function buildTicketProvider(config, opts) {
5096
5193
  return ticket;
5097
5194
  };
5098
5195
  }
5196
+ var _hasWarnedMissingDbId = false;
5197
+ function warnMissingDbIdOnce(config, dbOpts) {
5198
+ if (_hasWarnedMissingDbId) return;
5199
+ if (dbOpts.dbId) return;
5200
+ if (config.env === "prod") return;
5201
+ if (typeof window === "undefined") return;
5202
+ if (typeof console === "undefined" || typeof console.warn !== "function") return;
5203
+ _hasWarnedMissingDbId = true;
5204
+ console.warn(
5205
+ "[neetru/sdk] client.db sem `dbId` expl\xEDcito: escritas client-side (add/set/update/remove/list) caem no datastore gen\xE9rico do Core, escopado por tenant/produto (header x-neetru-tenant) \u2014 `dbId` s\xF3 nomeia o cache offline LOCAL (IndexedDB), n\xE3o roteia nem escopa nada no servidor. Veja o JSDoc de NeetruDbOptions.dbId."
5206
+ );
5207
+ }
5099
5208
  function createNeetruDb(config, dbOpts = {}) {
5209
+ warnMissingDbIdOnce(config, dbOpts);
5100
5210
  let resolvedTransport;
5101
5211
  if (dbOpts.transport !== void 0) {
5102
5212
  resolvedTransport = dbOpts.transport;
@@ -6519,7 +6629,18 @@ var MockSupport = class {
6519
6629
  severity: input.severity ?? "normal",
6520
6630
  status: "open",
6521
6631
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6522
- productSlug: input.productSlug
6632
+ productSlug: input.productSlug,
6633
+ // 3.4 (product-feedback): espelha o contrato — campos novos nullable,
6634
+ // populados a partir do input quando fornecidos.
6635
+ pageUrl: typeof input.pageUrl === "string" ? input.pageUrl : null,
6636
+ screenshotUrl: typeof input.screenshotBase64 === "string" ? input.screenshotBase64 : null,
6637
+ resolvedAt: null,
6638
+ resolvedNotifiedAt: null,
6639
+ satisfactionAttempts: 0,
6640
+ lastSatisfactionAttemptAt: null,
6641
+ satisfiedAt: null,
6642
+ rating: null,
6643
+ satisfactionComment: null
6523
6644
  };
6524
6645
  this._tickets.push(ticket);
6525
6646
  return ticket;
@@ -6530,6 +6651,62 @@ var MockSupport = class {
6530
6651
  }
6531
6652
  return [...this._tickets];
6532
6653
  }
6654
+ _find(ticketId) {
6655
+ if (!ticketId || typeof ticketId !== "string") {
6656
+ throw new NeetruError("validation_failed", "ticketId is required");
6657
+ }
6658
+ const ticket = this._tickets.find((t) => t.id === ticketId);
6659
+ if (!ticket) {
6660
+ throw new NeetruError("not_found", `ticket ${ticketId} not found`);
6661
+ }
6662
+ return ticket;
6663
+ }
6664
+ /** 3.4: espelha `markResolvedSeen` — idempotente (só seta se ainda null). */
6665
+ async markResolvedSeen(ticketId) {
6666
+ const ticket = this._find(ticketId);
6667
+ if (ticket.resolvedNotifiedAt === null) {
6668
+ ticket.resolvedNotifiedAt = (/* @__PURE__ */ new Date()).toISOString();
6669
+ }
6670
+ }
6671
+ /** 3.4: espelha `recordSatisfactionAttempt` — incrementa contador. */
6672
+ async recordSatisfactionAttempt(ticketId) {
6673
+ const ticket = this._find(ticketId);
6674
+ ticket.satisfactionAttempts += 1;
6675
+ ticket.lastSatisfactionAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
6676
+ }
6677
+ /** 3.4: espelha `submitSatisfaction` — terminal, seta `satisfiedAt`. */
6678
+ async submitSatisfaction(input) {
6679
+ if (!input || typeof input !== "object") {
6680
+ throw new NeetruError("validation_failed", "input is required");
6681
+ }
6682
+ if (input.rating !== null && input.rating !== void 0) {
6683
+ if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
6684
+ throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
6685
+ }
6686
+ }
6687
+ if (typeof input.comment === "string" && input.comment.length > 2e3) {
6688
+ throw new NeetruError("validation_failed", "comment max 2000 chars");
6689
+ }
6690
+ const ticket = this._find(input.ticketId);
6691
+ ticket.satisfiedAt = (/* @__PURE__ */ new Date()).toISOString();
6692
+ ticket.rating = input.rating ?? null;
6693
+ ticket.satisfactionComment = input.comment ?? null;
6694
+ }
6695
+ /** 3.4: espelha `contestTicket` — reabre e zera o ciclo de satisfação. */
6696
+ async contestTicket(ticketId, opts) {
6697
+ if (typeof opts?.comment === "string" && opts.comment.length > 2e3) {
6698
+ throw new NeetruError("validation_failed", "comment max 2000 chars");
6699
+ }
6700
+ const ticket = this._find(ticketId);
6701
+ ticket.status = "open";
6702
+ ticket.resolvedAt = null;
6703
+ ticket.resolvedNotifiedAt = null;
6704
+ ticket.satisfactionAttempts = 0;
6705
+ ticket.lastSatisfactionAttemptAt = null;
6706
+ ticket.satisfiedAt = null;
6707
+ ticket.rating = null;
6708
+ ticket.satisfactionComment = null;
6709
+ }
6533
6710
  /** Test helper. */
6534
6711
  __reset() {
6535
6712
  this._tickets = [];