@neetru/sdk 3.3.0 → 3.4.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 (56) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/ai.d.cts +1 -1
  3. package/dist/ai.d.ts +1 -1
  4. package/dist/auth.cjs +365 -25
  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 +365 -25
  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 +202 -22
  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 +202 -23
  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 +366 -26
  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 +366 -26
  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-DslphKIM.d.ts → types-BlOLDMR0.d.ts} +192 -4
  51. package/dist/{types-DQplLXIk.d.cts → types-D-QdG502.d.cts} +192 -4
  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/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createNeetruClient } from './auth.cjs';
2
2
  export { NeetruError, NeetruErrorCode } from './errors.cjs';
3
- 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, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, aq as TelemetryEventAck, ar as TelemetryEventInput, av as UsageEventInput, aw as UsageNamespace, ax as UsageQuota, ay as VerifyTokenOptions, az as VerifyWebhookRequestOptions, aA as VerifyWebhookRequestResult, aB as WebhookEndpoint, aC as WebhookEvent, aD as WebhookScopeOptions, aE as WebhookTestResult, aF as WebhooksNamespace, aG as WithFieldSentinels, aH as createAiNamespace, aI as createCheckoutNamespace, aJ as createNeetruDb, aK as createNotificationsNamespace, aM as createWebhooksNamespace, aO as increment, aP as isFieldSentinel, aQ as isIncrementSentinel, aR as isServerTimestampSentinel, aS as serverTimestamp, aT as verifyWebhookRequest, aU as verifyWebhookSignature } from './types-DQplLXIk.cjs';
3
+ 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, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, ar as TelemetryEventAck, as as TelemetryEventInput, aw as UsageEventInput, ax as UsageNamespace, ay as UsageQuota, az as VerifyTokenOptions, aA as VerifyWebhookRequestOptions, aB as VerifyWebhookRequestResult, aC as WebhookEndpoint, aD as WebhookEvent, aE as WebhookScopeOptions, aF as WebhookTestResult, aG as WebhooksNamespace, aH as WithFieldSentinels, aJ as createAiNamespace, aK as createCheckoutNamespace, aL as createNeetruDb, aM as createNotificationsNamespace, aO as createWebhooksNamespace, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp, aV as verifyWebhookRequest, aW as verifyWebhookSignature } from './types-D-QdG502.cjs';
4
4
  export { DEV_FIXTURE_USER, MockAi, MockAuth, MockAuthOptions, MockDb, MockEntitlements, MockSupport, MockUsage } from './mocks.cjs';
5
5
  import 'drizzle-orm/node-postgres';
6
6
 
@@ -42,6 +42,6 @@ declare function checkCoreVersionCompat(coreVersion: string): void;
42
42
  */
43
43
 
44
44
  /** Versão do SDK — exportada explicitamente pra detecção em runtime. */
45
- declare const VERSION: "3.3.0";
45
+ declare const VERSION: "3.4.1";
46
46
 
47
47
  export { VERSION, checkCoreVersionCompat };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createNeetruClient } from './auth.js';
2
2
  export { NeetruError, NeetruErrorCode } from './errors.js';
3
- 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, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, aq as TelemetryEventAck, ar as TelemetryEventInput, av as UsageEventInput, aw as UsageNamespace, ax as UsageQuota, ay as VerifyTokenOptions, az as VerifyWebhookRequestOptions, aA as VerifyWebhookRequestResult, aB as WebhookEndpoint, aC as WebhookEvent, aD as WebhookScopeOptions, aE as WebhookTestResult, aF as WebhooksNamespace, aG as WithFieldSentinels, aH as createAiNamespace, aI as createCheckoutNamespace, aJ as createNeetruDb, aK as createNotificationsNamespace, aM as createWebhooksNamespace, aO as increment, aP as isFieldSentinel, aQ as isIncrementSentinel, aR as isServerTimestampSentinel, aS as serverTimestamp, aT as verifyWebhookRequest, aU as verifyWebhookSignature } from './types-DslphKIM.js';
3
+ 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, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, ar as TelemetryEventAck, as as TelemetryEventInput, aw as UsageEventInput, ax as UsageNamespace, ay as UsageQuota, az as VerifyTokenOptions, aA as VerifyWebhookRequestOptions, aB as VerifyWebhookRequestResult, aC as WebhookEndpoint, aD as WebhookEvent, aE as WebhookScopeOptions, aF as WebhookTestResult, aG as WebhooksNamespace, aH as WithFieldSentinels, aJ as createAiNamespace, aK as createCheckoutNamespace, aL as createNeetruDb, aM as createNotificationsNamespace, aO as createWebhooksNamespace, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp, aV as verifyWebhookRequest, aW as verifyWebhookSignature } from './types-BlOLDMR0.js';
4
4
  export { DEV_FIXTURE_USER, MockAi, MockAuth, MockAuthOptions, MockDb, MockEntitlements, MockSupport, MockUsage } from './mocks.js';
