@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.
- package/CHANGELOG.md +36 -0
- package/dist/ai.cjs +229 -7
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.cts +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.mjs +229 -7
- package/dist/ai.mjs.map +1 -1
- package/dist/auth.cjs +556 -20
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.mjs +556 -20
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.cjs +19 -3
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.mjs +19 -3
- package/dist/catalog.mjs.map +1 -1
- package/dist/checkout.cjs +19 -3
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/checkout.mjs +19 -3
- package/dist/checkout.mjs.map +1 -1
- package/dist/db-react.d.cts +1 -1
- package/dist/db-react.d.ts +1 -1
- package/dist/db.cjs +330 -12
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.mjs +330 -12
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.cjs +19 -3
- package/dist/entitlements.cjs.map +1 -1
- package/dist/entitlements.d.cts +1 -1
- package/dist/entitlements.d.ts +1 -1
- package/dist/entitlements.mjs +19 -3
- package/dist/entitlements.mjs.map +1 -1
- package/dist/errors.cjs +11 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +10 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.mjs +11 -1
- package/dist/errors.mjs.map +1 -1
- package/dist/firestore-compat.d.cts +1 -1
- package/dist/firestore-compat.d.ts +1 -1
- package/dist/index.cjs +557 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +557 -21
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +19 -3
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +3 -1
- package/dist/mocks.d.ts +3 -1
- package/dist/mocks.mjs +19 -3
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.cjs +19 -3
- package/dist/notifications.cjs.map +1 -1
- package/dist/notifications.d.cts +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.mjs +19 -3
- package/dist/notifications.mjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/support.cjs +19 -3
- package/dist/support.cjs.map +1 -1
- package/dist/support.d.cts +1 -1
- package/dist/support.d.ts +1 -1
- package/dist/support.mjs +19 -3
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.cjs +19 -3
- package/dist/telemetry.cjs.map +1 -1
- package/dist/telemetry.d.cts +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.mjs +19 -3
- package/dist/telemetry.mjs.map +1 -1
- package/dist/{types-PzHvWMbV.d.cts → types-DQplLXIk.d.cts} +69 -5
- package/dist/{types-iLDjiqqk.d.ts → types-DslphKIM.d.ts} +69 -5
- package/dist/usage.cjs +19 -3
- package/dist/usage.cjs.map +1 -1
- package/dist/usage.d.cts +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/usage.mjs +19 -3
- package/dist/usage.mjs.map +1 -1
- package/dist/webhooks.cjs +19 -3
- package/dist/webhooks.cjs.map +1 -1
- package/dist/webhooks.d.cts +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/dist/webhooks.mjs +19 -3
- package/dist/webhooks.mjs.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -277,8 +293,98 @@ var init_db_errors = __esm({
|
|
|
277
293
|
}
|
|
278
294
|
});
|
|
279
295
|
|
|
296
|
+
// src/db/sql/resilient-pool.ts
|
|
297
|
+
function isResilientPool(x) {
|
|
298
|
+
return typeof x === "object" && x !== null && x[RESILIENT_POOL_BRAND] === true;
|
|
299
|
+
}
|
|
300
|
+
function isConnectionEstablishmentError(err) {
|
|
301
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
302
|
+
if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "EHOSTUNREACH" || code === "EAI_AGAIN") {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (typeof code === "string") {
|
|
306
|
+
const upper = code.toUpperCase();
|
|
307
|
+
if (upper === "CERT_HAS_EXPIRED" || upper.startsWith("ERR_SSL") || upper.startsWith("ERR_TLS") || upper.includes("CERTIFICATE_EXPIRED")) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const rawMessage = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
312
|
+
const message = rawMessage.toLowerCase();
|
|
313
|
+
if (message.length === 0) return false;
|
|
314
|
+
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")) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (message.includes("econnrefused") || message.includes("etimedout") || message.includes("enotfound") || message.includes("ehostunreach") || message.includes("getaddrinfo")) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function createResilientPool(initialInner) {
|
|
323
|
+
const state = { current: initialInner };
|
|
324
|
+
let reLease = null;
|
|
325
|
+
const controls = {
|
|
326
|
+
__neetruSwapInner(newInner) {
|
|
327
|
+
const old = state.current;
|
|
328
|
+
state.current = newInner;
|
|
329
|
+
return old;
|
|
330
|
+
},
|
|
331
|
+
__neetruGetInner() {
|
|
332
|
+
return state.current;
|
|
333
|
+
},
|
|
334
|
+
__neetruSetReLease(fn) {
|
|
335
|
+
reLease = fn;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const invokeQuery = (pool, args) => pool.query(...args);
|
|
339
|
+
const wrappedQuery = (...args) => {
|
|
340
|
+
if (args.length > 0 && typeof args[args.length - 1] === "function") {
|
|
341
|
+
return invokeQuery(state.current, args);
|
|
342
|
+
}
|
|
343
|
+
return (async () => {
|
|
344
|
+
try {
|
|
345
|
+
return await invokeQuery(state.current, args);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const fn = reLease;
|
|
348
|
+
if (fn === null || !isConnectionEstablishmentError(err)) {
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
await fn();
|
|
353
|
+
} catch {
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
return await invokeQuery(state.current, args);
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
};
|
|
360
|
+
const target = Object.create(Object.getPrototypeOf(initialInner));
|
|
361
|
+
const proxy = new Proxy(target, {
|
|
362
|
+
get(_t, prop) {
|
|
363
|
+
if (prop === RESILIENT_POOL_BRAND) return true;
|
|
364
|
+
if (prop === "__neetruSwapInner") return controls.__neetruSwapInner;
|
|
365
|
+
if (prop === "__neetruGetInner") return controls.__neetruGetInner;
|
|
366
|
+
if (prop === "__neetruSetReLease") return controls.__neetruSetReLease;
|
|
367
|
+
if (prop === "query") return wrappedQuery;
|
|
368
|
+
if (prop === "constructor") {
|
|
369
|
+
return state.current.constructor;
|
|
370
|
+
}
|
|
371
|
+
const current = state.current;
|
|
372
|
+
const value = Reflect.get(current, prop, current);
|
|
373
|
+
return typeof value === "function" ? value.bind(current) : value;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return proxy;
|
|
377
|
+
}
|
|
378
|
+
var RESILIENT_POOL_BRAND;
|
|
379
|
+
var init_resilient_pool = __esm({
|
|
380
|
+
"src/db/sql/resilient-pool.ts"() {
|
|
381
|
+
RESILIENT_POOL_BRAND = /* @__PURE__ */ Symbol.for("neetru.sdk.resilientPool");
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
280
385
|
// src/db/sql/lease.ts
|
|
281
386
|
function hasConnectionChanged(oldLease, newLease) {
|
|
387
|
+
if (!oldLease) return true;
|
|
282
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;
|
|
283
389
|
}
|
|
284
390
|
function mapPoolError(err) {
|
|
@@ -306,14 +412,31 @@ var RENEWAL_THRESHOLD, MIN_RENEWAL_DELAY_MS, SqlLeaseManager;
|
|
|
306
412
|
var init_lease = __esm({
|
|
307
413
|
"src/db/sql/lease.ts"() {
|
|
308
414
|
init_db_errors();
|
|
415
|
+
init_resilient_pool();
|
|
309
416
|
RENEWAL_THRESHOLD = 0.8;
|
|
310
417
|
MIN_RENEWAL_DELAY_MS = 3e4;
|
|
311
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
|
+
*/
|
|
312
423
|
_lease;
|
|
313
424
|
_pool;
|
|
314
425
|
_orm;
|
|
315
426
|
_renewalTimer = null;
|
|
316
427
|
_closed = false;
|
|
428
|
+
/**
|
|
429
|
+
* Promessa de re-lease REATIVO em voo — coalesce chamadas concorrentes
|
|
430
|
+
* (N queries falhando ao mesmo tempo disparam UM único re-lease). Ver
|
|
431
|
+
* `_reactiveReLease`.
|
|
432
|
+
*/
|
|
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;
|
|
317
440
|
_deps;
|
|
318
441
|
constructor(lease, pool, orm, deps) {
|
|
319
442
|
this._lease = lease;
|
|
@@ -323,7 +446,7 @@ var init_lease = __esm({
|
|
|
323
446
|
...deps,
|
|
324
447
|
now: deps.now ?? (() => Date.now())
|
|
325
448
|
};
|
|
326
|
-
this._scheduleRenewal();
|
|
449
|
+
if (this._lease) this._scheduleRenewal();
|
|
327
450
|
}
|
|
328
451
|
// ─── Superfície pública (NeetruSqlClient) ──────────────────────────────────
|
|
329
452
|
get orm() {
|
|
@@ -361,6 +484,7 @@ var init_lease = __esm({
|
|
|
361
484
|
* usa `MIN_RENEWAL_DELAY_MS` como fallback seguro.
|
|
362
485
|
*/
|
|
363
486
|
_calcRenewalDelayMs() {
|
|
487
|
+
if (!this._lease) return MIN_RENEWAL_DELAY_MS;
|
|
364
488
|
const expiresAtMs = Date.parse(this._lease.expiresAt);
|
|
365
489
|
if (!Number.isFinite(expiresAtMs)) return MIN_RENEWAL_DELAY_MS;
|
|
366
490
|
const now = this._deps.now();
|
|
@@ -384,6 +508,8 @@ var init_lease = __esm({
|
|
|
384
508
|
}
|
|
385
509
|
async _doRenew() {
|
|
386
510
|
if (this._closed) return;
|
|
511
|
+
const currentLease = this._lease;
|
|
512
|
+
if (!currentLease) return;
|
|
387
513
|
try {
|
|
388
514
|
const withDb = (o) => {
|
|
389
515
|
if (this._deps.database !== void 0) o.database = this._deps.database;
|
|
@@ -391,7 +517,7 @@ var init_lease = __esm({
|
|
|
391
517
|
};
|
|
392
518
|
let newLease;
|
|
393
519
|
try {
|
|
394
|
-
newLease = await this._deps.fetchLease(withDb({ leaseId:
|
|
520
|
+
newLease = await this._deps.fetchLease(withDb({ leaseId: currentLease.leaseId }));
|
|
395
521
|
} catch (renewErr) {
|
|
396
522
|
newLease = await this._deps.fetchLease(withDb({}));
|
|
397
523
|
}
|
|
@@ -408,6 +534,114 @@ var init_lease = __esm({
|
|
|
408
534
|
}
|
|
409
535
|
this._scheduleRenewal();
|
|
410
536
|
}
|
|
537
|
+
// ─── Re-lease REATIVO (P1.5) ───────────────────────────────────────────────
|
|
538
|
+
/**
|
|
539
|
+
* Recuperação REATIVA: chamado pelo wrapper resiliente (`resilient-pool.ts`)
|
|
540
|
+
* quando uma query plana falha num erro de ESTABELECIMENTO de conexão (cert
|
|
541
|
+
* mTLS expirado no meio de uma request, ou timer de renovação que não
|
|
542
|
+
* disparou). Emite um lease NOVO e faz o swap do pool interno do wrapper com
|
|
543
|
+
* o certificado fresco; o wrapper então re-tenta a query 1x no pool novo.
|
|
544
|
+
*
|
|
545
|
+
* Vai DIRETO ao lease novo (sem `leaseId`) — a conexão falhou porque o
|
|
546
|
+
* certificado do lease atual está ruim, então `renew` (que depende do lease
|
|
547
|
+
* antigo estar vivo) não é o caminho mais robusto. Emitir do zero é o que já
|
|
548
|
+
* cura o death-spiral no caminho proativo (ver `_doRenew`).
|
|
549
|
+
*
|
|
550
|
+
* Coalesce concorrência: N queries falhando ao mesmo tempo compartilham UM
|
|
551
|
+
* único re-lease (não N leases). Após o swap, cada query re-tenta no pool
|
|
552
|
+
* vigente.
|
|
553
|
+
*
|
|
554
|
+
* NÃO engole erro: se o re-lease falhar, propaga — o wrapper então relança o
|
|
555
|
+
* erro ORIGINAL da query pro consumer.
|
|
556
|
+
*/
|
|
557
|
+
async _reactiveReLease() {
|
|
558
|
+
if (this._closed) {
|
|
559
|
+
throw new exports.NeetruDbError(
|
|
560
|
+
"db_unavailable",
|
|
561
|
+
"SqlLeaseManager foi fechado \u2014 re-lease reativo abortado."
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
if (this._reLeaseInFlight !== null) return this._reLeaseInFlight;
|
|
565
|
+
const run = (async () => {
|
|
566
|
+
const withDb = (o) => {
|
|
567
|
+
if (this._deps.database !== void 0) o.database = this._deps.database;
|
|
568
|
+
return o;
|
|
569
|
+
};
|
|
570
|
+
const fresh = await this._deps.fetchLease(withDb({}));
|
|
571
|
+
await this._swapPool(fresh);
|
|
572
|
+
})();
|
|
573
|
+
this._reLeaseInFlight = run;
|
|
574
|
+
try {
|
|
575
|
+
await run;
|
|
576
|
+
} finally {
|
|
577
|
+
this._reLeaseInFlight = null;
|
|
578
|
+
}
|
|
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
|
+
}
|
|
411
645
|
/**
|
|
412
646
|
* Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
|
|
413
647
|
*
|
|
@@ -419,6 +653,13 @@ var init_lease = __esm({
|
|
|
419
653
|
return;
|
|
420
654
|
}
|
|
421
655
|
const newPool = this._deps.createPool(newLease);
|
|
656
|
+
if (isResilientPool(this._pool)) {
|
|
657
|
+
const oldInner = this._pool.__neetruSwapInner(newPool);
|
|
658
|
+
this._lease = newLease;
|
|
659
|
+
void oldInner.end().catch(() => {
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
422
663
|
const newOrm = this._deps.createDrizzle(newPool, this._deps.schema);
|
|
423
664
|
const oldPool = this._pool;
|
|
424
665
|
this._lease = newLease;
|
|
@@ -484,10 +725,19 @@ function createHttpLeaseFetcher(config) {
|
|
|
484
725
|
retries: 2
|
|
485
726
|
});
|
|
486
727
|
} catch (err) {
|
|
728
|
+
const verb = isRenewal ? "renovar" : "obter";
|
|
487
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
|
+
}
|
|
488
738
|
throw new exports.NeetruDbError(
|
|
489
739
|
"db_unavailable",
|
|
490
|
-
`Falha ao ${
|
|
740
|
+
`Falha ao ${verb} lease SQL: ${msg}`
|
|
491
741
|
);
|
|
492
742
|
}
|
|
493
743
|
return parseLease(raw);
|
|
@@ -559,8 +809,64 @@ async function createDrizzleHandle(pool, schema) {
|
|
|
559
809
|
);
|
|
560
810
|
return drizzle(pool, { schema });
|
|
561
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
|
+
}
|
|
562
847
|
async function createSqlClientWithDeps(deps, options) {
|
|
563
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
|
+
}
|
|
564
870
|
let lease;
|
|
565
871
|
try {
|
|
566
872
|
if (options?.database !== void 0) {
|
|
@@ -573,23 +879,26 @@ async function createSqlClientWithDeps(deps, options) {
|
|
|
573
879
|
const msg = err instanceof Error ? err.message : String(err);
|
|
574
880
|
throw new exports.NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
|
|
575
881
|
}
|
|
576
|
-
let
|
|
882
|
+
let innerPool;
|
|
577
883
|
try {
|
|
578
|
-
|
|
884
|
+
innerPool = depsWithDb.createPool(lease);
|
|
579
885
|
} catch (err) {
|
|
580
886
|
const msg = err instanceof Error ? err.message : String(err);
|
|
581
887
|
throw new exports.NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
|
|
582
888
|
}
|
|
889
|
+
const pool = createResilientPool(innerPool);
|
|
583
890
|
let orm;
|
|
584
891
|
try {
|
|
585
892
|
orm = depsWithDb.createDrizzle(pool, depsWithDb.schema);
|
|
586
893
|
} catch (err) {
|
|
587
|
-
await
|
|
894
|
+
await innerPool.end().catch(() => {
|
|
588
895
|
});
|
|
589
896
|
const msg = err instanceof Error ? err.message : String(err);
|
|
590
897
|
throw new exports.NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
|
|
591
898
|
}
|
|
592
|
-
|
|
899
|
+
const manager = new SqlLeaseManager(lease, pool, orm, depsWithDb);
|
|
900
|
+
pool.__neetruSetReLease(() => manager._reactiveReLease());
|
|
901
|
+
return manager;
|
|
593
902
|
}
|
|
594
903
|
async function createSqlClientFromConfig(config, schema, options) {
|
|
595
904
|
const fetchLease = createHttpLeaseFetcher(config);
|
|
@@ -610,8 +919,8 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
610
919
|
`[client.db.sql] requer pg + drizzle-orm como peerDependencies do servidor. Instale: npm install pg drizzle-orm. (${err.message})`
|
|
611
920
|
);
|
|
612
921
|
}
|
|
922
|
+
const Pool = pgMod.Pool ?? pgMod.default?.Pool;
|
|
613
923
|
const createPool = (lease) => {
|
|
614
|
-
const Pool = pgMod.Pool ?? pgMod.default?.Pool;
|
|
615
924
|
const ssl = lease.sslca ? {
|
|
616
925
|
ca: lease.sslca,
|
|
617
926
|
cert: lease.clientCert ?? void 0,
|
|
@@ -638,7 +947,14 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
638
947
|
fetchLease,
|
|
639
948
|
createPool,
|
|
640
949
|
createDrizzle,
|
|
641
|
-
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
|
|
642
958
|
},
|
|
643
959
|
options
|
|
644
960
|
);
|
|
@@ -647,7 +963,9 @@ var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
|
|
|
647
963
|
var init_sql_client = __esm({
|
|
648
964
|
"src/db/sql/sql-client.ts"() {
|
|
649
965
|
init_db_errors();
|
|
966
|
+
init_errors();
|
|
650
967
|
init_lease();
|
|
968
|
+
init_resilient_pool();
|
|
651
969
|
LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
|
|
652
970
|
LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
|
|
653
971
|
}
|
|
@@ -5608,6 +5926,199 @@ init_errors();
|
|
|
5608
5926
|
init_http();
|
|
5609
5927
|
var AI_CHAT_PATH = "/api/sdk/v1/ai/chat";
|
|
5610
5928
|
var AI_NONSTREAM_TIMEOUT_MS = 12e4;
|
|
5929
|
+
var MAX_TEXT_PART_LEN = 32768;
|
|
5930
|
+
var MAX_CONTENT_PARTS = 32;
|
|
5931
|
+
var MAX_DATA_URL_LEN = 4194304;
|
|
5932
|
+
var MAX_HTTPS_URL_LEN = 2048;
|
|
5933
|
+
var DATA_IMAGE_URL_RE = /^data:image\/(?:png|jpe?g|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
|
|
5934
|
+
var HTTPS_URL_RE = /^https:\/\/[^\s]+$/;
|
|
5935
|
+
var IMAGE_DETAIL_VALUES = /* @__PURE__ */ new Set(["low", "high", "auto"]);
|
|
5936
|
+
var AI_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
|
|
5937
|
+
var MAX_MESSAGES = 100;
|
|
5938
|
+
var MAX_MODEL_LEN = 128;
|
|
5939
|
+
var MAX_TOKENS_CAP = 32768;
|
|
5940
|
+
var MIN_TEMPERATURE = 0;
|
|
5941
|
+
var MAX_TEMPERATURE = 2;
|
|
5942
|
+
var MAX_TOTAL_CONTENT_PARTS = 200;
|
|
5943
|
+
var MAX_TOTAL_IMAGES = 20;
|
|
5944
|
+
var MAX_TOTAL_IMAGE_BYTES = 12 * 1024 * 1024;
|
|
5945
|
+
var MAX_SCHEMA_JSON_LEN = 5e4;
|
|
5946
|
+
var MAX_SCHEMA_DEPTH = 16;
|
|
5947
|
+
var MAX_SCHEMA_KEYS = 500;
|
|
5948
|
+
var FORBIDDEN_SCHEMA_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
5949
|
+
var RESPONSE_FORMAT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
5950
|
+
function fail(msg) {
|
|
5951
|
+
throw new exports.NeetruError("validation_failed", msg);
|
|
5952
|
+
}
|
|
5953
|
+
function isStrictBase64Payload(b64) {
|
|
5954
|
+
if (b64.length === 0 || b64.length % 4 !== 0) return false;
|
|
5955
|
+
const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
5956
|
+
const body = pad > 0 ? b64.slice(0, b64.length - pad) : b64;
|
|
5957
|
+
return !body.includes("=");
|
|
5958
|
+
}
|
|
5959
|
+
function dataUrlDecodedBytes(url) {
|
|
5960
|
+
const comma = url.indexOf(",");
|
|
5961
|
+
if (comma === -1) return 0;
|
|
5962
|
+
const b64 = url.slice(comma + 1);
|
|
5963
|
+
const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
5964
|
+
return Math.max(0, Math.floor(b64.length * 3 / 4) - pad);
|
|
5965
|
+
}
|
|
5966
|
+
function checkAggregateMultimodalLimits(messages) {
|
|
5967
|
+
let totalParts = 0;
|
|
5968
|
+
let totalImages = 0;
|
|
5969
|
+
let totalImageBytes = 0;
|
|
5970
|
+
for (const m of messages) {
|
|
5971
|
+
const content = m?.content;
|
|
5972
|
+
if (!Array.isArray(content)) continue;
|
|
5973
|
+
for (const part of content) {
|
|
5974
|
+
totalParts++;
|
|
5975
|
+
if (totalParts > MAX_TOTAL_CONTENT_PARTS) {
|
|
5976
|
+
return `ai: total content parts exceeds ${MAX_TOTAL_CONTENT_PARTS} per request`;
|
|
5977
|
+
}
|
|
5978
|
+
if (part && typeof part === "object" && part.type === "image_url") {
|
|
5979
|
+
totalImages++;
|
|
5980
|
+
if (totalImages > MAX_TOTAL_IMAGES) {
|
|
5981
|
+
return `ai: total images exceeds ${MAX_TOTAL_IMAGES} per request`;
|
|
5982
|
+
}
|
|
5983
|
+
const url = part.image_url?.url;
|
|
5984
|
+
if (typeof url === "string" && url.startsWith("data:")) {
|
|
5985
|
+
totalImageBytes += dataUrlDecodedBytes(url);
|
|
5986
|
+
if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
|
|
5987
|
+
return `ai: aggregate image bytes exceeds ${MAX_TOTAL_IMAGE_BYTES} per request`;
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
return null;
|
|
5994
|
+
}
|
|
5995
|
+
function validateStructuredSchema(schema) {
|
|
5996
|
+
if (schema === void 0) return null;
|
|
5997
|
+
if (schema === null || typeof schema !== "object") {
|
|
5998
|
+
return "ai: responseFormat json_schema.schema must be an object";
|
|
5999
|
+
}
|
|
6000
|
+
let serialized;
|
|
6001
|
+
try {
|
|
6002
|
+
serialized = JSON.stringify(schema);
|
|
6003
|
+
} catch {
|
|
6004
|
+
return "ai: responseFormat json_schema.schema is not serializable";
|
|
6005
|
+
}
|
|
6006
|
+
if (serialized === void 0) return "ai: responseFormat json_schema.schema is invalid";
|
|
6007
|
+
if (serialized.length > MAX_SCHEMA_JSON_LEN) {
|
|
6008
|
+
return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_JSON_LEN} serialized chars`;
|
|
6009
|
+
}
|
|
6010
|
+
let keyCount = 0;
|
|
6011
|
+
const walk = (node, depth) => {
|
|
6012
|
+
if (depth > MAX_SCHEMA_DEPTH) {
|
|
6013
|
+
return `ai: responseFormat json_schema.schema exceeds max depth ${MAX_SCHEMA_DEPTH}`;
|
|
6014
|
+
}
|
|
6015
|
+
if (Array.isArray(node)) {
|
|
6016
|
+
for (const item of node) {
|
|
6017
|
+
const e = walk(item, depth + 1);
|
|
6018
|
+
if (e) return e;
|
|
6019
|
+
}
|
|
6020
|
+
return null;
|
|
6021
|
+
}
|
|
6022
|
+
if (node && typeof node === "object") {
|
|
6023
|
+
for (const key of Object.getOwnPropertyNames(node)) {
|
|
6024
|
+
if (FORBIDDEN_SCHEMA_KEYS.has(key)) {
|
|
6025
|
+
return `ai: responseFormat json_schema.schema has forbidden key: ${key}`;
|
|
6026
|
+
}
|
|
6027
|
+
keyCount++;
|
|
6028
|
+
if (keyCount > MAX_SCHEMA_KEYS) {
|
|
6029
|
+
return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_KEYS} keys`;
|
|
6030
|
+
}
|
|
6031
|
+
const e = walk(node[key], depth + 1);
|
|
6032
|
+
if (e) return e;
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
return null;
|
|
6036
|
+
};
|
|
6037
|
+
return walk(schema, 0);
|
|
6038
|
+
}
|
|
6039
|
+
function validateImageUrl(url) {
|
|
6040
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
6041
|
+
fail("ai: image_url.url must be a non-empty string");
|
|
6042
|
+
}
|
|
6043
|
+
const u = url;
|
|
6044
|
+
if (u.startsWith("data:")) {
|
|
6045
|
+
if (u.length > MAX_DATA_URL_LEN) fail(`ai: image_url.url (data:) exceeds ${MAX_DATA_URL_LEN} chars`);
|
|
6046
|
+
if (!DATA_IMAGE_URL_RE.test(u)) {
|
|
6047
|
+
fail("ai: image_url.url data: must be data:image/<png|jpeg|jpg|gif|webp>;base64,<...>");
|
|
6048
|
+
}
|
|
6049
|
+
const b64 = u.slice(u.indexOf(",") + 1);
|
|
6050
|
+
if (!isStrictBase64Payload(b64)) {
|
|
6051
|
+
fail("ai: image_url.url data: has invalid base64 (length/padding)");
|
|
6052
|
+
}
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
if (u.startsWith("https://")) {
|
|
6056
|
+
if (u.length > MAX_HTTPS_URL_LEN) fail(`ai: image_url.url (https:) exceeds ${MAX_HTTPS_URL_LEN} chars`);
|
|
6057
|
+
if (!HTTPS_URL_RE.test(u)) fail("ai: image_url.url https: is invalid");
|
|
6058
|
+
return;
|
|
6059
|
+
}
|
|
6060
|
+
fail("ai: image_url.url must be a data:image/...;base64 or https:// URL (other schemes are rejected)");
|
|
6061
|
+
}
|
|
6062
|
+
function validateAiContent(content, role) {
|
|
6063
|
+
if (typeof content === "string") {
|
|
6064
|
+
if (content.length === 0) fail("ai: message content string must be non-empty");
|
|
6065
|
+
if (content.length > MAX_TEXT_PART_LEN) fail(`ai: message content exceeds ${MAX_TEXT_PART_LEN} chars`);
|
|
6066
|
+
return;
|
|
6067
|
+
}
|
|
6068
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
6069
|
+
fail("ai: message content must be a non-empty string or array of parts");
|
|
6070
|
+
}
|
|
6071
|
+
if (content.length > MAX_CONTENT_PARTS) fail(`ai: message content has more than ${MAX_CONTENT_PARTS} parts`);
|
|
6072
|
+
for (const part of content) {
|
|
6073
|
+
if (!part || typeof part !== "object") fail("ai: each content part must be an object");
|
|
6074
|
+
const type = part.type;
|
|
6075
|
+
if (type === "text") {
|
|
6076
|
+
const text = part.text;
|
|
6077
|
+
if (typeof text !== "string" || text.length === 0) fail("ai: text part must have non-empty { text: string }");
|
|
6078
|
+
if (text.length > MAX_TEXT_PART_LEN) fail(`ai: text part exceeds ${MAX_TEXT_PART_LEN} chars`);
|
|
6079
|
+
} else if (type === "image_url") {
|
|
6080
|
+
if (role !== "user") {
|
|
6081
|
+
fail(`ai: image_url is only allowed in role 'user' messages (got '${role}')`);
|
|
6082
|
+
}
|
|
6083
|
+
const iu = part.image_url;
|
|
6084
|
+
if (!iu || typeof iu !== "object") fail("ai: image_url part must have { image_url: { url } }");
|
|
6085
|
+
validateImageUrl(iu.url);
|
|
6086
|
+
const detail = iu.detail;
|
|
6087
|
+
if (detail !== void 0 && (typeof detail !== "string" || !IMAGE_DETAIL_VALUES.has(detail))) {
|
|
6088
|
+
fail("ai: image_url.detail must be 'low' | 'high' | 'auto'");
|
|
6089
|
+
}
|
|
6090
|
+
} else {
|
|
6091
|
+
fail("ai: content part type must be 'text' or 'image_url'");
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
function validateResponseFormat(rf) {
|
|
6096
|
+
if (!rf || typeof rf !== "object") fail("ai: responseFormat must be an object");
|
|
6097
|
+
const type = rf.type;
|
|
6098
|
+
if (type === "text" || type === "json_object") return;
|
|
6099
|
+
if (type === "json_schema") {
|
|
6100
|
+
const js = rf.json_schema;
|
|
6101
|
+
if (!js || typeof js !== "object") fail("ai: responseFormat json_schema requires { json_schema: { name } }");
|
|
6102
|
+
const jso = js;
|
|
6103
|
+
const name = jso.name;
|
|
6104
|
+
if (typeof name !== "string" || name.length === 0) fail("ai: responseFormat json_schema.name is required");
|
|
6105
|
+
if (name.length > 64) fail("ai: responseFormat json_schema.name exceeds 64 chars");
|
|
6106
|
+
if (!RESPONSE_FORMAT_NAME_RE.test(name)) fail("ai: responseFormat json_schema.name must match a-zA-Z0-9_-");
|
|
6107
|
+
if (jso.description !== void 0) {
|
|
6108
|
+
if (typeof jso.description !== "string") fail("ai: responseFormat json_schema.description must be a string");
|
|
6109
|
+
if (jso.description.length > 1e3) fail("ai: responseFormat json_schema.description exceeds 1000 chars");
|
|
6110
|
+
}
|
|
6111
|
+
if (jso.strict !== void 0 && typeof jso.strict !== "boolean") {
|
|
6112
|
+
fail("ai: responseFormat json_schema.strict must be a boolean");
|
|
6113
|
+
}
|
|
6114
|
+
if (jso.schema !== void 0) {
|
|
6115
|
+
const err = validateStructuredSchema(jso.schema);
|
|
6116
|
+
if (err) fail(err);
|
|
6117
|
+
}
|
|
6118
|
+
return;
|
|
6119
|
+
}
|
|
6120
|
+
fail("ai: responseFormat.type must be 'text' | 'json_object' | 'json_schema'");
|
|
6121
|
+
}
|
|
5611
6122
|
function resolveAiBody(config, opts, stream) {
|
|
5612
6123
|
if (!opts || typeof opts !== "object") {
|
|
5613
6124
|
throw new exports.NeetruError("validation_failed", "ai: options object is required");
|
|
@@ -5615,17 +6126,26 @@ function resolveAiBody(config, opts, stream) {
|
|
|
5615
6126
|
if (typeof opts.model !== "string" || opts.model.length === 0) {
|
|
5616
6127
|
throw new exports.NeetruError("validation_failed", "ai: model is required");
|
|
5617
6128
|
}
|
|
6129
|
+
if (opts.model.length > MAX_MODEL_LEN) {
|
|
6130
|
+
throw new exports.NeetruError("validation_failed", `ai: model exceeds ${MAX_MODEL_LEN} chars`);
|
|
6131
|
+
}
|
|
5618
6132
|
if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
|
|
5619
6133
|
throw new exports.NeetruError("validation_failed", "ai: messages must be a non-empty array");
|
|
5620
6134
|
}
|
|
6135
|
+
if (opts.messages.length > MAX_MESSAGES) {
|
|
6136
|
+
throw new exports.NeetruError("validation_failed", `ai: messages exceeds ${MAX_MESSAGES} per request`);
|
|
6137
|
+
}
|
|
5621
6138
|
for (const m of opts.messages) {
|
|
5622
|
-
if (!m || typeof m !== "object" || typeof m.role !== "string" || m.role
|
|
6139
|
+
if (!m || typeof m !== "object" || typeof m.role !== "string" || !AI_ROLES.has(m.role)) {
|
|
5623
6140
|
throw new exports.NeetruError(
|
|
5624
6141
|
"validation_failed",
|
|
5625
|
-
"ai: each message must be
|
|
6142
|
+
"ai: each message role must be 'system' | 'user' | 'assistant'"
|
|
5626
6143
|
);
|
|
5627
6144
|
}
|
|
6145
|
+
validateAiContent(m.content, m.role);
|
|
5628
6146
|
}
|
|
6147
|
+
const aggErr = checkAggregateMultimodalLimits(opts.messages);
|
|
6148
|
+
if (aggErr) throw new exports.NeetruError("validation_failed", aggErr);
|
|
5629
6149
|
const productId = opts.productId ?? config.productId;
|
|
5630
6150
|
const tenantId = opts.tenantId ?? config.tenantId;
|
|
5631
6151
|
if (!productId) {
|
|
@@ -5650,17 +6170,27 @@ function resolveAiBody(config, opts, stream) {
|
|
|
5650
6170
|
stream
|
|
5651
6171
|
};
|
|
5652
6172
|
if (opts.maxTokens !== void 0) {
|
|
5653
|
-
if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0) {
|
|
5654
|
-
throw new exports.NeetruError(
|
|
6173
|
+
if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0 || opts.maxTokens > MAX_TOKENS_CAP) {
|
|
6174
|
+
throw new exports.NeetruError(
|
|
6175
|
+
"validation_failed",
|
|
6176
|
+
`ai: maxTokens must be a positive integer <= ${MAX_TOKENS_CAP}`
|
|
6177
|
+
);
|
|
5655
6178
|
}
|
|
5656
6179
|
body.maxTokens = opts.maxTokens;
|
|
5657
6180
|
}
|
|
5658
6181
|
if (opts.temperature !== void 0) {
|
|
5659
|
-
if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature)) {
|
|
5660
|
-
throw new exports.NeetruError(
|
|
6182
|
+
if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature) || opts.temperature < MIN_TEMPERATURE || opts.temperature > MAX_TEMPERATURE) {
|
|
6183
|
+
throw new exports.NeetruError(
|
|
6184
|
+
"validation_failed",
|
|
6185
|
+
`ai: temperature must be a finite number between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
|
|
6186
|
+
);
|
|
5661
6187
|
}
|
|
5662
6188
|
body.temperature = opts.temperature;
|
|
5663
6189
|
}
|
|
6190
|
+
if (opts.responseFormat !== void 0) {
|
|
6191
|
+
validateResponseFormat(opts.responseFormat);
|
|
6192
|
+
body.responseFormat = opts.responseFormat;
|
|
6193
|
+
}
|
|
5664
6194
|
return body;
|
|
5665
6195
|
}
|
|
5666
6196
|
async function safeJson2(res) {
|
|
@@ -6443,9 +6973,15 @@ var MockDb = class {
|
|
|
6443
6973
|
};
|
|
6444
6974
|
var MockAi = class {
|
|
6445
6975
|
_calls = [];
|
|
6976
|
+
/** Extrai texto de um content string OU array multimodal (imagens ignoradas). */
|
|
6977
|
+
_contentToText(content) {
|
|
6978
|
+
if (typeof content === "string") return content;
|
|
6979
|
+
if (!Array.isArray(content)) return "";
|
|
6980
|
+
return content.filter((p) => !!p && p.type === "text").map((p) => p.text).join(" ");
|
|
6981
|
+
}
|
|
6446
6982
|
_reply(opts) {
|
|
6447
6983
|
const last = [...opts.messages].reverse().find((m) => m.role === "user") ?? opts.messages[opts.messages.length - 1];
|
|
6448
|
-
const content = last?.content
|
|
6984
|
+
const content = this._contentToText(last?.content);
|
|
6449
6985
|
return content ? `mock-reply: ${content}` : "mock-reply";
|
|
6450
6986
|
}
|
|
6451
6987
|
async chat(opts) {
|
|
@@ -6457,7 +6993,7 @@ var MockAi = class {
|
|
|
6457
6993
|
}
|
|
6458
6994
|
this._calls.push({ model: opts.model, messages: opts.messages, stream: false });
|
|
6459
6995
|
const text = this._reply(opts);
|
|
6460
|
-
const promptTokens = opts.messages.reduce((n, m) => n + (m.content
|
|
6996
|
+
const promptTokens = opts.messages.reduce((n, m) => n + this._contentToText(m.content).length, 0);
|
|
6461
6997
|
const completionTokens = text.length;
|
|
6462
6998
|
return {
|
|
6463
6999
|
choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
|
|
@@ -7128,7 +7664,7 @@ function createNeetruClient(config = {}) {
|
|
|
7128
7664
|
init_errors();
|
|
7129
7665
|
init_db_errors();
|
|
7130
7666
|
init_http();
|
|
7131
|
-
var VERSION = "3.
|
|
7667
|
+
var VERSION = "3.3.0";
|
|
7132
7668
|
|
|
7133
7669
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
7134
7670
|
exports.DEV_FIXTURE_USER = DEV_FIXTURE_USER;
|