@neetru/sdk 3.2.3 → 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 (94) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/ai.cjs +229 -7
  3. package/dist/ai.cjs.map +1 -1
  4. package/dist/ai.d.cts +1 -1
  5. package/dist/ai.d.ts +1 -1
  6. package/dist/ai.mjs +229 -7
  7. package/dist/ai.mjs.map +1 -1
  8. package/dist/auth.cjs +581 -18
  9. package/dist/auth.cjs.map +1 -1
  10. package/dist/auth.d.cts +1 -1
  11. package/dist/auth.d.ts +1 -1
  12. package/dist/auth.mjs +581 -18
  13. package/dist/auth.mjs.map +1 -1
  14. package/dist/catalog.cjs +19 -3
  15. package/dist/catalog.cjs.map +1 -1
  16. package/dist/catalog.d.cts +1 -1
  17. package/dist/catalog.d.ts +1 -1
  18. package/dist/catalog.mjs +19 -3
  19. package/dist/catalog.mjs.map +1 -1
  20. package/dist/checkout.cjs +19 -3
  21. package/dist/checkout.cjs.map +1 -1
  22. package/dist/checkout.d.cts +1 -1
  23. package/dist/checkout.d.ts +1 -1
  24. package/dist/checkout.mjs +19 -3
  25. package/dist/checkout.mjs.map +1 -1
  26. package/dist/db-react.d.cts +1 -1
  27. package/dist/db-react.d.ts +1 -1
  28. package/dist/db.cjs +193 -8
  29. package/dist/db.cjs.map +1 -1
  30. package/dist/db.d.cts +1 -1
  31. package/dist/db.d.ts +1 -1
  32. package/dist/db.mjs +193 -9
  33. package/dist/db.mjs.map +1 -1
  34. package/dist/entitlements.cjs +19 -3
  35. package/dist/entitlements.cjs.map +1 -1
  36. package/dist/entitlements.d.cts +1 -1
  37. package/dist/entitlements.d.ts +1 -1
  38. package/dist/entitlements.mjs +19 -3
  39. package/dist/entitlements.mjs.map +1 -1
  40. package/dist/errors.cjs +11 -1
  41. package/dist/errors.cjs.map +1 -1
  42. package/dist/errors.d.cts +10 -1
  43. package/dist/errors.d.ts +10 -1
  44. package/dist/errors.mjs +11 -1
  45. package/dist/errors.mjs.map +1 -1
  46. package/dist/firestore-compat.d.cts +1 -1
  47. package/dist/firestore-compat.d.ts +1 -1
  48. package/dist/index.cjs +582 -19
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +2 -2
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.mjs +582 -19
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/mocks.cjs +87 -4
  55. package/dist/mocks.cjs.map +1 -1
  56. package/dist/mocks.d.cts +19 -1
  57. package/dist/mocks.d.ts +19 -1
  58. package/dist/mocks.mjs +87 -4
  59. package/dist/mocks.mjs.map +1 -1
  60. package/dist/notifications.cjs +19 -3
  61. package/dist/notifications.cjs.map +1 -1
  62. package/dist/notifications.d.cts +1 -1
  63. package/dist/notifications.d.ts +1 -1
  64. package/dist/notifications.mjs +19 -3
  65. package/dist/notifications.mjs.map +1 -1
  66. package/dist/react.d.cts +1 -1
  67. package/dist/react.d.ts +1 -1
  68. package/dist/support.cjs +117 -4
  69. package/dist/support.cjs.map +1 -1
  70. package/dist/support.d.cts +1 -1
  71. package/dist/support.d.ts +1 -1
  72. package/dist/support.mjs +117 -4
  73. package/dist/support.mjs.map +1 -1
  74. package/dist/telemetry.cjs +19 -3
  75. package/dist/telemetry.cjs.map +1 -1
  76. package/dist/telemetry.d.cts +1 -1
  77. package/dist/telemetry.d.ts +1 -1
  78. package/dist/telemetry.mjs +19 -3
  79. package/dist/telemetry.mjs.map +1 -1
  80. package/dist/{types-PzHvWMbV.d.cts → types-BoFSCHF_.d.cts} +185 -6
  81. package/dist/{types-iLDjiqqk.d.ts → types-DdJyqBy-.d.ts} +185 -6
  82. package/dist/usage.cjs +19 -3
  83. package/dist/usage.cjs.map +1 -1
  84. package/dist/usage.d.cts +1 -1
  85. package/dist/usage.d.ts +1 -1
  86. package/dist/usage.mjs +19 -3
  87. package/dist/usage.mjs.map +1 -1
  88. package/dist/webhooks.cjs +19 -3
  89. package/dist/webhooks.cjs.map +1 -1
  90. package/dist/webhooks.d.cts +1 -1
  91. package/dist/webhooks.d.ts +1 -1
  92. package/dist/webhooks.mjs +19 -3
  93. package/dist/webhooks.mjs.map +1 -1
  94. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -21,12 +21,22 @@ var init_errors = __esm({
21
21
  code;
22
22
  status;
23
23
  requestId;
24
- constructor(code, message, status, requestId) {
24
+ /**
25
+ * Motivo semântico opcional propagado pelo Core no corpo do erro (campo
26
+ * `reason` no TOPO do body, fora de `error`). Usado principalmente pelo 503
27
+ * de lease de DB, onde `reason` discrimina o estado do ambiente
28
+ * (`suspended` | `archived` | `revoked` | `provisioning` | `degraded` |
29
+ * `no_route` | `expired`). Camadas superiores (ex.: `db/sql`) mapeiam isso
30
+ * para um `NeetruDbErrorCode` mais específico (recuperável vs não).
31
+ */
32
+ reason;
33
+ constructor(code, message, status, requestId, reason) {
25
34
  super(message);
26
35
  this.name = "NeetruError";
27
36
  this.code = code;
28
37
  this.status = status;
29
38
  this.requestId = requestId;
39
+ this.reason = reason;
30
40
  Object.setPrototypeOf(this, _NeetruError.prototype);
31
41
  }
32
42
  };
@@ -197,16 +207,22 @@ async function httpRequest(config, opts) {
197
207
  const body = await safeJson(res);
198
208
  let code = statusToCode(res.status);
199
209
  let message = `HTTP ${res.status}`;
200
- if (body && typeof body === "object" && "error" in body) {
210
+ let reason;
211
+ if (body && typeof body === "object") {
201
212
  const errField = body.error;
202
213
  if (typeof errField === "string") {
203
214
  message = errField;
204
215
  } else if (errField && typeof errField === "object") {
205
216
  if (typeof errField.code === "string") code = errField.code;
206
217
  if (typeof errField.message === "string") message = errField.message;
218
+ if (typeof errField.reason === "string") reason = errField.reason;
219
+ } else {
220
+ if (typeof body.code === "string") code = body.code;
221
+ if (typeof body.message === "string") message = body.message;
207
222
  }
223
+ if (typeof body.reason === "string") reason = body.reason;
208
224
  }
209
- const err = new exports.NeetruError(code, message, res.status, requestId);
225
+ const err = new exports.NeetruError(code, message, res.status, requestId, reason);
210
226
  lastError = err;
211
227
  const isRetryable = RETRYABLE_CODES.has(code);
212
228
  if (isRetryable && attempt < maxRetries) {
@@ -368,6 +384,7 @@ var init_resilient_pool = __esm({
368
384
 
369
385
  // src/db/sql/lease.ts
370
386
  function hasConnectionChanged(oldLease, newLease) {
387
+ if (!oldLease) return true;
371
388
  return oldLease.leaseId !== newLease.leaseId || oldLease.credentialVersion !== newLease.credentialVersion || oldLease.host !== newLease.host || oldLease.port !== newLease.port || oldLease.dbName !== newLease.dbName || oldLease.user !== newLease.user || oldLease.password !== newLease.password || oldLease.sslca !== newLease.sslca || oldLease.clientCert !== newLease.clientCert || oldLease.clientKey !== newLease.clientKey;
372
389
  }
373
390
  function mapPoolError(err) {
@@ -399,6 +416,10 @@ var init_lease = __esm({
399
416
  RENEWAL_THRESHOLD = 0.8;
400
417
  MIN_RENEWAL_DELAY_MS = 3e4;
401
418
  SqlLeaseManager = class {
419
+ /**
420
+ * Lease atual. `null` no modo LAZY antes da 1ª query (o 1º lease é adiado —
421
+ * ver `_ensureInitialLease`). Após a 1ª aquisição, sempre não-nulo.
422
+ */
402
423
  _lease;
403
424
  _pool;
404
425
  _orm;
@@ -410,6 +431,12 @@ var init_lease = __esm({
410
431
  * `_reactiveReLease`.
411
432
  */
412
433
  _reLeaseInFlight = null;
434
+ /**
435
+ * Promessa da aquisição do LEASE INICIAL em voo (modo lazy) — coalesce as
436
+ * N primeiras queries concorrentes num único fetch. `null` quando não há
437
+ * aquisição inicial pendente. Ver `_ensureInitialLease`.
438
+ */
439
+ _initInFlight = null;
413
440
  _deps;
414
441
  constructor(lease, pool, orm, deps) {
415
442
  this._lease = lease;
@@ -419,7 +446,7 @@ var init_lease = __esm({
419
446
  ...deps,
420
447
  now: deps.now ?? (() => Date.now())
421
448
  };
422
- this._scheduleRenewal();
449
+ if (this._lease) this._scheduleRenewal();
423
450
  }
424
451
  // ─── Superfície pública (NeetruSqlClient) ──────────────────────────────────
425
452
  get orm() {
@@ -457,6 +484,7 @@ var init_lease = __esm({
457
484
  * usa `MIN_RENEWAL_DELAY_MS` como fallback seguro.
458
485
  */
459
486
  _calcRenewalDelayMs() {
487
+ if (!this._lease) return MIN_RENEWAL_DELAY_MS;
460
488
  const expiresAtMs = Date.parse(this._lease.expiresAt);
461
489
  if (!Number.isFinite(expiresAtMs)) return MIN_RENEWAL_DELAY_MS;
462
490
  const now = this._deps.now();
@@ -480,6 +508,8 @@ var init_lease = __esm({
480
508
  }
481
509
  async _doRenew() {
482
510
  if (this._closed) return;
511
+ const currentLease = this._lease;
512
+ if (!currentLease) return;
483
513
  try {
484
514
  const withDb = (o) => {
485
515
  if (this._deps.database !== void 0) o.database = this._deps.database;
@@ -487,7 +517,7 @@ var init_lease = __esm({
487
517
  };
488
518
  let newLease;
489
519
  try {
490
- newLease = await this._deps.fetchLease(withDb({ leaseId: this._lease.leaseId }));
520
+ newLease = await this._deps.fetchLease(withDb({ leaseId: currentLease.leaseId }));
491
521
  } catch (renewErr) {
492
522
  newLease = await this._deps.fetchLease(withDb({}));
493
523
  }
@@ -547,6 +577,71 @@ var init_lease = __esm({
547
577
  this._reLeaseInFlight = null;
548
578
  }
549
579
  }
580
+ // ─── Lease INICIAL lazy (motor de ambientes suspenso) ──────────────────────
581
+ /**
582
+ * Adquire o LEASE INICIAL sob demanda (modo lazy). Chamado pelo pool "adiado"
583
+ * (`createDeferredPool`) na 1ª query/transação — nunca no boot do módulo.
584
+ *
585
+ * Fluxo:
586
+ * 1. Se já há lease (`_lease != null`), devolve o pool REAL interno vigente
587
+ * (idempotente — chamadas após a 1ª aquisição são no-op de fetch).
588
+ * 2. Busca o 1º lease no Core (com `database` se houver — contrato C-H).
589
+ * 3. Cria o pool REAL e faz o swap no wrapper resiliente (`__neetruSwapInner`)
590
+ * pra que as queries seguintes passem direto pelo pool real.
591
+ * 4. Agenda a renovação proativa (só a partir daqui — antes não havia lease).
592
+ *
593
+ * Coalesce concorrência: N primeiras queries compartilham UM único fetch
594
+ * (`_initInFlight`). Se a aquisição FALHA (ex.: ambiente suspenso →
595
+ * `db_suspended`, ou Core fora → `db_unavailable`), o erro propaga pra query
596
+ * que a disparou e `_initInFlight` é limpo — a PRÓXIMA query re-tenta (o
597
+ * ambiente pode ter voltado). NÃO derruba o processo nem o import do módulo.
598
+ */
599
+ async _ensureInitialLease() {
600
+ if (this._closed) {
601
+ throw new exports.NeetruDbError(
602
+ "db_unavailable",
603
+ "SqlLeaseManager foi fechado \u2014 aquisi\xE7\xE3o de lease inicial abortada."
604
+ );
605
+ }
606
+ if (this._lease) {
607
+ return isResilientPool(this._pool) ? this._pool.__neetruGetInner() : this._pool;
608
+ }
609
+ if (this._initInFlight !== null) return this._initInFlight;
610
+ const run = (async () => {
611
+ let lease;
612
+ try {
613
+ lease = this._deps.database !== void 0 ? await this._deps.fetchLease({ database: this._deps.database }) : await this._deps.fetchLease();
614
+ } catch (err) {
615
+ if (err instanceof exports.NeetruDbError) throw err;
616
+ const msg = err instanceof Error ? err.message : String(err);
617
+ throw new exports.NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
618
+ }
619
+ let realPool;
620
+ try {
621
+ realPool = this._deps.createPool(lease);
622
+ } catch (err) {
623
+ const msg = err instanceof Error ? err.message : String(err);
624
+ throw new exports.NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
625
+ }
626
+ if (isResilientPool(this._pool)) {
627
+ const old = this._pool.__neetruSwapInner(realPool);
628
+ void old.end().catch(() => {
629
+ });
630
+ } else {
631
+ this._pool = realPool;
632
+ }
633
+ this._lease = lease;
634
+ this._scheduleRenewal();
635
+ return realPool;
636
+ })();
637
+ this._initInFlight = run;
638
+ try {
639
+ return await run;
640
+ } catch (err) {
641
+ this._initInFlight = null;
642
+ throw err;
643
+ }
644
+ }
550
645
  /**
551
646
  * Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
552
647
  *
@@ -630,10 +725,19 @@ function createHttpLeaseFetcher(config) {
630
725
  retries: 2
631
726
  });
632
727
  } catch (err) {
728
+ const verb = isRenewal ? "renovar" : "obter";
633
729
  const msg = err instanceof Error ? err.message : String(err);
730
+ const reason = err instanceof exports.NeetruError ? err.reason : void 0;
731
+ if (reason === "suspended" || reason === "archived" || reason === "revoked") {
732
+ const human = reason === "suspended" ? "ambiente suspenso \u2014 a m\xE1quina que o servia parou; ser\xE1 religado quando o ambiente voltar" : reason === "archived" ? "ambiente arquivado \u2014 este banco foi arquivado e n\xE3o aceita mais conex\xF5es" : "lease revogado \u2014 as credenciais deste ambiente foram revogadas pelo Core";
733
+ throw new exports.NeetruDbError(
734
+ "db_suspended",
735
+ `Falha ao ${verb} lease SQL: ${human} (reason=${reason}).`
736
+ );
737
+ }
634
738
  throw new exports.NeetruDbError(
635
739
  "db_unavailable",
636
- `Falha ao ${isRenewal ? "renovar" : "obter"} lease SQL: ${msg}`
740
+ `Falha ao ${verb} lease SQL: ${msg}`
637
741
  );
638
742
  }
639
743
  return parseLease(raw);
@@ -705,8 +809,64 @@ async function createDrizzleHandle(pool, schema) {
705
809
  );
706
810
  return drizzle(pool, { schema });
707
811
  }
812
+ function createDeferredPool(poolPrototype, ensureRealPool) {
813
+ let real = null;
814
+ let inflight = null;
815
+ const getReal = () => {
816
+ if (real) return Promise.resolve(real);
817
+ if (!inflight) {
818
+ inflight = ensureRealPool().then((p) => {
819
+ real = p;
820
+ inflight = null;
821
+ return p;
822
+ }).catch((e) => {
823
+ inflight = null;
824
+ throw e;
825
+ });
826
+ }
827
+ return inflight;
828
+ };
829
+ const deferred = {
830
+ async query(...args) {
831
+ const p = await getReal();
832
+ return p.query(...args);
833
+ },
834
+ async connect(...args) {
835
+ const p = await getReal();
836
+ return p.connect(...args);
837
+ },
838
+ async end() {
839
+ if (real) await real.end();
840
+ }
841
+ };
842
+ if (poolPrototype) {
843
+ Object.setPrototypeOf(deferred, poolPrototype);
844
+ }
845
+ return deferred;
846
+ }
708
847
  async function createSqlClientWithDeps(deps, options) {
709
848
  const depsWithDb = options?.database !== void 0 ? { ...deps, database: options.database } : deps;
849
+ if (depsWithDb.lazy) {
850
+ let managerRef = null;
851
+ const deferred = createDeferredPool(depsWithDb.poolPrototype ?? null, () => {
852
+ if (!managerRef) {
853
+ throw new exports.NeetruDbError("db_unavailable", "SQL client n\xE3o inicializado (manager ausente).");
854
+ }
855
+ return managerRef._ensureInitialLease();
856
+ });
857
+ const pool2 = createResilientPool(deferred);
858
+ let orm2;
859
+ try {
860
+ orm2 = depsWithDb.createDrizzle(pool2, depsWithDb.schema);
861
+ } catch (err) {
862
+ const msg = err instanceof Error ? err.message : String(err);
863
+ throw new exports.NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
864
+ }
865
+ const manager2 = new SqlLeaseManager(null, pool2, orm2, depsWithDb);
866
+ managerRef = manager2;
867
+ pool2.__neetruSetReLease(() => manager2._reactiveReLease());
868
+ return manager2;
869
+ }
710
870
  let lease;
711
871
  try {
712
872
  if (options?.database !== void 0) {
@@ -759,8 +919,8 @@ async function createSqlClientFromConfig(config, schema, options) {
759
919
  `[client.db.sql] requer pg + drizzle-orm como peerDependencies do servidor. Instale: npm install pg drizzle-orm. (${err.message})`
760
920
  );
761
921
  }
922
+ const Pool = pgMod.Pool ?? pgMod.default?.Pool;
762
923
  const createPool = (lease) => {
763
- const Pool = pgMod.Pool ?? pgMod.default?.Pool;
764
924
  const ssl = lease.sslca ? {
765
925
  ca: lease.sslca,
766
926
  cert: lease.clientCert ?? void 0,
@@ -787,7 +947,14 @@ async function createSqlClientFromConfig(config, schema, options) {
787
947
  fetchLease,
788
948
  createPool,
789
949
  createDrizzle,
790
- schema
950
+ schema,
951
+ // Lease inicial LAZY (motor de ambientes suspenso): `client.db.sql()` não
952
+ // pode explodir o `import` do módulo do produto quando o ambiente está
953
+ // suspenso/arquivado no boot. O 1º lease é adiado pra 1ª query.
954
+ lazy: true,
955
+ // Placeholder pré-1ª-query herda `pg.Pool.prototype` pra `instanceof Pool`
956
+ // valer no wrapper resiliente (transações Drizzle PASS-THROUGH seguras).
957
+ poolPrototype: Pool?.prototype ?? null
791
958
  },
792
959
  options
793
960
  );
@@ -796,6 +963,7 @@ var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
796
963
  var init_sql_client = __esm({
797
964
  "src/db/sql/sql-client.ts"() {
798
965
  init_db_errors();
966
+ init_errors();
799
967
  init_lease();
800
968
  init_resilient_pool();
801
969
  LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
@@ -1416,6 +1584,7 @@ function toTicket(raw) {
1416
1584
  if (typeof r.id !== "string") {
1417
1585
  throw new exports.NeetruError("invalid_response", "Ticket missing id");
1418
1586
  }
1587
+ const firstAttachment = Array.isArray(r.attachments) && typeof r.attachments[0] === "string" ? r.attachments[0] : void 0;
1419
1588
  return {
1420
1589
  id: r.id,
1421
1590
  subject: typeof r.subject === "string" ? r.subject : "",
@@ -1424,9 +1593,28 @@ function toTicket(raw) {
1424
1593
  status: VALID_STATUSES.includes(r.status) ? r.status : "open",
1425
1594
  createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
1426
1595
  updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
1427
- 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
1428
1606
  };
1429
1607
  }
1608
+ function assertTicketId(ticketId) {
1609
+ if (!ticketId || typeof ticketId !== "string") {
1610
+ throw new exports.NeetruError("validation_failed", "ticketId is required");
1611
+ }
1612
+ }
1613
+ function assertValidComment(comment) {
1614
+ if (typeof comment === "string" && comment.length > 2e3) {
1615
+ throw new exports.NeetruError("validation_failed", "comment max 2000 chars");
1616
+ }
1617
+ }
1430
1618
  function resolveProductId(config, ...candidates) {
1431
1619
  for (const c of candidates) {
1432
1620
  if (typeof c === "string" && c.length > 0) {
@@ -1483,6 +1671,15 @@ function createSupportNamespace(config) {
1483
1671
  if (input.metadata && typeof input.metadata === "object") {
1484
1672
  body.metadata = input.metadata;
1485
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
+ }
1486
1683
  const raw = await httpRequest(config, {
1487
1684
  method: "POST",
1488
1685
  path: "/api/sdk/v1/support/tickets",
@@ -1508,6 +1705,74 @@ function createSupportNamespace(config) {
1508
1705
  });
1509
1706
  const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
1510
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 exports.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 exports.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
+ });
1511
1776
  }
1512
1777
  };
1513
1778
  }
@@ -4939,7 +5204,20 @@ function buildTicketProvider(config, opts) {
4939
5204
  return ticket;
4940
5205
  };
4941
5206
  }
5207
+ var _hasWarnedMissingDbId = false;
5208
+ function warnMissingDbIdOnce(config, dbOpts) {
5209
+ if (_hasWarnedMissingDbId) return;
5210
+ if (dbOpts.dbId) return;
5211
+ if (config.env === "prod") return;
5212
+ if (typeof window === "undefined") return;
5213
+ if (typeof console === "undefined" || typeof console.warn !== "function") return;
5214
+ _hasWarnedMissingDbId = true;
5215
+ console.warn(
5216
+ "[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."
5217
+ );
5218
+ }
4942
5219
  function createNeetruDb(config, dbOpts = {}) {
5220
+ warnMissingDbIdOnce(config, dbOpts);
4943
5221
  let resolvedTransport;
4944
5222
  if (dbOpts.transport !== void 0) {
4945
5223
  resolvedTransport = dbOpts.transport;
@@ -5758,6 +6036,199 @@ init_errors();
5758
6036
  init_http();
5759
6037
  var AI_CHAT_PATH = "/api/sdk/v1/ai/chat";
5760
6038
  var AI_NONSTREAM_TIMEOUT_MS = 12e4;
6039
+ var MAX_TEXT_PART_LEN = 32768;
6040
+ var MAX_CONTENT_PARTS = 32;
6041
+ var MAX_DATA_URL_LEN = 4194304;
6042
+ var MAX_HTTPS_URL_LEN = 2048;
6043
+ var DATA_IMAGE_URL_RE = /^data:image\/(?:png|jpe?g|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
6044
+ var HTTPS_URL_RE = /^https:\/\/[^\s]+$/;
6045
+ var IMAGE_DETAIL_VALUES = /* @__PURE__ */ new Set(["low", "high", "auto"]);
6046
+ var AI_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
6047
+ var MAX_MESSAGES = 100;
6048
+ var MAX_MODEL_LEN = 128;
6049
+ var MAX_TOKENS_CAP = 32768;
6050
+ var MIN_TEMPERATURE = 0;
6051
+ var MAX_TEMPERATURE = 2;
6052
+ var MAX_TOTAL_CONTENT_PARTS = 200;
6053
+ var MAX_TOTAL_IMAGES = 20;
6054
+ var MAX_TOTAL_IMAGE_BYTES = 12 * 1024 * 1024;
6055
+ var MAX_SCHEMA_JSON_LEN = 5e4;
6056
+ var MAX_SCHEMA_DEPTH = 16;
6057
+ var MAX_SCHEMA_KEYS = 500;
6058
+ var FORBIDDEN_SCHEMA_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
6059
+ var RESPONSE_FORMAT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
6060
+ function fail(msg) {
6061
+ throw new exports.NeetruError("validation_failed", msg);
6062
+ }
6063
+ function isStrictBase64Payload(b64) {
6064
+ if (b64.length === 0 || b64.length % 4 !== 0) return false;
6065
+ const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
6066
+ const body = pad > 0 ? b64.slice(0, b64.length - pad) : b64;
6067
+ return !body.includes("=");
6068
+ }
6069
+ function dataUrlDecodedBytes(url) {
6070
+ const comma = url.indexOf(",");
6071
+ if (comma === -1) return 0;
6072
+ const b64 = url.slice(comma + 1);
6073
+ const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
6074
+ return Math.max(0, Math.floor(b64.length * 3 / 4) - pad);
6075
+ }
6076
+ function checkAggregateMultimodalLimits(messages) {
6077
+ let totalParts = 0;
6078
+ let totalImages = 0;
6079
+ let totalImageBytes = 0;
6080
+ for (const m of messages) {
6081
+ const content = m?.content;
6082
+ if (!Array.isArray(content)) continue;
6083
+ for (const part of content) {
6084
+ totalParts++;
6085
+ if (totalParts > MAX_TOTAL_CONTENT_PARTS) {
6086
+ return `ai: total content parts exceeds ${MAX_TOTAL_CONTENT_PARTS} per request`;
6087
+ }
6088
+ if (part && typeof part === "object" && part.type === "image_url") {
6089
+ totalImages++;
6090
+ if (totalImages > MAX_TOTAL_IMAGES) {
6091
+ return `ai: total images exceeds ${MAX_TOTAL_IMAGES} per request`;
6092
+ }
6093
+ const url = part.image_url?.url;
6094
+ if (typeof url === "string" && url.startsWith("data:")) {
6095
+ totalImageBytes += dataUrlDecodedBytes(url);
6096
+ if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
6097
+ return `ai: aggregate image bytes exceeds ${MAX_TOTAL_IMAGE_BYTES} per request`;
6098
+ }
6099
+ }
6100
+ }
6101
+ }
6102
+ }
6103
+ return null;
6104
+ }
6105
+ function validateStructuredSchema(schema) {
6106
+ if (schema === void 0) return null;
6107
+ if (schema === null || typeof schema !== "object") {
6108
+ return "ai: responseFormat json_schema.schema must be an object";
6109
+ }
6110
+ let serialized;
6111
+ try {
6112
+ serialized = JSON.stringify(schema);
6113
+ } catch {
6114
+ return "ai: responseFormat json_schema.schema is not serializable";
6115
+ }
6116
+ if (serialized === void 0) return "ai: responseFormat json_schema.schema is invalid";
6117
+ if (serialized.length > MAX_SCHEMA_JSON_LEN) {
6118
+ return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_JSON_LEN} serialized chars`;
6119
+ }
6120
+ let keyCount = 0;
6121
+ const walk = (node, depth) => {
6122
+ if (depth > MAX_SCHEMA_DEPTH) {
6123
+ return `ai: responseFormat json_schema.schema exceeds max depth ${MAX_SCHEMA_DEPTH}`;
6124
+ }
6125
+ if (Array.isArray(node)) {
6126
+ for (const item of node) {
6127
+ const e = walk(item, depth + 1);
6128
+ if (e) return e;
6129
+ }
6130
+ return null;
6131
+ }
6132
+ if (node && typeof node === "object") {
6133
+ for (const key of Object.getOwnPropertyNames(node)) {
6134
+ if (FORBIDDEN_SCHEMA_KEYS.has(key)) {
6135
+ return `ai: responseFormat json_schema.schema has forbidden key: ${key}`;
6136
+ }
6137
+ keyCount++;
6138
+ if (keyCount > MAX_SCHEMA_KEYS) {
6139
+ return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_KEYS} keys`;
6140
+ }
6141
+ const e = walk(node[key], depth + 1);
6142
+ if (e) return e;
6143
+ }
6144
+ }
6145
+ return null;
6146
+ };
6147
+ return walk(schema, 0);
6148
+ }
6149
+ function validateImageUrl(url) {
6150
+ if (typeof url !== "string" || url.length === 0) {
6151
+ fail("ai: image_url.url must be a non-empty string");
6152
+ }
6153
+ const u = url;
6154
+ if (u.startsWith("data:")) {
6155
+ if (u.length > MAX_DATA_URL_LEN) fail(`ai: image_url.url (data:) exceeds ${MAX_DATA_URL_LEN} chars`);
6156
+ if (!DATA_IMAGE_URL_RE.test(u)) {
6157
+ fail("ai: image_url.url data: must be data:image/<png|jpeg|jpg|gif|webp>;base64,<...>");
6158
+ }
6159
+ const b64 = u.slice(u.indexOf(",") + 1);
6160
+ if (!isStrictBase64Payload(b64)) {
6161
+ fail("ai: image_url.url data: has invalid base64 (length/padding)");
6162
+ }
6163
+ return;
6164
+ }
6165
+ if (u.startsWith("https://")) {
6166
+ if (u.length > MAX_HTTPS_URL_LEN) fail(`ai: image_url.url (https:) exceeds ${MAX_HTTPS_URL_LEN} chars`);
6167
+ if (!HTTPS_URL_RE.test(u)) fail("ai: image_url.url https: is invalid");
6168
+ return;
6169
+ }
6170
+ fail("ai: image_url.url must be a data:image/...;base64 or https:// URL (other schemes are rejected)");
6171
+ }
6172
+ function validateAiContent(content, role) {
6173
+ if (typeof content === "string") {
6174
+ if (content.length === 0) fail("ai: message content string must be non-empty");
6175
+ if (content.length > MAX_TEXT_PART_LEN) fail(`ai: message content exceeds ${MAX_TEXT_PART_LEN} chars`);
6176
+ return;
6177
+ }
6178
+ if (!Array.isArray(content) || content.length === 0) {
6179
+ fail("ai: message content must be a non-empty string or array of parts");
6180
+ }
6181
+ if (content.length > MAX_CONTENT_PARTS) fail(`ai: message content has more than ${MAX_CONTENT_PARTS} parts`);
6182
+ for (const part of content) {
6183
+ if (!part || typeof part !== "object") fail("ai: each content part must be an object");
6184
+ const type = part.type;
6185
+ if (type === "text") {
6186
+ const text = part.text;
6187
+ if (typeof text !== "string" || text.length === 0) fail("ai: text part must have non-empty { text: string }");
6188
+ if (text.length > MAX_TEXT_PART_LEN) fail(`ai: text part exceeds ${MAX_TEXT_PART_LEN} chars`);
6189
+ } else if (type === "image_url") {
6190
+ if (role !== "user") {
6191
+ fail(`ai: image_url is only allowed in role 'user' messages (got '${role}')`);
6192
+ }
6193
+ const iu = part.image_url;
6194
+ if (!iu || typeof iu !== "object") fail("ai: image_url part must have { image_url: { url } }");
6195
+ validateImageUrl(iu.url);
6196
+ const detail = iu.detail;
6197
+ if (detail !== void 0 && (typeof detail !== "string" || !IMAGE_DETAIL_VALUES.has(detail))) {
6198
+ fail("ai: image_url.detail must be 'low' | 'high' | 'auto'");
6199
+ }
6200
+ } else {
6201
+ fail("ai: content part type must be 'text' or 'image_url'");
6202
+ }
6203
+ }
6204
+ }
6205
+ function validateResponseFormat(rf) {
6206
+ if (!rf || typeof rf !== "object") fail("ai: responseFormat must be an object");
6207
+ const type = rf.type;
6208
+ if (type === "text" || type === "json_object") return;
6209
+ if (type === "json_schema") {
6210
+ const js = rf.json_schema;
6211
+ if (!js || typeof js !== "object") fail("ai: responseFormat json_schema requires { json_schema: { name } }");
6212
+ const jso = js;
6213
+ const name = jso.name;
6214
+ if (typeof name !== "string" || name.length === 0) fail("ai: responseFormat json_schema.name is required");
6215
+ if (name.length > 64) fail("ai: responseFormat json_schema.name exceeds 64 chars");
6216
+ if (!RESPONSE_FORMAT_NAME_RE.test(name)) fail("ai: responseFormat json_schema.name must match a-zA-Z0-9_-");
6217
+ if (jso.description !== void 0) {
6218
+ if (typeof jso.description !== "string") fail("ai: responseFormat json_schema.description must be a string");
6219
+ if (jso.description.length > 1e3) fail("ai: responseFormat json_schema.description exceeds 1000 chars");
6220
+ }
6221
+ if (jso.strict !== void 0 && typeof jso.strict !== "boolean") {
6222
+ fail("ai: responseFormat json_schema.strict must be a boolean");
6223
+ }
6224
+ if (jso.schema !== void 0) {
6225
+ const err = validateStructuredSchema(jso.schema);
6226
+ if (err) fail(err);
6227
+ }
6228
+ return;
6229
+ }
6230
+ fail("ai: responseFormat.type must be 'text' | 'json_object' | 'json_schema'");
6231
+ }
5761
6232
  function resolveAiBody(config, opts, stream) {
5762
6233
  if (!opts || typeof opts !== "object") {
5763
6234
  throw new exports.NeetruError("validation_failed", "ai: options object is required");
@@ -5765,17 +6236,26 @@ function resolveAiBody(config, opts, stream) {
5765
6236
  if (typeof opts.model !== "string" || opts.model.length === 0) {
5766
6237
  throw new exports.NeetruError("validation_failed", "ai: model is required");
5767
6238
  }
6239
+ if (opts.model.length > MAX_MODEL_LEN) {
6240
+ throw new exports.NeetruError("validation_failed", `ai: model exceeds ${MAX_MODEL_LEN} chars`);
6241
+ }
5768
6242
  if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
5769
6243
  throw new exports.NeetruError("validation_failed", "ai: messages must be a non-empty array");
5770
6244
  }
6245
+ if (opts.messages.length > MAX_MESSAGES) {
6246
+ throw new exports.NeetruError("validation_failed", `ai: messages exceeds ${MAX_MESSAGES} per request`);
6247
+ }
5771
6248
  for (const m of opts.messages) {
5772
- if (!m || typeof m !== "object" || typeof m.role !== "string" || m.role.length === 0 || typeof m.content !== "string") {
6249
+ if (!m || typeof m !== "object" || typeof m.role !== "string" || !AI_ROLES.has(m.role)) {
5773
6250
  throw new exports.NeetruError(
5774
6251
  "validation_failed",
5775
- "ai: each message must be { role: string, content: string }"
6252
+ "ai: each message role must be 'system' | 'user' | 'assistant'"
5776
6253
  );
5777
6254
  }
6255
+ validateAiContent(m.content, m.role);
5778
6256
  }
6257
+ const aggErr = checkAggregateMultimodalLimits(opts.messages);
6258
+ if (aggErr) throw new exports.NeetruError("validation_failed", aggErr);
5779
6259
  const productId = opts.productId ?? config.productId;
5780
6260
  const tenantId = opts.tenantId ?? config.tenantId;
5781
6261
  if (!productId) {
@@ -5800,17 +6280,27 @@ function resolveAiBody(config, opts, stream) {
5800
6280
  stream
5801
6281
  };
5802
6282
  if (opts.maxTokens !== void 0) {
5803
- if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0) {
5804
- throw new exports.NeetruError("validation_failed", "ai: maxTokens must be a positive integer");
6283
+ if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0 || opts.maxTokens > MAX_TOKENS_CAP) {
6284
+ throw new exports.NeetruError(
6285
+ "validation_failed",
6286
+ `ai: maxTokens must be a positive integer <= ${MAX_TOKENS_CAP}`
6287
+ );
5805
6288
  }
5806
6289
  body.maxTokens = opts.maxTokens;
5807
6290
  }
5808
6291
  if (opts.temperature !== void 0) {
5809
- if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature)) {
5810
- throw new exports.NeetruError("validation_failed", "ai: temperature must be a finite number");
6292
+ if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature) || opts.temperature < MIN_TEMPERATURE || opts.temperature > MAX_TEMPERATURE) {
6293
+ throw new exports.NeetruError(
6294
+ "validation_failed",
6295
+ `ai: temperature must be a finite number between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
6296
+ );
5811
6297
  }
5812
6298
  body.temperature = opts.temperature;
5813
6299
  }
6300
+ if (opts.responseFormat !== void 0) {
6301
+ validateResponseFormat(opts.responseFormat);
6302
+ body.responseFormat = opts.responseFormat;
6303
+ }
5814
6304
  return body;
5815
6305
  }
5816
6306
  async function safeJson2(res) {
@@ -6237,7 +6727,18 @@ var MockSupport = class {
6237
6727
  severity: input.severity ?? "normal",
6238
6728
  status: "open",
6239
6729
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6240
- productSlug: input.productSlug
6730
+ productSlug: input.productSlug,
6731
+ // 3.4 (product-feedback): espelha o contrato — campos novos nullable,
6732
+ // populados a partir do input quando fornecidos.
6733
+ pageUrl: typeof input.pageUrl === "string" ? input.pageUrl : null,
6734
+ screenshotUrl: typeof input.screenshotBase64 === "string" ? input.screenshotBase64 : null,
6735
+ resolvedAt: null,
6736
+ resolvedNotifiedAt: null,
6737
+ satisfactionAttempts: 0,
6738
+ lastSatisfactionAttemptAt: null,
6739
+ satisfiedAt: null,
6740
+ rating: null,
6741
+ satisfactionComment: null
6241
6742
  };
6242
6743
  this._tickets.push(ticket);
6243
6744
  return ticket;
@@ -6248,6 +6749,62 @@ var MockSupport = class {
6248
6749
  }
6249
6750
  return [...this._tickets];
6250
6751
  }
6752
+ _find(ticketId) {
6753
+ if (!ticketId || typeof ticketId !== "string") {
6754
+ throw new exports.NeetruError("validation_failed", "ticketId is required");
6755
+ }
6756
+ const ticket = this._tickets.find((t) => t.id === ticketId);
6757
+ if (!ticket) {
6758
+ throw new exports.NeetruError("not_found", `ticket ${ticketId} not found`);
6759
+ }
6760
+ return ticket;
6761
+ }
6762
+ /** 3.4: espelha `markResolvedSeen` — idempotente (só seta se ainda null). */
6763
+ async markResolvedSeen(ticketId) {
6764
+ const ticket = this._find(ticketId);
6765
+ if (ticket.resolvedNotifiedAt === null) {
6766
+ ticket.resolvedNotifiedAt = (/* @__PURE__ */ new Date()).toISOString();
6767
+ }
6768
+ }
6769
+ /** 3.4: espelha `recordSatisfactionAttempt` — incrementa contador. */
6770
+ async recordSatisfactionAttempt(ticketId) {
6771
+ const ticket = this._find(ticketId);
6772
+ ticket.satisfactionAttempts += 1;
6773
+ ticket.lastSatisfactionAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
6774
+ }
6775
+ /** 3.4: espelha `submitSatisfaction` — terminal, seta `satisfiedAt`. */
6776
+ async submitSatisfaction(input) {
6777
+ if (!input || typeof input !== "object") {
6778
+ throw new exports.NeetruError("validation_failed", "input is required");
6779
+ }
6780
+ if (input.rating !== null && input.rating !== void 0) {
6781
+ if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
6782
+ throw new exports.NeetruError("validation_failed", "rating must be an integer 1-5 or null");
6783
+ }
6784
+ }
6785
+ if (typeof input.comment === "string" && input.comment.length > 2e3) {
6786
+ throw new exports.NeetruError("validation_failed", "comment max 2000 chars");
6787
+ }
6788
+ const ticket = this._find(input.ticketId);
6789
+ ticket.satisfiedAt = (/* @__PURE__ */ new Date()).toISOString();
6790
+ ticket.rating = input.rating ?? null;
6791
+ ticket.satisfactionComment = input.comment ?? null;
6792
+ }
6793
+ /** 3.4: espelha `contestTicket` — reabre e zera o ciclo de satisfação. */
6794
+ async contestTicket(ticketId, opts) {
6795
+ if (typeof opts?.comment === "string" && opts.comment.length > 2e3) {
6796
+ throw new exports.NeetruError("validation_failed", "comment max 2000 chars");
6797
+ }
6798
+ const ticket = this._find(ticketId);
6799
+ ticket.status = "open";
6800
+ ticket.resolvedAt = null;
6801
+ ticket.resolvedNotifiedAt = null;
6802
+ ticket.satisfactionAttempts = 0;
6803
+ ticket.lastSatisfactionAttemptAt = null;
6804
+ ticket.satisfiedAt = null;
6805
+ ticket.rating = null;
6806
+ ticket.satisfactionComment = null;
6807
+ }
6251
6808
  /** Test helper. */
6252
6809
  __reset() {
6253
6810
  this._tickets = [];
@@ -6593,9 +7150,15 @@ var MockDb = class {
6593
7150
  };
6594
7151
  var MockAi = class {
6595
7152
  _calls = [];
7153
+ /** Extrai texto de um content string OU array multimodal (imagens ignoradas). */
7154
+ _contentToText(content) {
7155
+ if (typeof content === "string") return content;
7156
+ if (!Array.isArray(content)) return "";
7157
+ return content.filter((p) => !!p && p.type === "text").map((p) => p.text).join(" ");
7158
+ }
6596
7159
  _reply(opts) {
6597
7160
  const last = [...opts.messages].reverse().find((m) => m.role === "user") ?? opts.messages[opts.messages.length - 1];
6598
- const content = last?.content ?? "";
7161
+ const content = this._contentToText(last?.content);
6599
7162
  return content ? `mock-reply: ${content}` : "mock-reply";
6600
7163
  }
6601
7164
  async chat(opts) {
@@ -6607,7 +7170,7 @@ var MockAi = class {
6607
7170
  }
6608
7171
  this._calls.push({ model: opts.model, messages: opts.messages, stream: false });
6609
7172
  const text = this._reply(opts);
6610
- const promptTokens = opts.messages.reduce((n, m) => n + (m.content?.length ?? 0), 0);
7173
+ const promptTokens = opts.messages.reduce((n, m) => n + this._contentToText(m.content).length, 0);
6611
7174
  const completionTokens = text.length;
6612
7175
  return {
6613
7176
  choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
@@ -7278,7 +7841,7 @@ function createNeetruClient(config = {}) {
7278
7841
  init_errors();
7279
7842
  init_db_errors();
7280
7843
  init_http();
7281
- var VERSION = "3.2.3";
7844
+ var VERSION = "3.4.0";
7282
7845
 
7283
7846
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
7284
7847
  exports.DEV_FIXTURE_USER = DEV_FIXTURE_USER;