5
5
  import 'drizzle-orm/node-postgres';
6
6
 
@@ -42,6 +42,6 @@ declare function checkCoreVersionCompat(coreVersion: string): void;
42
42
  */
43
43
 
44
44
  /** Versão do SDK — exportada explicitamente pra detecção em runtime. */
45
- declare const VERSION: "3.3.0";
45
+ declare const VERSION: "3.4.1";
46
46
 
47
47
  export { VERSION, checkCoreVersionCompat };
package/dist/index.mjs CHANGED
@@ -1582,6 +1582,7 @@ function toTicket(raw) {
1582
1582
  if (typeof r.id !== "string") {
1583
1583
  throw new NeetruError("invalid_response", "Ticket missing id");
1584
1584
  }
1585
+ const firstAttachment = Array.isArray(r.attachments) && typeof r.attachments[0] === "string" ? r.attachments[0] : void 0;
1585
1586
  return {
1586
1587
  id: r.id,
1587
1588
  subject: typeof r.subject === "string" ? r.subject : "",
@@ -1590,9 +1591,28 @@ function toTicket(raw) {
1590
1591
  status: VALID_STATUSES.includes(r.status) ? r.status : "open",
1591
1592
  createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
1592
1593
  updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
1593
- productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0
1594
+ productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0,
1595
+ pageUrl: typeof r.pageUrl === "string" ? r.pageUrl : null,
1596
+ screenshotUrl: typeof r.screenshotUrl === "string" ? r.screenshotUrl : firstAttachment ?? null,
1597
+ resolvedAt: typeof r.resolvedAt === "string" ? r.resolvedAt : null,
1598
+ resolvedNotifiedAt: typeof r.resolvedNotifiedAt === "string" ? r.resolvedNotifiedAt : null,
1599
+ satisfactionAttempts: typeof r.satisfactionAttempts === "number" && Number.isFinite(r.satisfactionAttempts) ? r.satisfactionAttempts : 0,
1600
+ lastSatisfactionAttemptAt: typeof r.lastSatisfactionAttemptAt === "string" ? r.lastSatisfactionAttemptAt : null,
1601
+ satisfiedAt: typeof r.satisfiedAt === "string" ? r.satisfiedAt : null,
1602
+ rating: typeof r.rating === "number" && Number.isFinite(r.rating) ? r.rating : null,
1603
+ satisfactionComment: typeof r.satisfactionComment === "string" ? r.satisfactionComment : null
1594
1604
  };
1595
1605
  }
1606
+ function assertTicketId(ticketId) {
1607
+ if (!ticketId || typeof ticketId !== "string") {
1608
+ throw new NeetruError("validation_failed", "ticketId is required");
1609
+ }
1610
+ }
1611
+ function assertValidComment(comment) {
1612
+ if (typeof comment === "string" && comment.length > 2e3) {
1613
+ throw new NeetruError("validation_failed", "comment max 2000 chars");
1614
+ }
1615
+ }
1596
1616
  function resolveProductId(config, ...candidates) {
1597
1617
  for (const c of candidates) {
1598
1618
  if (typeof c === "string" && c.length > 0) {
@@ -1649,6 +1669,15 @@ function createSupportNamespace(config) {
1649
1669
  if (input.metadata && typeof input.metadata === "object") {
1650
1670
  body.metadata = input.metadata;
1651
1671
  }
1672
+ if (typeof input.pageUrl === "string") {
1673
+ body.pageUrl = input.pageUrl;
1674
+ }
1675
+ if (typeof input.screenshotBase64 === "string") {
1676
+ body.screenshotBase64 = input.screenshotBase64;
1677
+ }
1678
+ if (typeof input.consoleLogs === "string") {
1679
+ body.consoleLogs = input.consoleLogs;
1680
+ }
1652
1681
  const raw = await httpRequest(config, {
1653
1682
  method: "POST",
1654
1683
  path: "/api/sdk/v1/support/tickets",
@@ -1674,6 +1703,74 @@ function createSupportNamespace(config) {
1674
1703
  });
1675
1704
  const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
1676
1705
  return list.map(toTicket);
1706
+ },
1707
+ /**
1708
+ * 3.4: marca a notificação de resolução como vista (`resolvedNotifiedAt`,
1709
+ * idempotente no Core). Endpoint canonical:
1710
+ * `POST /api/sdk/v1/support/tickets/{id}/resolved-seen`.
1711
+ */
1712
+ async markResolvedSeen(ticketId) {
1713
+ assertTicketId(ticketId);
1714
+ await httpRequest(config, {
1715
+ method: "POST",
1716
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/resolved-seen`,
1717
+ requireAuth: true
1718
+ });
1719
+ },
1720
+ /**
1721
+ * 3.4: registra uma tentativa de exibição do modal de satisfação
1722
+ * (`satisfactionAttempts += 1`). Chamar no SHOW do modal, não na
1723
+ * resposta. Endpoint canonical:
1724
+ * `POST /api/sdk/v1/support/tickets/{id}/satisfaction-attempt`.
1725
+ */
1726
+ async recordSatisfactionAttempt(ticketId) {
1727
+ assertTicketId(ticketId);
1728
+ await httpRequest(config, {
1729
+ method: "POST",
1730
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/satisfaction-attempt`,
1731
+ requireAuth: true
1732
+ });
1733
+ },
1734
+ /**
1735
+ * 3.4: envia (ou pula) a avaliação de satisfação (CSAT 1-5). Endpoint
1736
+ * canonical: `POST /api/sdk/v1/support/tickets/{id}/satisfaction`.
1737
+ */
1738
+ async submitSatisfaction(input) {
1739
+ if (!input || typeof input !== "object") {
1740
+ throw new NeetruError("validation_failed", "input is required");
1741
+ }
1742
+ assertTicketId(input.ticketId);
1743
+ if (input.rating !== null && input.rating !== void 0) {
1744
+ if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
1745
+ throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
1746
+ }
1747
+ }
1748
+ assertValidComment(input.comment);
1749
+ await httpRequest(config, {
1750
+ method: "POST",
1751
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(input.ticketId)}/satisfaction`,
1752
+ body: {
1753
+ rating: input.rating ?? null,
1754
+ comment: input.comment ?? null,
1755
+ skipped: input.skipped
1756
+ },
1757
+ requireAuth: true
1758
+ });
1759
+ },
1760
+ /**
1761
+ * 3.4: contesta um ticket resolvido — reabre e zera o ciclo de
1762
+ * satisfação/notificação. Endpoint canonical:
1763
+ * `POST /api/sdk/v1/support/tickets/{id}/contest`.
1764
+ */
1765
+ async contestTicket(ticketId, opts) {
1766
+ assertTicketId(ticketId);
1767
+ assertValidComment(opts?.comment);
1768
+ await httpRequest(config, {
1769
+ method: "POST",
1770
+ path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/contest`,
1771
+ body: { comment: opts?.comment ?? void 0 },
1772
+ requireAuth: true
1773
+ });
1677
1774
  }
1678
1775
  };
1679
1776
  }
@@ -2444,8 +2541,10 @@ var MutationQueue = class {
2444
2541
  await this.store.deleteMutation(mutationId);
2445
2542
  }
2446
2543
  /**
2447
- * Marca uma mutação como `failed` e registra o erro.
2448
- * Incrementa `attempts`. Chamado pelo SyncEngine em falha permanente.
2544
+ * Marca uma mutação como `failed` (dead-letter) e registra o erro.
2545
+ * Incrementa `attempts` e carimba `lastAttemptAt`. Chamado pelo SyncEngine
2546
+ * quando a mutação esgota o teto de tentativas (mutação-veneno): sai do
2547
+ * conjunto `queued`, então `drain()` para de devolvê-la e a fila DESTRAVA.
2449
2548
  */
2450
2549
  async markFailed(mutationId, error) {
2451
2550
  const mut = await this.store.getMutation(mutationId);
@@ -2454,12 +2553,14 @@ var MutationQueue = class {
2454
2553
  ...mut,
2455
2554
  status: "failed",
2456
2555
  lastError: error,
2457
- attempts: mut.attempts + 1
2556
+ attempts: mut.attempts + 1,
2557
+ lastAttemptAt: Date.now()
2458
2558
  });
2459
2559
  }
