@neetru/sdk 3.2.2 → 3.3.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 +36 -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 +556 -20
  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 +556 -20
  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 +330 -12
  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 +330 -12
  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 +557 -21
  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 +557 -21
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/mocks.cjs +19 -3
  55. package/dist/mocks.cjs.map +1 -1
  56. package/dist/mocks.d.cts +3 -1
  57. package/dist/mocks.d.ts +3 -1
  58. package/dist/mocks.mjs +19 -3
  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 +19 -3
  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 +19 -3
  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-DQplLXIk.d.cts} +69 -5
  81. package/dist/{types-iLDjiqqk.d.ts → types-DslphKIM.d.ts} +69 -5
  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.mjs CHANGED
@@ -19,12 +19,22 @@ var init_errors = __esm({
19
19
  code;
20
20
  status;
21
21
  requestId;
22
- constructor(code, message, status, requestId) {
22
+ /**
23
+ * Motivo semântico opcional propagado pelo Core no corpo do erro (campo
24
+ * `reason` no TOPO do body, fora de `error`). Usado principalmente pelo 503
25
+ * de lease de DB, onde `reason` discrimina o estado do ambiente
26
+ * (`suspended` | `archived` | `revoked` | `provisioning` | `degraded` |
27
+ * `no_route` | `expired`). Camadas superiores (ex.: `db/sql`) mapeiam isso
28
+ * para um `NeetruDbErrorCode` mais específico (recuperável vs não).
29
+ */
30
+ reason;
31
+ constructor(code, message, status, requestId, reason) {
23
32
  super(message);
24
33
  this.name = "NeetruError";
25
34
  this.code = code;
26
35
  this.status = status;
27
36
  this.requestId = requestId;
37
+ this.reason = reason;
28
38
  Object.setPrototypeOf(this, _NeetruError.prototype);
29
39
  }
30
40
  };
@@ -195,16 +205,22 @@ async function httpRequest(config, opts) {
195
205
  const body = await safeJson(res);
196
206
  let code = statusToCode(res.status);
197
207
  let message = `HTTP ${res.status}`;
198
- if (body && typeof body === "object" && "error" in body) {
208
+ let reason;
209
+ if (body && typeof body === "object") {
199
210
  const errField = body.error;
200
211
  if (typeof errField === "string") {
201
212
  message = errField;
202
213
  } else if (errField && typeof errField === "object") {
203
214
  if (typeof errField.code === "string") code = errField.code;
204
215
  if (typeof errField.message === "string") message = errField.message;
216
+ if (typeof errField.reason === "string") reason = errField.reason;
217
+ } else {
218
+ if (typeof body.code === "string") code = body.code;
219
+ if (typeof body.message === "string") message = body.message;
205
220
  }
221
+ if (typeof body.reason === "string") reason = body.reason;
206
222
  }
207
- const err = new NeetruError(code, message, res.status, requestId);
223
+ const err = new NeetruError(code, message, res.status, requestId, reason);
208
224
  lastError = err;
209
225
  const isRetryable = RETRYABLE_CODES.has(code);
210
226
  if (isRetryable && attempt < maxRetries) {
@@ -275,8 +291,98 @@ var init_db_errors = __esm({
275
291
  }
276
292
  });
277
293
 
294
+ // src/db/sql/resilient-pool.ts
295
+ function isResilientPool(x) {
296
+ return typeof x === "object" && x !== null && x[RESILIENT_POOL_BRAND] === true;
297
+ }
298
+ function isConnectionEstablishmentError(err) {
299
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
300
+ if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "EHOSTUNREACH" || code === "EAI_AGAIN") {
301
+ return true;
302
+ }
303
+ if (typeof code === "string") {
304
+ const upper = code.toUpperCase();
305
+ if (upper === "CERT_HAS_EXPIRED" || upper.startsWith("ERR_SSL") || upper.startsWith("ERR_TLS") || upper.includes("CERTIFICATE_EXPIRED")) {
306
+ return true;
307
+ }
308
+ }
309
+ const rawMessage = err instanceof Error ? err.message : typeof err === "string" ? err : "";
310
+ const message = rawMessage.toLowerCase();
311
+ if (message.length === 0) return false;
312
+ if (message.includes("certificate expired") || message.includes("certificate has expired") || message.includes("cert_has_expired") || message.includes("alert certificate expired") || message.includes("err_ssl") || message.includes("sslv3 alert") || message.includes("tlsv1 alert")) {
313
+ return true;
314
+ }
315
+ if (message.includes("econnrefused") || message.includes("etimedout") || message.includes("enotfound") || message.includes("ehostunreach") || message.includes("getaddrinfo")) {
316
+ return true;
317
+ }
318
+ return false;
319
+ }
320
+ function createResilientPool(initialInner) {
321
+ const state = { current: initialInner };
322
+ let reLease = null;
323
+ const controls = {
324
+ __neetruSwapInner(newInner) {
325
+ const old = state.current;
326
+ state.current = newInner;
327
+ return old;
328
+ },
329
+ __neetruGetInner() {
330
+ return state.current;
331
+ },
332
+ __neetruSetReLease(fn) {
333
+ reLease = fn;
334
+ }
335
+ };
336
+ const invokeQuery = (pool, args) => pool.query(...args);
337
+ const wrappedQuery = (...args) => {
338
+ if (args.length > 0 && typeof args[args.length - 1] === "function") {
339
+ return invokeQuery(state.current, args);
340
+ }
341
+ return (async () => {
342
+ try {
343
+ return await invokeQuery(state.current, args);
344
+ } catch (err) {
345
+ const fn = reLease;
346
+ if (fn === null || !isConnectionEstablishmentError(err)) {
347
+ throw err;
348
+ }
349
+ try {
350
+ await fn();
351
+ } catch {
352
+ throw err;
353
+ }
354
+ return await invokeQuery(state.current, args);
355
+ }
356
+ })();
357
+ };
358
+ const target = Object.create(Object.getPrototypeOf(initialInner));
359
+ const proxy = new Proxy(target, {
360
+ get(_t, prop) {
361
+ if (prop === RESILIENT_POOL_BRAND) return true;
362
+ if (prop === "__neetruSwapInner") return controls.__neetruSwapInner;
363
+ if (prop === "__neetruGetInner") return controls.__neetruGetInner;
364
+ if (prop === "__neetruSetReLease") return controls.__neetruSetReLease;
365
+ if (prop === "query") return wrappedQuery;
366
+ if (prop === "constructor") {
367
+ return state.current.constructor;
368
+ }
369
+ const current = state.current;
370
+ const value = Reflect.get(current, prop, current);
371
+ return typeof value === "function" ? value.bind(current) : value;
372
+ }
373
+ });
374
+ return proxy;
375
+ }
376
+ var RESILIENT_POOL_BRAND;
377
+ var init_resilient_pool = __esm({
378
+ "src/db/sql/resilient-pool.ts"() {
379
+ RESILIENT_POOL_BRAND = /* @__PURE__ */ Symbol.for("neetru.sdk.resilientPool");
380
+ }
381
+ });
382
+
278
383
  // src/db/sql/lease.ts
279
384
  function hasConnectionChanged(oldLease, newLease) {
385
+ if (!oldLease) return true;
280
386
  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;
281
387
  }
282
388
  function mapPoolError(err) {
@@ -304,14 +410,31 @@ var RENEWAL_THRESHOLD, MIN_RENEWAL_DELAY_MS, SqlLeaseManager;
304
410
  var init_lease = __esm({
305
411
  "src/db/sql/lease.ts"() {
306
412
  init_db_errors();
413
+ init_resilient_pool();
307
414
  RENEWAL_THRESHOLD = 0.8;
308
415
  MIN_RENEWAL_DELAY_MS = 3e4;
309
416
  SqlLeaseManager = class {
417
+ /**
418
+ * Lease atual. `null` no modo LAZY antes da 1ª query (o 1º lease é adiado —
419
+ * ver `_ensureInitialLease`). Após a 1ª aquisição, sempre não-nulo.
420
+ */
310
421
  _lease;
311
422
  _pool;
312
423
  _orm;
313
424
  _renewalTimer = null;
314
425
  _closed = false;
426
+ /**
427
+ * Promessa de re-lease REATIVO em voo — coalesce chamadas concorrentes
428
+ * (N queries falhando ao mesmo tempo disparam UM único re-lease). Ver
429
+ * `_reactiveReLease`.
430
+ */
431
+ _reLeaseInFlight = null;
432
+ /**
433
+ * Promessa da aquisição do LEASE INICIAL em voo (modo lazy) — coalesce as
434
+ * N primeiras queries concorrentes num único fetch. `null` quando não há
435
+ * aquisição inicial pendente. Ver `_ensureInitialLease`.
436
+ */
437
+ _initInFlight = null;
315
438
  _deps;
316
439
  constructor(lease, pool, orm, deps) {
317
440
  this._lease = lease;
@@ -321,7 +444,7 @@ var init_lease = __esm({
321
444
  ...deps,
322
445
  now: deps.now ?? (() => Date.now())
323
446
  };
324
- this._scheduleRenewal();
447
+ if (this._lease) this._scheduleRenewal();
325
448
  }
326
449
  // ─── Superfície pública (NeetruSqlClient) ──────────────────────────────────
327
450
  get orm() {
@@ -359,6 +482,7 @@ var init_lease = __esm({
359
482
  * usa `MIN_RENEWAL_DELAY_MS` como fallback seguro.
360
483
  */
361
484
  _calcRenewalDelayMs() {
485
+ if (!this._lease) return MIN_RENEWAL_DELAY_MS;
362
486
  const expiresAtMs = Date.parse(this._lease.expiresAt);
363
487
  if (!Number.isFinite(expiresAtMs)) return MIN_RENEWAL_DELAY_MS;
364
488
  const now = this._deps.now();
@@ -382,6 +506,8 @@ var init_lease = __esm({
382
506
  }
383
507
  async _doRenew() {
384
508
  if (this._closed) return;
509
+ const currentLease = this._lease;
510
+ if (!currentLease) return;
385
511
  try {
386
512
  const withDb = (o) => {
387
513
  if (this._deps.database !== void 0) o.database = this._deps.database;
@@ -389,7 +515,7 @@ var init_lease = __esm({
389
515
  };
390
516
  let newLease;
391
517
  try {
392
- newLease = await this._deps.fetchLease(withDb({ leaseId: this._lease.leaseId }));
518
+ newLease = await this._deps.fetchLease(withDb({ leaseId: currentLease.leaseId }));
393
519
  } catch (renewErr) {
394
520
  newLease = await this._deps.fetchLease(withDb({}));
395
521
  }
@@ -406,6 +532,114 @@ var init_lease = __esm({
406
532
  }
407
533
  this._scheduleRenewal();
408
534
  }
535
+ // ─── Re-lease REATIVO (P1.5) ───────────────────────────────────────────────
536
+ /**
537
+ * Recuperação REATIVA: chamado pelo wrapper resiliente (`resilient-pool.ts`)
538
+ * quando uma query plana falha num erro de ESTABELECIMENTO de conexão (cert
539
+ * mTLS expirado no meio de uma request, ou timer de renovação que não
540
+ * disparou). Emite um lease NOVO e faz o swap do pool interno do wrapper com
541
+ * o certificado fresco; o wrapper então re-tenta a query 1x no pool novo.
542
+ *
543
+ * Vai DIRETO ao lease novo (sem `leaseId`) — a conexão falhou porque o
544
+ * certificado do lease atual está ruim, então `renew` (que depende do lease
545
+ * antigo estar vivo) não é o caminho mais robusto. Emitir do zero é o que já
546
+ * cura o death-spiral no caminho proativo (ver `_doRenew`).
547
+ *
548
+ * Coalesce concorrência: N queries falhando ao mesmo tempo compartilham UM
549
+ * único re-lease (não N leases). Após o swap, cada query re-tenta no pool
550
+ * vigente.
551
+ *
552
+ * NÃO engole erro: se o re-lease falhar, propaga — o wrapper então relança o
553
+ * erro ORIGINAL da query pro consumer.
554
+ */
555
+ async _reactiveReLease() {
556
+ if (this._closed) {
557
+ throw new NeetruDbError(
558
+ "db_unavailable",
559
+ "SqlLeaseManager foi fechado \u2014 re-lease reativo abortado."
560
+ );
561
+ }
562
+ if (this._reLeaseInFlight !== null) return this._reLeaseInFlight;
563
+ const run = (async () => {
564
+ const withDb = (o) => {
565
+ if (this._deps.database !== void 0) o.database = this._deps.database;
566
+ return o;
567
+ };
568
+ const fresh = await this._deps.fetchLease(withDb({}));
569
+ await this._swapPool(fresh);
570
+ })();
571
+ this._reLeaseInFlight = run;
572
+ try {
573
+ await run;
574
+ } finally {
575
+ this._reLeaseInFlight = null;
576
+ }
577
+ }
578
+ // ─── Lease INICIAL lazy (motor de ambientes suspenso) ──────────────────────
579
+ /**
580
+ * Adquire o LEASE INICIAL sob demanda (modo lazy). Chamado pelo pool "adiado"
581
+ * (`createDeferredPool`) na 1ª query/transação — nunca no boot do módulo.
582
+ *
583
+ * Fluxo:
584
+ * 1. Se já há lease (`_lease != null`), devolve o pool REAL interno vigente
585
+ * (idempotente — chamadas após a 1ª aquisição são no-op de fetch).
586
+ * 2. Busca o 1º lease no Core (com `database` se houver — contrato C-H).
587
+ * 3. Cria o pool REAL e faz o swap no wrapper resiliente (`__neetruSwapInner`)
588
+ * pra que as queries seguintes passem direto pelo pool real.
589
+ * 4. Agenda a renovação proativa (só a partir daqui — antes não havia lease).
590
+ *
591
+ * Coalesce concorrência: N primeiras queries compartilham UM único fetch
592
+ * (`_initInFlight`). Se a aquisição FALHA (ex.: ambiente suspenso →
593
+ * `db_suspended`, ou Core fora → `db_unavailable`), o erro propaga pra query
594
+ * que a disparou e `_initInFlight` é limpo — a PRÓXIMA query re-tenta (o
595
+ * ambiente pode ter voltado). NÃO derruba o processo nem o import do módulo.
596
+ */
597
+ async _ensureInitialLease() {
598
+ if (this._closed) {
599
+ throw new NeetruDbError(
600
+ "db_unavailable",
601
+ "SqlLeaseManager foi fechado \u2014 aquisi\xE7\xE3o de lease inicial abortada."
602
+ );
603
+ }
604
+ if (this._lease) {
605
+ return isResilientPool(this._pool) ? this._pool.__neetruGetInner() : this._pool;
606
+ }
607
+ if (this._initInFlight !== null) return this._initInFlight;
608
+ const run = (async () => {
609
+ let lease;
610
+ try {
611
+ lease = this._deps.database !== void 0 ? await this._deps.fetchLease({ database: this._deps.database }) : await this._deps.fetchLease();
612
+ } catch (err) {
613
+ if (err instanceof NeetruDbError) throw err;
614
+ const msg = err instanceof Error ? err.message : String(err);
615
+ throw new NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
616
+ }
617
+ let realPool;
618
+ try {
619
+ realPool = this._deps.createPool(lease);
620
+ } catch (err) {
621
+ const msg = err instanceof Error ? err.message : String(err);
622
+ throw new NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
623
+ }
624
+ if (isResilientPool(this._pool)) {
625
+ const old = this._pool.__neetruSwapInner(realPool);
626
+ void old.end().catch(() => {
627
+ });
628
+ } else {
629
+ this._pool = realPool;
630
+ }
631
+ this._lease = lease;
632
+ this._scheduleRenewal();
633
+ return realPool;
634
+ })();
635
+ this._initInFlight = run;
636
+ try {
637
+ return await run;
638
+ } catch (err) {
639
+ this._initInFlight = null;
640
+ throw err;
641
+ }
642
+ }
409
643
  /**
410
644
  * Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
411
645
  *
@@ -417,6 +651,13 @@ var init_lease = __esm({
417
651
  return;
418
652
  }
419
653
  const newPool = this._deps.createPool(newLease);
654
+ if (isResilientPool(this._pool)) {
655
+ const oldInner = this._pool.__neetruSwapInner(newPool);
656
+ this._lease = newLease;
657
+ void oldInner.end().catch(() => {
658
+ });
659
+ return;
660
+ }
420
661
  const newOrm = this._deps.createDrizzle(newPool, this._deps.schema);
421
662
  const oldPool = this._pool;
422
663
  this._lease = newLease;
@@ -482,10 +723,19 @@ function createHttpLeaseFetcher(config) {
482
723
  retries: 2
483
724
  });
484
725
  } catch (err) {
726
+ const verb = isRenewal ? "renovar" : "obter";
485
727
  const msg = err instanceof Error ? err.message : String(err);
728
+ const reason = err instanceof NeetruError ? err.reason : void 0;
729
+ if (reason === "suspended" || reason === "archived" || reason === "revoked") {
730
+ 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";
731
+ throw new NeetruDbError(
732
+ "db_suspended",
733
+ `Falha ao ${verb} lease SQL: ${human} (reason=${reason}).`
734
+ );
735
+ }
486
736
  throw new NeetruDbError(
487
737
  "db_unavailable",
488
- `Falha ao ${isRenewal ? "renovar" : "obter"} lease SQL: ${msg}`
738
+ `Falha ao ${verb} lease SQL: ${msg}`
489
739
  );
490
740
  }
491
741
  return parseLease(raw);
@@ -557,8 +807,64 @@ async function createDrizzleHandle(pool, schema) {
557
807
  );
558
808
  return drizzle(pool, { schema });
559
809
  }
810
+ function createDeferredPool(poolPrototype, ensureRealPool) {
811
+ let real = null;
812
+ let inflight = null;
813
+ const getReal = () => {
814
+ if (real) return Promise.resolve(real);
815
+ if (!inflight) {
816
+ inflight = ensureRealPool().then((p) => {
817
+ real = p;
818
+ inflight = null;
819
+ return p;
820
+ }).catch((e) => {
821
+ inflight = null;
822
+ throw e;
823
+ });
824
+ }
825
+ return inflight;
826
+ };
827
+ const deferred = {
828
+ async query(...args) {
829
+ const p = await getReal();
830
+ return p.query(...args);
831
+ },
832
+ async connect(...args) {
833
+ const p = await getReal();
834
+ return p.connect(...args);
835
+ },
836
+ async end() {
837
+ if (real) await real.end();
838
+ }
839
+ };
840
+ if (poolPrototype) {
841
+ Object.setPrototypeOf(deferred, poolPrototype);
842
+ }
843
+ return deferred;
844
+ }
560
845
  async function createSqlClientWithDeps(deps, options) {
561
846
  const depsWithDb = options?.database !== void 0 ? { ...deps, database: options.database } : deps;
847
+ if (depsWithDb.lazy) {
848
+ let managerRef = null;
849
+ const deferred = createDeferredPool(depsWithDb.poolPrototype ?? null, () => {
850
+ if (!managerRef) {
851
+ throw new NeetruDbError("db_unavailable", "SQL client n\xE3o inicializado (manager ausente).");
852
+ }
853
+ return managerRef._ensureInitialLease();
854
+ });
855
+ const pool2 = createResilientPool(deferred);
856
+ let orm2;
857
+ try {
858
+ orm2 = depsWithDb.createDrizzle(pool2, depsWithDb.schema);
859
+ } catch (err) {
860
+ const msg = err instanceof Error ? err.message : String(err);
861
+ throw new NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
862
+ }
863
+ const manager2 = new SqlLeaseManager(null, pool2, orm2, depsWithDb);
864
+ managerRef = manager2;
865
+ pool2.__neetruSetReLease(() => manager2._reactiveReLease());
866
+ return manager2;
867
+ }
562
868
  let lease;
563
869
  try {
564
870
  if (options?.database !== void 0) {
@@ -571,23 +877,26 @@ async function createSqlClientWithDeps(deps, options) {
571
877
  const msg = err instanceof Error ? err.message : String(err);
572
878
  throw new NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
573
879
  }
574
- let pool;
880
+ let innerPool;
575
881
  try {
576
- pool = depsWithDb.createPool(lease);
882
+ innerPool = depsWithDb.createPool(lease);
577
883
  } catch (err) {
578
884
  const msg = err instanceof Error ? err.message : String(err);
579
885
  throw new NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
580
886
  }
887
+ const pool = createResilientPool(innerPool);
581
888
  let orm;
582
889
  try {
583
890
  orm = depsWithDb.createDrizzle(pool, depsWithDb.schema);
584
891
  } catch (err) {
585
- await pool.end().catch(() => {
892
+ await innerPool.end().catch(() => {
586
893
  });
587
894
  const msg = err instanceof Error ? err.message : String(err);
588
895
  throw new NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
589
896
  }
590
- return new SqlLeaseManager(lease, pool, orm, depsWithDb);
897
+ const manager = new SqlLeaseManager(lease, pool, orm, depsWithDb);
898
+ pool.__neetruSetReLease(() => manager._reactiveReLease());
899
+ return manager;
591
900
  }
592
901
  async function createSqlClientFromConfig(config, schema, options) {
593
902
  const fetchLease = createHttpLeaseFetcher(config);
@@ -608,8 +917,8 @@ async function createSqlClientFromConfig(config, schema, options) {
608
917
  `[client.db.sql] requer pg + drizzle-orm como peerDependencies do servidor. Instale: npm install pg drizzle-orm. (${err.message})`
609
918
  );
610
919
  }
920
+ const Pool = pgMod.Pool ?? pgMod.default?.Pool;
611
921
  const createPool = (lease) => {
612
- const Pool = pgMod.Pool ?? pgMod.default?.Pool;
613
922
  const ssl = lease.sslca ? {
614
923
  ca: lease.sslca,
615
924
  cert: lease.clientCert ?? void 0,
@@ -636,7 +945,14 @@ async function createSqlClientFromConfig(config, schema, options) {
636
945
  fetchLease,
637
946
  createPool,
638
947
  createDrizzle,
639
- schema
948
+ schema,
949
+ // Lease inicial LAZY (motor de ambientes suspenso): `client.db.sql()` não
950
+ // pode explodir o `import` do módulo do produto quando o ambiente está
951
+ // suspenso/arquivado no boot. O 1º lease é adiado pra 1ª query.
952
+ lazy: true,
953
+ // Placeholder pré-1ª-query herda `pg.Pool.prototype` pra `instanceof Pool`
954
+ // valer no wrapper resiliente (transações Drizzle PASS-THROUGH seguras).
955
+ poolPrototype: Pool?.prototype ?? null
640
956
  },
641
957
  options
642
958
  );
@@ -645,7 +961,9 @@ var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
645
961
  var init_sql_client = __esm({
646
962
  "src/db/sql/sql-client.ts"() {
647
963
  init_db_errors();
964
+ init_errors();
648
965
  init_lease();
966
+ init_resilient_pool();
649
967
  LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
650
968
  LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
651
969
  }
@@ -5606,6 +5924,199 @@ init_errors();
5606
5924
  init_http();
5607
5925
  var AI_CHAT_PATH = "/api/sdk/v1/ai/chat";
5608
5926
  var AI_NONSTREAM_TIMEOUT_MS = 12e4;
5927
+ var MAX_TEXT_PART_LEN = 32768;
5928
+ var MAX_CONTENT_PARTS = 32;
5929
+ var MAX_DATA_URL_LEN = 4194304;
5930
+ var MAX_HTTPS_URL_LEN = 2048;
5931
+ var DATA_IMAGE_URL_RE = /^data:image\/(?:png|jpe?g|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
5932
+ var HTTPS_URL_RE = /^https:\/\/[^\s]+$/;
5933
+ var IMAGE_DETAIL_VALUES = /* @__PURE__ */ new Set(["low", "high", "auto"]);
5934
+ var AI_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
5935
+ var MAX_MESSAGES = 100;
5936
+ var MAX_MODEL_LEN = 128;
5937
+ var MAX_TOKENS_CAP = 32768;
5938
+ var MIN_TEMPERATURE = 0;
5939
+ var MAX_TEMPERATURE = 2;
5940
+ var MAX_TOTAL_CONTENT_PARTS = 200;
5941
+ var MAX_TOTAL_IMAGES = 20;
5942
+ var MAX_TOTAL_IMAGE_BYTES = 12 * 1024 * 1024;
5943
+ var MAX_SCHEMA_JSON_LEN = 5e4;
5944
+ var MAX_SCHEMA_DEPTH = 16;
5945
+ var MAX_SCHEMA_KEYS = 500;
5946
+ var FORBIDDEN_SCHEMA_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
5947
+ var RESPONSE_FORMAT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
5948
+ function fail(msg) {
5949
+ throw new NeetruError("validation_failed", msg);
5950
+ }
5951
+ function isStrictBase64Payload(b64) {
5952
+ if (b64.length === 0 || b64.length % 4 !== 0) return false;
5953
+ const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
5954
+ const body = pad > 0 ? b64.slice(0, b64.length - pad) : b64;
5955
+ return !body.includes("=");
5956
+ }
5957
+ function dataUrlDecodedBytes(url) {
5958
+ const comma = url.indexOf(",");
5959
+ if (comma === -1) return 0;
5960
+ const b64 = url.slice(comma + 1);
5961
+ const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
5962
+ return Math.max(0, Math.floor(b64.length * 3 / 4) - pad);
5963
+ }
5964
+ function checkAggregateMultimodalLimits(messages) {
5965
+ let totalParts = 0;
5966
+ let totalImages = 0;
5967
+ let totalImageBytes = 0;
5968
+ for (const m of messages) {
5969
+ const content = m?.content;
5970
+ if (!Array.isArray(content)) continue;
5971
+ for (const part of content) {
5972
+ totalParts++;
5973
+ if (totalParts > MAX_TOTAL_CONTENT_PARTS) {
5974
+ return `ai: total content parts exceeds ${MAX_TOTAL_CONTENT_PARTS} per request`;
5975
+ }
5976
+ if (part && typeof part === "object" && part.type === "image_url") {
5977
+ totalImages++;
5978
+ if (totalImages > MAX_TOTAL_IMAGES) {
5979
+ return `ai: total images exceeds ${MAX_TOTAL_IMAGES} per request`;
5980
+ }
5981
+ const url = part.image_url?.url;
5982
+ if (typeof url === "string" && url.startsWith("data:")) {
5983
+ totalImageBytes += dataUrlDecodedBytes(url);
5984
+ if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
5985
+ return `ai: aggregate image bytes exceeds ${MAX_TOTAL_IMAGE_BYTES} per request`;
5986
+ }
5987
+ }
5988
+ }
5989
+ }
5990
+ }
5991
+ return null;
5992
+ }
5993
+ function validateStructuredSchema(schema) {
5994
+ if (schema === void 0) return null;
5995
+ if (schema === null || typeof schema !== "object") {
5996
+ return "ai: responseFormat json_schema.schema must be an object";
5997
+ }
5998
+ let serialized;
5999
+ try {
6000
+ serialized = JSON.stringify(schema);
6001
+ } catch {
6002
+ return "ai: responseFormat json_schema.schema is not serializable";
6003
+ }
6004
+ if (serialized === void 0) return "ai: responseFormat json_schema.schema is invalid";
6005
+ if (serialized.length > MAX_SCHEMA_JSON_LEN) {
6006
+ return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_JSON_LEN} serialized chars`;
6007
+ }
6008
+ let keyCount = 0;
6009
+ const walk = (node, depth) => {
6010
+ if (depth > MAX_SCHEMA_DEPTH) {
6011
+ return `ai: responseFormat json_schema.schema exceeds max depth ${MAX_SCHEMA_DEPTH}`;
6012
+ }
6013
+ if (Array.isArray(node)) {
6014
+ for (const item of node) {
6015
+ const e = walk(item, depth + 1);
6016
+ if (e) return e;
6017
+ }
6018
+ return null;
6019
+ }
6020
+ if (node && typeof node === "object") {
6021
+ for (const key of Object.getOwnPropertyNames(node)) {
6022
+ if (FORBIDDEN_SCHEMA_KEYS.has(key)) {
6023
+ return `ai: responseFormat json_schema.schema has forbidden key: ${key}`;
6024
+ }
6025
+ keyCount++;
6026
+ if (keyCount > MAX_SCHEMA_KEYS) {
6027
+ return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_KEYS} keys`;
6028
+ }
6029
+ const e = walk(node[key], depth + 1);
6030
+ if (e) return e;
6031
+ }
6032
+ }
6033
+ return null;
6034
+ };
6035
+ return walk(schema, 0);
6036
+ }
6037
+ function validateImageUrl(url) {
6038
+ if (typeof url !== "string" || url.length === 0) {
6039
+ fail("ai: image_url.url must be a non-empty string");
6040
+ }
6041
+ const u = url;
6042
+ if (u.startsWith("data:")) {
6043
+ if (u.length > MAX_DATA_URL_LEN) fail(`ai: image_url.url (data:) exceeds ${MAX_DATA_URL_LEN} chars`);
6044
+ if (!DATA_IMAGE_URL_RE.test(u)) {
6045
+ fail("ai: image_url.url data: must be data:image/<png|jpeg|jpg|gif|webp>;base64,<...>");
6046
+ }
6047
+ const b64 = u.slice(u.indexOf(",") + 1);
6048
+ if (!isStrictBase64Payload(b64)) {
6049
+ fail("ai: image_url.url data: has invalid base64 (length/padding)");
6050
+ }
6051
+ return;
6052
+ }
6053
+ if (u.startsWith("https://")) {
6054
+ if (u.length > MAX_HTTPS_URL_LEN) fail(`ai: image_url.url (https:) exceeds ${MAX_HTTPS_URL_LEN} chars`);
6055
+ if (!HTTPS_URL_RE.test(u)) fail("ai: image_url.url https: is invalid");
6056
+ return;
6057
+ }
6058
+ fail("ai: image_url.url must be a data:image/...;base64 or https:// URL (other schemes are rejected)");
6059
+ }
6060
+ function validateAiContent(content, role) {
6061
+ if (typeof content === "string") {
6062
+ if (content.length === 0) fail("ai: message content string must be non-empty");
6063
+ if (content.length > MAX_TEXT_PART_LEN) fail(`ai: message content exceeds ${MAX_TEXT_PART_LEN} chars`);
6064
+ return;
6065
+ }
6066
+ if (!Array.isArray(content) || content.length === 0) {
6067
+ fail("ai: message content must be a non-empty string or array of parts");
6068
+ }
6069
+ if (content.length > MAX_CONTENT_PARTS) fail(`ai: message content has more than ${MAX_CONTENT_PARTS} parts`);
6070
+ for (const part of content) {
6071
+ if (!part || typeof part !== "object") fail("ai: each content part must be an object");
6072
+ const type = part.type;
6073
+ if (type === "text") {
6074
+ const text = part.text;
6075
+ if (typeof text !== "string" || text.length === 0) fail("ai: text part must have non-empty { text: string }");
6076
+ if (text.length > MAX_TEXT_PART_LEN) fail(`ai: text part exceeds ${MAX_TEXT_PART_LEN} chars`);
6077
+ } else if (type === "image_url") {
6078
+ if (role !== "user") {
6079
+ fail(`ai: image_url is only allowed in role 'user' messages (got '${role}')`);
6080
+ }
6081
+ const iu = part.image_url;
6082
+ if (!iu || typeof iu !== "object") fail("ai: image_url part must have { image_url: { url } }");
6083
+ validateImageUrl(iu.url);
6084
+ const detail = iu.detail;
6085
+ if (detail !== void 0 && (typeof detail !== "string" || !IMAGE_DETAIL_VALUES.has(detail))) {
6086
+ fail("ai: image_url.detail must be 'low' | 'high' | 'auto'");
6087
+ }
6088
+ } else {
6089
+ fail("ai: content part type must be 'text' or 'image_url'");
6090
+ }
6091
+ }
6092
+ }
6093
+ function validateResponseFormat(rf) {
6094
+ if (!rf || typeof rf !== "object") fail("ai: responseFormat must be an object");
6095
+ const type = rf.type;
6096
+ if (type === "text" || type === "json_object") return;
6097
+ if (type === "json_schema") {
6098
+ const js = rf.json_schema;
6099
+ if (!js || typeof js !== "object") fail("ai: responseFormat json_schema requires { json_schema: { name } }");
6100
+ const jso = js;
6101
+ const name = jso.name;
6102
+ if (typeof name !== "string" || name.length === 0) fail("ai: responseFormat json_schema.name is required");
6103
+ if (name.length > 64) fail("ai: responseFormat json_schema.name exceeds 64 chars");
6104
+ if (!RESPONSE_FORMAT_NAME_RE.test(name)) fail("ai: responseFormat json_schema.name must match a-zA-Z0-9_-");
6105
+ if (jso.description !== void 0) {
6106
+ if (typeof jso.description !== "string") fail("ai: responseFormat json_schema.description must be a string");
6107
+ if (jso.description.length > 1e3) fail("ai: responseFormat json_schema.description exceeds 1000 chars");
6108
+ }
6109
+ if (jso.strict !== void 0 && typeof jso.strict !== "boolean") {
6110
+ fail("ai: responseFormat json_schema.strict must be a boolean");
6111
+ }
6112
+ if (jso.schema !== void 0) {
6113
+ const err = validateStructuredSchema(jso.schema);
6114
+ if (err) fail(err);
6115
+ }
6116
+ return;
6117
+ }
6118
+ fail("ai: responseFormat.type must be 'text' | 'json_object' | 'json_schema'");
6119
+ }
5609
6120
  function resolveAiBody(config, opts, stream) {
5610
6121
  if (!opts || typeof opts !== "object") {
5611
6122
  throw new NeetruError("validation_failed", "ai: options object is required");
@@ -5613,17 +6124,26 @@ function resolveAiBody(config, opts, stream) {
5613
6124
  if (typeof opts.model !== "string" || opts.model.length === 0) {
5614
6125
  throw new NeetruError("validation_failed", "ai: model is required");
5615
6126
  }
6127
+ if (opts.model.length > MAX_MODEL_LEN) {
6128
+ throw new NeetruError("validation_failed", `ai: model exceeds ${MAX_MODEL_LEN} chars`);
6129
+ }
5616
6130
  if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
5617
6131
  throw new NeetruError("validation_failed", "ai: messages must be a non-empty array");
5618
6132
  }
6133
+ if (opts.messages.length > MAX_MESSAGES) {
6134
+ throw new NeetruError("validation_failed", `ai: messages exceeds ${MAX_MESSAGES} per request`);
6135
+ }
5619
6136
  for (const m of opts.messages) {
5620
- if (!m || typeof m !== "object" || typeof m.role !== "string" || m.role.length === 0 || typeof m.content !== "string") {
6137
+ if (!m || typeof m !== "object" || typeof m.role !== "string" || !AI_ROLES.has(m.role)) {
5621
6138
  throw new NeetruError(
5622
6139
  "validation_failed",
5623
- "ai: each message must be { role: string, content: string }"
6140
+ "ai: each message role must be 'system' | 'user' | 'assistant'"
5624
6141
  );
5625
6142
  }
6143
+ validateAiContent(m.content, m.role);
5626
6144
  }
6145
+ const aggErr = checkAggregateMultimodalLimits(opts.messages);
6146
+ if (aggErr) throw new NeetruError("validation_failed", aggErr);
5627
6147
  const productId = opts.productId ?? config.productId;
5628
6148
  const tenantId = opts.tenantId ?? config.tenantId;
5629
6149
  if (!productId) {
@@ -5648,17 +6168,27 @@ function resolveAiBody(config, opts, stream) {
5648
6168
  stream
5649
6169
  };
5650
6170
  if (opts.maxTokens !== void 0) {
5651
- if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0) {
5652
- throw new NeetruError("validation_failed", "ai: maxTokens must be a positive integer");
6171
+ if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0 || opts.maxTokens > MAX_TOKENS_CAP) {
6172
+ throw new NeetruError(
6173
+ "validation_failed",
6174
+ `ai: maxTokens must be a positive integer <= ${MAX_TOKENS_CAP}`
6175
+ );
5653
6176
  }
5654
6177
  body.maxTokens = opts.maxTokens;
5655
6178
  }
5656
6179
  if (opts.temperature !== void 0) {
5657
- if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature)) {
5658
- throw new NeetruError("validation_failed", "ai: temperature must be a finite number");
6180
+ if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature) || opts.temperature < MIN_TEMPERATURE || opts.temperature > MAX_TEMPERATURE) {
6181
+ throw new NeetruError(
6182
+ "validation_failed",
6183
+ `ai: temperature must be a finite number between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
6184
+ );
5659
6185
  }
5660
6186
  body.temperature = opts.temperature;
5661
6187
  }
6188
+ if (opts.responseFormat !== void 0) {
6189
+ validateResponseFormat(opts.responseFormat);
6190
+ body.responseFormat = opts.responseFormat;
6191
+ }
5662
6192
  return body;
5663
6193
  }
5664
6194
  async function safeJson2(res) {
@@ -6441,9 +6971,15 @@ var MockDb = class {
6441
6971
  };
6442
6972
  var MockAi = class {
6443
6973
  _calls = [];
6974
+ /** Extrai texto de um content string OU array multimodal (imagens ignoradas). */
6975
+ _contentToText(content) {
6976
+ if (typeof content === "string") return content;
6977
+ if (!Array.isArray(content)) return "";
6978
+ return content.filter((p) => !!p && p.type === "text").map((p) => p.text).join(" ");
6979
+ }
6444
6980
  _reply(opts) {
6445
6981
  const last = [...opts.messages].reverse().find((m) => m.role === "user") ?? opts.messages[opts.messages.length - 1];
6446
- const content = last?.content ?? "";
6982
+ const content = this._contentToText(last?.content);
6447
6983
  return content ? `mock-reply: ${content}` : "mock-reply";
6448
6984
  }
6449
6985
  async chat(opts) {
@@ -6455,7 +6991,7 @@ var MockAi = class {
6455
6991
  }
6456
6992
  this._calls.push({ model: opts.model, messages: opts.messages, stream: false });
6457
6993
  const text = this._reply(opts);
6458
- const promptTokens = opts.messages.reduce((n, m) => n + (m.content?.length ?? 0), 0);
6994
+ const promptTokens = opts.messages.reduce((n, m) => n + this._contentToText(m.content).length, 0);
6459
6995
  const completionTokens = text.length;
6460
6996
  return {
6461
6997
  choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
@@ -7126,7 +7662,7 @@ function createNeetruClient(config = {}) {
7126
7662
  init_errors();
7127
7663
  init_db_errors();
7128
7664
  init_http();
7129
- var VERSION = "3.2.2";
7665
+ var VERSION = "3.3.0";
7130
7666
 
7131
7667
  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 };
7132
7668
  //# sourceMappingURL=index.mjs.map