2460
2560
  /**
2461
- * Incrementa `attempts` e volta para `queued` (falha transiente com backoff).
2462
- * Chamado pelo SyncEngine em falha transiente (5xx, timeout).
2561
+ * Incrementa `attempts`, carimba `lastAttemptAt` e volta para `queued`
2562
+ * (falha transiente com backoff). Chamado pelo SyncEngine em falha
2563
+ * transiente (5xx, timeout) da mutação que REALMENTE foi tentada.
2463
2564
  */
2464
2565
  async markRetry(mutationId, error) {
2465
2566
  const mut = await this.store.getMutation(mutationId);
@@ -2468,9 +2569,24 @@ var MutationQueue = class {
2468
2569
  ...mut,
2469
2570
  status: "queued",
2470
2571
  lastError: error,
2471
- attempts: mut.attempts + 1
2572
+ attempts: mut.attempts + 1,
2573
+ lastAttemptAt: Date.now()
2472
2574
  });
2473
2575
  }
2576
+ /**
2577
+ * Reverte uma mutação `inflight` de volta a `queued` SEM tocar em `attempts`
2578
+ * nem `lastAttemptAt`. Usado para mutações que foram marcadas inflight mas
2579
+ * NUNCA chegaram a ser enviadas (estavam atrás da head no FIFO e o push
2580
+ * parou antes delas): contá-las como tentativa as empurraria pro dead-letter
2581
+ * por engano, e adiaria seu envio via backoff sem que tenham falhado.
2582
+ * No-op se a mutação não está mais `inflight`.
2583
+ */
2584
+ async requeueInflight(mutationId) {
2585
+ const mut = await this.store.getMutation(mutationId);
2586
+ if (!mut) return;
2587
+ if (mut.status !== "inflight") return;
2588
+ await this.store.putMutation({ ...mut, status: "queued" });
2589
+ }
2474
2590
  };
2475
2591
 
2476
2592
  // src/db/field-value.ts
@@ -2526,6 +2642,10 @@ function hasFieldSentinel(data) {
2526
2642
  }
2527
2643
 
2528
2644
  // src/db/offline/sync-engine.ts
2645
+ var MAX_MUTATION_ATTEMPTS = 8;
2646
+ var BACKOFF_BASE_MS = 1e3;
2647
+ var BACKOFF_CAP_MS = 5 * 60 * 1e3;
2648
+ var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
2529
2649
  function hasSentinels(payload) {
2530
2650
  return payload !== null && hasFieldSentinel(payload);
2531
2651
  }
@@ -2560,6 +2680,8 @@ var SyncEngine = class {
2560
2680
  _state;
2561
2681
  /** Listeners de mudança de SyncState. */
2562
2682
  _stateListeners = /* @__PURE__ */ new Set();
2683
+ /** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
2684
+ _errorListeners = /* @__PURE__ */ new Set();
2563
2685
  /** Cleanup functions dos listeners externos. */
2564
2686
  _cleanups = [];
2565
2687
  /** Timer de resync periódico. */
@@ -2618,6 +2740,19 @@ var SyncEngine = class {
2618
2740
  this._stateListeners.delete(cb);
2619
2741
  };
2620
2742
  }
2743
+ /**
2744
+ * Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
2745
+ * (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
2746
+ * mutação-veneno) — o que antes era engolido com "log silencioso".
2747
+ * O mesmo diagnóstico também aparece em `SyncState.lastError`.
2748
+ * Retorna uma função de unsubscribe.
2749
+ */
2750
+ onError(cb) {
2751
+ this._errorListeners.add(cb);
2752
+ return () => {
2753
+ this._errorListeners.delete(cb);
2754
+ };
2755
+ }
2621
2756
  /**
2622
2757
  * Força um sync imediato (I3 §10.3 `flush()`).
2623
2758
  * Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
@@ -2650,8 +2785,10 @@ var SyncEngine = class {
2650
2785
  }
2651
2786
  await this._phase2Pull();
2652
2787
  this._phase3Realtime();
2653
- this._updateState({ status: "idle", lastSyncedAt: Date.now() });
2788
+ this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
2654
2789
  } catch (err) {
2790
+ const msg = err instanceof Error ? err.message : String(err);
2791
+ this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
2655
2792
  this._updateState({ status: "idle" });
2656
2793
  } finally {
2657
2794
  this._syncing = false;
@@ -2686,6 +2823,9 @@ var SyncEngine = class {
2686
2823
  async _phase1Push() {
2687
2824
  const mutations = await this._queue.drain();
2688
2825
  if (mutations.length === 0) return true;
2826
+ if (this._isInBackoff(mutations[0])) {
2827
+ return true;
2828
+ }
2689
2829
  for (const mut of mutations) {
2690
2830
  await this._queue.markInflight(mut.mutationId);
2691
2831
  }
@@ -2693,10 +2833,16 @@ var SyncEngine = class {
2693
2833
  try {
2694
2834
  result = await this._transport.pushMutations(mutations);
2695
2835
  } catch (err) {
2696
- for (const mut of mutations) {
2697
- const errMsg = err instanceof Error ? err.message : String(err);
2698
- await this._queue.markRetry(mut.mutationId, errMsg);
2699
- }
2836
+ const errMsg = err instanceof Error ? err.message : String(err);
2837
+ const busChangesOnThrow = [];
2838
+ const [head, ...behind] = mutations;
2839
+ await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
2840
+ for (const mut of behind) {
2841
+ await this._queue.requeueInflight(mut.mutationId);
2842
+ }
2843
+ if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
2844
+ this._refreshPendingWrites().catch(() => {
2845
+ });
2700
2846
  return false;
2701
2847
  }
2702
2848
  const busChanges = [];
@@ -2723,8 +2869,16 @@ var SyncEngine = class {
2723
2869
  }
2724
2870
  const processedIds = new Set(result.results.map((r) => r.mutationId));
2725
2871
  const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
2726
- for (const mut of unprocessed) {
2727
- await this._queue.markRetry(mut.mutationId, "partial_push: muta\xE7\xE3o n\xE3o processada no lote");
2872
+ if (unprocessed.length > 0) {
2873
+ const [boundary, ...behind] = unprocessed;
2874
+ await this._retryOrDeadLetter(
2875
+ boundary,
2876
+ "partial_push: muta\xE7\xE3o n\xE3o processada no lote",
2877
+ busChanges
2878
+ );
2879
+ for (const mut of behind) {
2880
+ await this._queue.requeueInflight(mut.mutationId);
2881
+ }
2728
2882
  }
2729
2883
  if (busChanges.length > 0) {
2730
2884
  this._bus.emit(busChanges);
@@ -2916,6 +3070,67 @@ var SyncEngine = class {
2916
3070
  });
2917
3071
  }
2918
3072
  }
3073
+ // ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
3074
+ /**
3075
+ * Trata a falha transiente de UMA mutação que foi realmente tentada.
3076
+ *
3077
+ * - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
3078
+ * backoff, `attempts++`).
3079
+ * - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
3080
+ * `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
3081
+ * e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
3082
+ * `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
3083
+ * pending do doc no cache + registra `lastError`/dispara `onError`.
3084
+ */
3085
+ async _retryOrDeadLetter(mut, errMsg, busChanges) {
3086
+ if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
3087
+ await this._queue.markRetry(mut.mutationId, errMsg);
3088
+ return;
3089
+ }
3090
+ await this._queue.markFailed(mut.mutationId, errMsg);
3091
+ const conflictRecord = {
3092
+ collection: mut.collection,
3093
+ docId: mut.docId,
3094
+ mutationId: mut.mutationId,
3095
+ losingData: mut.payload ?? {},
3096
+ winningData: {},
3097
+ reason: "failed_max_attempts",
3098
+ detectedAt: Date.now(),
3099
+ delivered: false
3100
+ };
3101
+ await this._store.appendConflict(conflictRecord);
3102
+ const existing = await this._store.getDoc(mut.collection, mut.docId);
3103
+ if (existing) {
3104
+ await this._store.putDoc({
3105
+ ...existing,
3106
+ meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
3107
+ });
3108
+ busChanges.push({
3109
+ type: "modified",
3110
+ collection: mut.collection,
3111
+ doc: { id: mut.docId, data: existing.data }
3112
+ });
3113
+ }
3114
+ this._recordSyncError(
3115
+ "push",
3116
+ `muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
3117
+ );
3118
+ }
3119
+ /**
3120
+ * `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
3121
+ * desde a última tentativa falha. Só se aplica a mutações já tentadas
3122
+ * (`attempts > 0` e `lastAttemptAt` carimbado).
3123
+ */
3124
+ _isInBackoff(mut) {
3125
+ if (mut.attempts <= 0) return false;
3126
+ const last = mut.lastAttemptAt;
3127
+ if (typeof last !== "number") return false;
3128
+ return Date.now() < last + this._backoffDelayMs(mut.attempts);
3129
+ }
3130
+ /** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
3131
+ _backoffDelayMs(attempts) {
3132
+ return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
3133
+ }
2919
3134
  // ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
2920
3135
  /**
2921
3136
  * Busca mudanças do servidor e aplica ao cache via ConflictResolver.
@@ -2955,10 +3170,14 @@ var SyncEngine = class {
2955
3170
  }
2956
3171
  const busChanges = [];
2957
3172
  await this._applyServerDocs(resyncResult.docs, busChanges, true);
3173
+ const truncatedCollections = resyncResult.truncatedCollections ?? [];
3174
+ const truncatedSet = new Set(truncatedCollections);
3175
+ await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
2958
3176
  const returnedKeys = new Set(
2959
3177
  resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
2960
3178
  );
2961
3179
  for (const col of collections) {
3180
+ if (truncatedSet.has(col)) continue;
2962
3181
  const queryResult = await this._store.listDocs(col, {});
2963
3182
  for (const { id } of queryResult.docs) {
2964
3183
  const key = `${col}::${id}`;
@@ -3118,7 +3337,8 @@ var SyncEngine = class {
3118
3337
  status: isOnline ? "idle" : "offline",
3119
3338
  pendingWrites: 0,
3120
3339
  lastSyncedAt: null,
3121
- isLeaderTab: this._tabCoordinator.isLeader()
3340
+ isLeaderTab: this._tabCoordinator.isLeader(),
3341
+ lastError: null
3122
3342
  };
3123
3343
  }
3124
3344
  /**
@@ -3128,7 +3348,7 @@ var SyncEngine = class {
3128
3348
  _updateState(partial) {
3129
3349
  const prev = this._state;
3130
3350
  const next = { ...prev, ...partial };
3131
- const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
3351
+ const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab || (prev.lastError ?? null) !== (next.lastError ?? null);
3132
3352
  this._state = next;
3133
3353
  if (changed) {
3134
3354
  this._emitState(next);
@@ -3142,6 +3362,21 @@ var SyncEngine = class {
3142
3362
  }
3143
3363
  }
3144
3364
  }
3365
+ /**
3366
+ * P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
3367
+ * Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
3368
+ * dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
3369
+ */
3370
+ _recordSyncError(phase, message) {
3371
+ this._updateState({ lastError: message });
3372
+ const event = { phase, message, at: Date.now() };
3373
+ for (const listener of this._errorListeners) {
3374
+ try {
3375
+ listener({ ...event });
3376
+ } catch {
3377
+ }
3378
+ }
3379
+ }
3145
3380
  /**
3146
3381
  * Atualiza `pendingWrites` no estado.
3147
3382
  * Chamado de forma assíncrona quando necessário.
@@ -3951,7 +4186,9 @@ var DbCollectionRefImpl = class {
3951
4186
  const isOnline = this._isOnline();
3952
4187
  const syncState = this._engine.getSyncState();
3953
4188
  const nextCursor = buildNextCursor(result.docs, effectiveLimit);
3954
- const stale = !isOnline || syncState.status === "offline";
4189
+ const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
4190
+ const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
4191
+ const stale = !isOnline || syncState.status === "offline" || truncated;
3955
4192
  const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
3956
4193
  return {
3957
4194
  docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
@@ -3959,7 +4196,8 @@ var DbCollectionRefImpl = class {
3959
4196
  fromCache,
3960
4197
  stale,
3961
4198
  hasPendingWrites: hasPending,
3962
- changes: []
4199
+ changes: [],
4200
+ truncated
3963
4201
  };
3964
4202
  }
3965
4203
  // ── CRUD ────────────────────────────────────────────────────────────────────
@@ -4405,6 +4643,9 @@ var NeetruDbDocumentsImpl = class {
4405
4643
  async getConflicts() {
4406
4644
  return this._store.listConflicts();
4407
4645
  }
4646
+ onError(cb) {
4647
+ return this._engine.onError(cb);
4648
+ }
4408
4649
  };
4409
4650
  async function createOfflineDocumentsNamespace(opts) {
4410
4651
  const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
@@ -4880,6 +5121,7 @@ var defaultWebSocketFactory = (url) => {
4880
5121
  // src/db/client-db.ts
4881
5122
  var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
4882
5123
  var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
5124
+ var DOCUMENTS_RESYNC_PAGE_CAP = 200;
4883
5125
  function resolveTransport(transport, opts, config) {
4884
5126
  if (opts._transport) {
4885
5127
  return opts._transport;
@@ -4992,6 +5234,7 @@ function createRestSyncTransport(config) {
4992
5234
  const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
4993
5235
  const now = Date.now();
4994
5236
  const docs = [];
5237
+ const truncatedCollections = [];
4995
5238
  for (const collection of collections) {
4996
5239
  let raw;
4997
5240
  try {
@@ -5001,11 +5244,11 @@ function createRestSyncTransport(config) {
5001
5244
  requireAuth: true,
5002
5245
  retries: 2,
5003
5246
  headers: tenantHeaders,
5004
- // Teto do Core é 200. Coleções > 200 docs truncam silenciosamente
5005
- // aqui (paginação cursor é tarefa Core separada) — a detecção de
5006
- // deleção-por-ausência do SyncEngine NÃO deve rodar sobre truncamento,
5007
- // então mantemos o limite no máximo do servidor por ora.
5008
- query: { limit: 200 }
5247
+ // Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
5248
+ // TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
5249
+ // (a) NÃO rodar deleção-por-ausência e (b) sinalizar
5250
+ // `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
5251
+ query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
5009
5252
  });
5010
5253
  } catch (err) {
5011
5254
  const msg = err instanceof Error ? err.message : String(err);
@@ -5016,6 +5259,9 @@ function createRestSyncTransport(config) {
5016
5259
  }
5017
5260
  const resp = raw;
5018
5261
  const items = Array.isArray(resp?.items) ? resp.items : [];
5262
+ if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
5263
+ truncatedCollections.push(collection);
5264
+ }
5019
5265
  for (const item of items) {
5020
5266
  if (!item || typeof item !== "object") continue;
5021
5267
  const record = item;
@@ -5043,7 +5289,7 @@ function createRestSyncTransport(config) {
5043
5289
  });
5044
5290
  }
5045
5291
  }
5046
- return { docs, newWatermark: now };
5292
+ return { docs, newWatermark: now, truncatedCollections };
5047
5293
  }
5048
5294
  };
5049
5295
  }
@@ -5105,7 +5351,20 @@ function buildTicketProvider(config, opts) {
5105
5351
  return ticket;
5106
5352
  };
5107
5353
  }
5354
+ var _hasWarnedDbIdIgnored = false;
5355
+ function warnDbIdIgnoredOnce(config, dbOpts) {
5356
+ if (_hasWarnedDbIdIgnored) return;
5357
+ if (!dbOpts.dbId) return;
5358
+ if (config.env === "prod") return;
5359
+ if (typeof window === "undefined") return;
5360
+ if (typeof console === "undefined" || typeof console.warn !== "function") return;
5361
+ _hasWarnedDbIdIgnored = true;
5362
+ console.warn(
5363
+ '[neetru/sdk] client.db recebeu `dbId` ("' + dbOpts.dbId + '") mas ele N\xC3O roteia nem escopa nada no servidor: as escritas do mundo Documentos (add/set/update/remove/list) SEMPRE caem no datastore gen\xE9rico do Core, escopado s\xF3 por tenant/produto (header x-neetru-tenant). `dbId` apenas nomeia o cache offline LOCAL (IndexedDB). ATEN\xC7\xC3O: usar dbIds distintos no MESMO tenant/produto N\xC3O isola os dados no servidor \u2014 eles se misturam (co-mingling). Veja o JSDoc de NeetruDbOptions.dbId.'
5364
+ );
5365
+ }
5108
5366
  function createNeetruDb(config, dbOpts = {}) {
5367
+ warnDbIdIgnoredOnce(config, dbOpts);
5109
5368
  let resolvedTransport;
5110
5369
  if (dbOpts.transport !== void 0) {
5111
5370
  resolvedTransport = dbOpts.transport;
@@ -5183,6 +5442,20 @@ function createNeetruDb(config, dbOpts = {}) {
5183
5442
  async getConflicts() {
5184
5443
  const docs = await getDocsPromise();
5185
5444
  return docs.getConflicts();
5445
+ },
5446
+ onError(cb) {
5447
+ if (_docsResolved) return _docsResolved.onError(cb);
5448
+ let unsub = null;
5449
+ let cancelled = false;
5450
+ getDocsPromise().then((docs) => {
5451
+ if (!cancelled) {
5452
+ unsub = docs.onError(cb);
5453
+ }
5454
+ });
5455
+ return () => {
5456
+ cancelled = true;
5457
+ unsub?.();
5458
+ };
5186
5459
  }
5187
5460
  };
5188
5461
  }
@@ -6615,7 +6888,18 @@ var MockSupport = class {
6615
6888
  severity: input.severity ?? "normal",
6616
6889
  status: "open",
6617
6890
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6618
- productSlug: input.productSlug
6891
+ productSlug: input.productSlug,
6892
+ // 3.4 (product-feedback): espelha o contrato — campos novos nullable,
6893
+ // populados a partir do input quando fornecidos.
6894
+ pageUrl: typeof input.pageUrl === "string" ? input.pageUrl : null,
6895
+ screenshotUrl: typeof input.screenshotBase64 === "string" ? input.screenshotBase64 : null,
6896
+ resolvedAt: null,
6897
+ resolvedNotifiedAt: null,
6898
+ satisfactionAttempts: 0,
6899
+ lastSatisfactionAttemptAt: null,
6900
+ satisfiedAt: null,
6901
+ rating: null,
6902
+ satisfactionComment: null
6619
6903
  };
6620
6904
  this._tickets.push(ticket);
6621
6905
  return ticket;
@@ -6626,6 +6910,62 @@ var MockSupport = class {
6626
6910
  }
6627
6911
  return [...this._tickets];
6628
6912
  }
6913
+ _find(ticketId) {
6914
+ if (!ticketId || typeof ticketId !== "string") {
6915
+ throw new NeetruError("validation_failed", "ticketId is required");
6916
+ }
6917
+ const ticket = this._tickets.find((t) => t.id === ticketId);
6918
+ if (!ticket) {
6919
+ throw new NeetruError("not_found", `ticket ${ticketId} not found`);
6920
+ }
6921
+ return ticket;
6922
+ }
6923
+ /** 3.4: espelha `markResolvedSeen` — idempotente (só seta se ainda null). */
6924
+ async markResolvedSeen(ticketId) {
6925
+ const ticket = this._find(ticketId);
6926
+ if (ticket.resolvedNotifiedAt === null) {
6927
+ ticket.resolvedNotifiedAt = (/* @__PURE__ */ new Date()).toISOString();
6928
+ }
6929
+ }
6930
+ /** 3.4: espelha `recordSatisfactionAttempt` — incrementa contador. */
6931
+ async recordSatisfactionAttempt(ticketId) {
6932
+ const ticket = this._find(ticketId);
6933
+ ticket.satisfactionAttempts += 1;
6934
+ ticket.lastSatisfactionAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
6935
+ }
6936
+ /** 3.4: espelha `submitSatisfaction` — terminal, seta `satisfiedAt`. */
6937
+ async submitSatisfaction(input) {
6938
+ if (!input || typeof input !== "object") {
6939
+ throw new NeetruError("validation_failed", "input is required");
6940
+ }
6941
+ if (input.rating !== null && input.rating !== void 0) {
6942
+ if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
6943
+ throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
6944
+ }
6945
+ }
6946
+ if (typeof input.comment === "string" && input.comment.length > 2e3) {
6947
+ throw new NeetruError("validation_failed", "comment max 2000 chars");
6948
+ }
6949
+ const ticket = this._find(input.ticketId);
6950
+ ticket.satisfiedAt = (/* @__PURE__ */ new Date()).toISOString();
6951
+ ticket.rating = input.rating ?? null;
6952
+ ticket.satisfactionComment = input.comment ?? null;
6953
+ }
6954
+ /** 3.4: espelha `contestTicket` — reabre e zera o ciclo de satisfação. */
6955
+ async contestTicket(ticketId, opts) {
6956
+ if (typeof opts?.comment === "string" && opts.comment.length > 2e3) {
6957
+ throw new NeetruError("validation_failed", "comment max 2000 chars");
6958
+ }
6959
+ const ticket = this._find(ticketId);
6960
+ ticket.status = "open";
6961
+ ticket.resolvedAt = null;
6962
+ ticket.resolvedNotifiedAt = null;
6963
+ ticket.satisfactionAttempts = 0;
6964
+ ticket.lastSatisfactionAttemptAt = null;
6965
+ ticket.satisfiedAt = null;
6966
+ ticket.rating = null;
6967
+ ticket.satisfactionComment = null;
6968
+ }
6629
6969
  /** Test helper. */
6630
6970
  __reset() {
6631
6971
  this._tickets = [];
@@ -7662,7 +8002,7 @@ function createNeetruClient(config = {}) {
7662
8002
  init_errors();
7663
8003
  init_db_errors();
7664
8004
  init_http();
7665
- var VERSION = "3.3.0";
8005
+ var VERSION = "3.4.1";
7666
8006
 
7667
8007
  export { DEFAULT_BASE_URL, DEV_FIXTURE_USER, MockAi, MockAuth, MockCheckout, MockDb, MockEntitlements, MockNotifications, MockSupport, MockUsage, MockWebhooks, NEETRU_SENTINEL_KEY, NeetruDbError, NeetruError, VERSION, checkCoreVersionCompat, createAiNamespace, createCheckoutNamespace, createNeetruClient, createNeetruDb, createNotificationsNamespace, createWebhooksNamespace, increment, isFieldSentinel, isIncrementSentinel, isServerTimestampSentinel, serverTimestamp, verifyWebhookRequest, verifyWebhookSignature };
7668
8008
  //# sourceMappingURL=index.mjs.map