@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.
- package/CHANGELOG.md +45 -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 +581 -18
- 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 +581 -18
- 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 +193 -8
- 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 +193 -9
- 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 +582 -19
- 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 +582 -19
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +87 -4
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +19 -1
- package/dist/mocks.d.ts +19 -1
- package/dist/mocks.mjs +87 -4
- 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 +117 -4
- 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 +117 -4
- 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-BoFSCHF_.d.cts} +185 -6
- package/dist/{types-iLDjiqqk.d.ts → types-DdJyqBy-.d.ts} +185 -6
- 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/auth.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 NeetruError(code, message, res.status, requestId);
|
|
225
|
+
const err = new 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:
|
|
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 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 NeetruDbError) throw err;
|
|
616
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
617
|
+
throw new 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 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 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 NeetruDbError(
|
|
734
|
+
"db_suspended",
|
|
735
|
+
`Falha ao ${verb} lease SQL: ${human} (reason=${reason}).`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
634
738
|
throw new NeetruDbError(
|
|
635
739
|
"db_unavailable",
|
|
636
|
-
`Falha ao ${
|
|
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 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 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 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 NeetruError("validation_failed", "ticketId is required");
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function assertValidComment(comment) {
|
|
1614
|
+
if (typeof comment === "string" && comment.length > 2e3) {
|
|
1615
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
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 NeetruError("validation_failed", "input is required");
|
|
1743
|
+
}
|
|
1744
|
+
assertTicketId(input.ticketId);
|
|
1745
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
1746
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
1747
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
assertValidComment(input.comment);
|
|
1751
|
+
await httpRequest(config, {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(input.ticketId)}/satisfaction`,
|
|
1754
|
+
body: {
|
|
1755
|
+
rating: input.rating ?? null,
|
|
1756
|
+
comment: input.comment ?? null,
|
|
1757
|
+
skipped: input.skipped
|
|
1758
|
+
},
|
|
1759
|
+
requireAuth: true
|
|
1760
|
+
});
|
|
1761
|
+
},
|
|
1762
|
+
/**
|
|
1763
|
+
* 3.4: contesta um ticket resolvido — reabre e zera o ciclo de
|
|
1764
|
+
* satisfação/notificação. Endpoint canonical:
|
|
1765
|
+
* `POST /api/sdk/v1/support/tickets/{id}/contest`.
|
|
1766
|
+
*/
|
|
1767
|
+
async contestTicket(ticketId, opts) {
|
|
1768
|
+
assertTicketId(ticketId);
|
|
1769
|
+
assertValidComment(opts?.comment);
|
|
1770
|
+
await httpRequest(config, {
|
|
1771
|
+
method: "POST",
|
|
1772
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/contest`,
|
|
1773
|
+
body: { comment: opts?.comment ?? void 0 },
|
|
1774
|
+
requireAuth: true
|
|
1775
|
+
});
|
|
1511
1776
|
}
|
|
1512
1777
|
};
|
|
1513
1778
|
}
|
|
@@ -4928,7 +5193,20 @@ function buildTicketProvider(config, opts) {
|
|
|
4928
5193
|
return ticket;
|
|
4929
5194
|
};
|
|
4930
5195
|
}
|
|
5196
|
+
var _hasWarnedMissingDbId = false;
|
|
5197
|
+
function warnMissingDbIdOnce(config, dbOpts) {
|
|
5198
|
+
if (_hasWarnedMissingDbId) return;
|
|
5199
|
+
if (dbOpts.dbId) return;
|
|
5200
|
+
if (config.env === "prod") return;
|
|
5201
|
+
if (typeof window === "undefined") return;
|
|
5202
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
5203
|
+
_hasWarnedMissingDbId = true;
|
|
5204
|
+
console.warn(
|
|
5205
|
+
"[neetru/sdk] client.db sem `dbId` expl\xEDcito: escritas client-side (add/set/update/remove/list) caem no datastore gen\xE9rico do Core, escopado por tenant/produto (header x-neetru-tenant) \u2014 `dbId` s\xF3 nomeia o cache offline LOCAL (IndexedDB), n\xE3o roteia nem escopa nada no servidor. Veja o JSDoc de NeetruDbOptions.dbId."
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
4931
5208
|
function createNeetruDb(config, dbOpts = {}) {
|
|
5209
|
+
warnMissingDbIdOnce(config, dbOpts);
|
|
4932
5210
|
let resolvedTransport;
|
|
4933
5211
|
if (dbOpts.transport !== void 0) {
|
|
4934
5212
|
resolvedTransport = dbOpts.transport;
|
|
@@ -5661,6 +5939,199 @@ init_errors();
|
|
|
5661
5939
|
init_http();
|
|
5662
5940
|
var AI_CHAT_PATH = "/api/sdk/v1/ai/chat";
|
|
5663
5941
|
var AI_NONSTREAM_TIMEOUT_MS = 12e4;
|
|
5942
|
+
var MAX_TEXT_PART_LEN = 32768;
|
|
5943
|
+
var MAX_CONTENT_PARTS = 32;
|
|
5944
|
+
var MAX_DATA_URL_LEN = 4194304;
|
|
5945
|
+
var MAX_HTTPS_URL_LEN = 2048;
|
|
5946
|
+
var DATA_IMAGE_URL_RE = /^data:image\/(?:png|jpe?g|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
|
|
5947
|
+
var HTTPS_URL_RE = /^https:\/\/[^\s]+$/;
|
|
5948
|
+
var IMAGE_DETAIL_VALUES = /* @__PURE__ */ new Set(["low", "high", "auto"]);
|
|
5949
|
+
var AI_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
|
|
5950
|
+
var MAX_MESSAGES = 100;
|
|
5951
|
+
var MAX_MODEL_LEN = 128;
|
|
5952
|
+
var MAX_TOKENS_CAP = 32768;
|
|
5953
|
+
var MIN_TEMPERATURE = 0;
|
|
5954
|
+
var MAX_TEMPERATURE = 2;
|
|
5955
|
+
var MAX_TOTAL_CONTENT_PARTS = 200;
|
|
5956
|
+
var MAX_TOTAL_IMAGES = 20;
|
|
5957
|
+
var MAX_TOTAL_IMAGE_BYTES = 12 * 1024 * 1024;
|
|
5958
|
+
var MAX_SCHEMA_JSON_LEN = 5e4;
|
|
5959
|
+
var MAX_SCHEMA_DEPTH = 16;
|
|
5960
|
+
var MAX_SCHEMA_KEYS = 500;
|
|
5961
|
+
var FORBIDDEN_SCHEMA_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
5962
|
+
var RESPONSE_FORMAT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
5963
|
+
function fail(msg) {
|
|
5964
|
+
throw new NeetruError("validation_failed", msg);
|
|
5965
|
+
}
|
|
5966
|
+
function isStrictBase64Payload(b64) {
|
|
5967
|
+
if (b64.length === 0 || b64.length % 4 !== 0) return false;
|
|
5968
|
+
const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
5969
|
+
const body = pad > 0 ? b64.slice(0, b64.length - pad) : b64;
|
|
5970
|
+
return !body.includes("=");
|
|
5971
|
+
}
|
|
5972
|
+
function dataUrlDecodedBytes(url) {
|
|
5973
|
+
const comma = url.indexOf(",");
|
|
5974
|
+
if (comma === -1) return 0;
|
|
5975
|
+
const b64 = url.slice(comma + 1);
|
|
5976
|
+
const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
5977
|
+
return Math.max(0, Math.floor(b64.length * 3 / 4) - pad);
|
|
5978
|
+
}
|
|
5979
|
+
function checkAggregateMultimodalLimits(messages) {
|
|
5980
|
+
let totalParts = 0;
|
|
5981
|
+
let totalImages = 0;
|
|
5982
|
+
let totalImageBytes = 0;
|
|
5983
|
+
for (const m of messages) {
|
|
5984
|
+
const content = m?.content;
|
|
5985
|
+
if (!Array.isArray(content)) continue;
|
|
5986
|
+
for (const part of content) {
|
|
5987
|
+
totalParts++;
|
|
5988
|
+
if (totalParts > MAX_TOTAL_CONTENT_PARTS) {
|
|
5989
|
+
return `ai: total content parts exceeds ${MAX_TOTAL_CONTENT_PARTS} per request`;
|
|
5990
|
+
}
|
|
5991
|
+
if (part && typeof part === "object" && part.type === "image_url") {
|
|
5992
|
+
totalImages++;
|
|
5993
|
+
if (totalImages > MAX_TOTAL_IMAGES) {
|
|
5994
|
+
return `ai: total images exceeds ${MAX_TOTAL_IMAGES} per request`;
|
|
5995
|
+
}
|
|
5996
|
+
const url = part.image_url?.url;
|
|
5997
|
+
if (typeof url === "string" && url.startsWith("data:")) {
|
|
5998
|
+
totalImageBytes += dataUrlDecodedBytes(url);
|
|
5999
|
+
if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
|
|
6000
|
+
return `ai: aggregate image bytes exceeds ${MAX_TOTAL_IMAGE_BYTES} per request`;
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
return null;
|
|
6007
|
+
}
|
|
6008
|
+
function validateStructuredSchema(schema) {
|
|
6009
|
+
if (schema === void 0) return null;
|
|
6010
|
+
if (schema === null || typeof schema !== "object") {
|
|
6011
|
+
return "ai: responseFormat json_schema.schema must be an object";
|
|
6012
|
+
}
|
|
6013
|
+
let serialized;
|
|
6014
|
+
try {
|
|
6015
|
+
serialized = JSON.stringify(schema);
|
|
6016
|
+
} catch {
|
|
6017
|
+
return "ai: responseFormat json_schema.schema is not serializable";
|
|
6018
|
+
}
|
|
6019
|
+
if (serialized === void 0) return "ai: responseFormat json_schema.schema is invalid";
|
|
6020
|
+
if (serialized.length > MAX_SCHEMA_JSON_LEN) {
|
|
6021
|
+
return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_JSON_LEN} serialized chars`;
|
|
6022
|
+
}
|
|
6023
|
+
let keyCount = 0;
|
|
6024
|
+
const walk = (node, depth) => {
|
|
6025
|
+
if (depth > MAX_SCHEMA_DEPTH) {
|
|
6026
|
+
return `ai: responseFormat json_schema.schema exceeds max depth ${MAX_SCHEMA_DEPTH}`;
|
|
6027
|
+
}
|
|
6028
|
+
if (Array.isArray(node)) {
|
|
6029
|
+
for (const item of node) {
|
|
6030
|
+
const e = walk(item, depth + 1);
|
|
6031
|
+
if (e) return e;
|
|
6032
|
+
}
|
|
6033
|
+
return null;
|
|
6034
|
+
}
|
|
6035
|
+
if (node && typeof node === "object") {
|
|
6036
|
+
for (const key of Object.getOwnPropertyNames(node)) {
|
|
6037
|
+
if (FORBIDDEN_SCHEMA_KEYS.has(key)) {
|
|
6038
|
+
return `ai: responseFormat json_schema.schema has forbidden key: ${key}`;
|
|
6039
|
+
}
|
|
6040
|
+
keyCount++;
|
|
6041
|
+
if (keyCount > MAX_SCHEMA_KEYS) {
|
|
6042
|
+
return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_KEYS} keys`;
|
|
6043
|
+
}
|
|
6044
|
+
const e = walk(node[key], depth + 1);
|
|
6045
|
+
if (e) return e;
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
return null;
|
|
6049
|
+
};
|
|
6050
|
+
return walk(schema, 0);
|
|
6051
|
+
}
|
|
6052
|
+
function validateImageUrl(url) {
|
|
6053
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
6054
|
+
fail("ai: image_url.url must be a non-empty string");
|
|
6055
|
+
}
|
|
6056
|
+
const u = url;
|
|
6057
|
+
if (u.startsWith("data:")) {
|
|
6058
|
+
if (u.length > MAX_DATA_URL_LEN) fail(`ai: image_url.url (data:) exceeds ${MAX_DATA_URL_LEN} chars`);
|
|
6059
|
+
if (!DATA_IMAGE_URL_RE.test(u)) {
|
|
6060
|
+
fail("ai: image_url.url data: must be data:image/<png|jpeg|jpg|gif|webp>;base64,<...>");
|
|
6061
|
+
}
|
|
6062
|
+
const b64 = u.slice(u.indexOf(",") + 1);
|
|
6063
|
+
if (!isStrictBase64Payload(b64)) {
|
|
6064
|
+
fail("ai: image_url.url data: has invalid base64 (length/padding)");
|
|
6065
|
+
}
|
|
6066
|
+
return;
|
|
6067
|
+
}
|
|
6068
|
+
if (u.startsWith("https://")) {
|
|
6069
|
+
if (u.length > MAX_HTTPS_URL_LEN) fail(`ai: image_url.url (https:) exceeds ${MAX_HTTPS_URL_LEN} chars`);
|
|
6070
|
+
if (!HTTPS_URL_RE.test(u)) fail("ai: image_url.url https: is invalid");
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
fail("ai: image_url.url must be a data:image/...;base64 or https:// URL (other schemes are rejected)");
|
|
6074
|
+
}
|
|
6075
|
+
function validateAiContent(content, role) {
|
|
6076
|
+
if (typeof content === "string") {
|
|
6077
|
+
if (content.length === 0) fail("ai: message content string must be non-empty");
|
|
6078
|
+
if (content.length > MAX_TEXT_PART_LEN) fail(`ai: message content exceeds ${MAX_TEXT_PART_LEN} chars`);
|
|
6079
|
+
return;
|
|
6080
|
+
}
|
|
6081
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
6082
|
+
fail("ai: message content must be a non-empty string or array of parts");
|
|
6083
|
+
}
|
|
6084
|
+
if (content.length > MAX_CONTENT_PARTS) fail(`ai: message content has more than ${MAX_CONTENT_PARTS} parts`);
|
|
6085
|
+
for (const part of content) {
|
|
6086
|
+
if (!part || typeof part !== "object") fail("ai: each content part must be an object");
|
|
6087
|
+
const type = part.type;
|
|
6088
|
+
if (type === "text") {
|
|
6089
|
+
const text = part.text;
|
|
6090
|
+
if (typeof text !== "string" || text.length === 0) fail("ai: text part must have non-empty { text: string }");
|
|
6091
|
+
if (text.length > MAX_TEXT_PART_LEN) fail(`ai: text part exceeds ${MAX_TEXT_PART_LEN} chars`);
|
|
6092
|
+
} else if (type === "image_url") {
|
|
6093
|
+
if (role !== "user") {
|
|
6094
|
+
fail(`ai: image_url is only allowed in role 'user' messages (got '${role}')`);
|
|
6095
|
+
}
|
|
6096
|
+
const iu = part.image_url;
|
|
6097
|
+
if (!iu || typeof iu !== "object") fail("ai: image_url part must have { image_url: { url } }");
|
|
6098
|
+
validateImageUrl(iu.url);
|
|
6099
|
+
const detail = iu.detail;
|
|
6100
|
+
if (detail !== void 0 && (typeof detail !== "string" || !IMAGE_DETAIL_VALUES.has(detail))) {
|
|
6101
|
+
fail("ai: image_url.detail must be 'low' | 'high' | 'auto'");
|
|
6102
|
+
}
|
|
6103
|
+
} else {
|
|
6104
|
+
fail("ai: content part type must be 'text' or 'image_url'");
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
function validateResponseFormat(rf) {
|
|
6109
|
+
if (!rf || typeof rf !== "object") fail("ai: responseFormat must be an object");
|
|
6110
|
+
const type = rf.type;
|
|
6111
|
+
if (type === "text" || type === "json_object") return;
|
|
6112
|
+
if (type === "json_schema") {
|
|
6113
|
+
const js = rf.json_schema;
|
|
6114
|
+
if (!js || typeof js !== "object") fail("ai: responseFormat json_schema requires { json_schema: { name } }");
|
|
6115
|
+
const jso = js;
|
|
6116
|
+
const name = jso.name;
|
|
6117
|
+
if (typeof name !== "string" || name.length === 0) fail("ai: responseFormat json_schema.name is required");
|
|
6118
|
+
if (name.length > 64) fail("ai: responseFormat json_schema.name exceeds 64 chars");
|
|
6119
|
+
if (!RESPONSE_FORMAT_NAME_RE.test(name)) fail("ai: responseFormat json_schema.name must match a-zA-Z0-9_-");
|
|
6120
|
+
if (jso.description !== void 0) {
|
|
6121
|
+
if (typeof jso.description !== "string") fail("ai: responseFormat json_schema.description must be a string");
|
|
6122
|
+
if (jso.description.length > 1e3) fail("ai: responseFormat json_schema.description exceeds 1000 chars");
|
|
6123
|
+
}
|
|
6124
|
+
if (jso.strict !== void 0 && typeof jso.strict !== "boolean") {
|
|
6125
|
+
fail("ai: responseFormat json_schema.strict must be a boolean");
|
|
6126
|
+
}
|
|
6127
|
+
if (jso.schema !== void 0) {
|
|
6128
|
+
const err = validateStructuredSchema(jso.schema);
|
|
6129
|
+
if (err) fail(err);
|
|
6130
|
+
}
|
|
6131
|
+
return;
|
|
6132
|
+
}
|
|
6133
|
+
fail("ai: responseFormat.type must be 'text' | 'json_object' | 'json_schema'");
|
|
6134
|
+
}
|
|
5664
6135
|
function resolveAiBody(config, opts, stream) {
|
|
5665
6136
|
if (!opts || typeof opts !== "object") {
|
|
5666
6137
|
throw new NeetruError("validation_failed", "ai: options object is required");
|
|
@@ -5668,17 +6139,26 @@ function resolveAiBody(config, opts, stream) {
|
|
|
5668
6139
|
if (typeof opts.model !== "string" || opts.model.length === 0) {
|
|
5669
6140
|
throw new NeetruError("validation_failed", "ai: model is required");
|
|
5670
6141
|
}
|
|
6142
|
+
if (opts.model.length > MAX_MODEL_LEN) {
|
|
6143
|
+
throw new NeetruError("validation_failed", `ai: model exceeds ${MAX_MODEL_LEN} chars`);
|
|
6144
|
+
}
|
|
5671
6145
|
if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
|
|
5672
6146
|
throw new NeetruError("validation_failed", "ai: messages must be a non-empty array");
|
|
5673
6147
|
}
|
|
6148
|
+
if (opts.messages.length > MAX_MESSAGES) {
|
|
6149
|
+
throw new NeetruError("validation_failed", `ai: messages exceeds ${MAX_MESSAGES} per request`);
|
|
6150
|
+
}
|
|
5674
6151
|
for (const m of opts.messages) {
|
|
5675
|
-
if (!m || typeof m !== "object" || typeof m.role !== "string" || m.role
|
|
6152
|
+
if (!m || typeof m !== "object" || typeof m.role !== "string" || !AI_ROLES.has(m.role)) {
|
|
5676
6153
|
throw new NeetruError(
|
|
5677
6154
|
"validation_failed",
|
|
5678
|
-
"ai: each message must be
|
|
6155
|
+
"ai: each message role must be 'system' | 'user' | 'assistant'"
|
|
5679
6156
|
);
|
|
5680
6157
|
}
|
|
6158
|
+
validateAiContent(m.content, m.role);
|
|
5681
6159
|
}
|
|
6160
|
+
const aggErr = checkAggregateMultimodalLimits(opts.messages);
|
|
6161
|
+
if (aggErr) throw new NeetruError("validation_failed", aggErr);
|
|
5682
6162
|
const productId = opts.productId ?? config.productId;
|
|
5683
6163
|
const tenantId = opts.tenantId ?? config.tenantId;
|
|
5684
6164
|
if (!productId) {
|
|
@@ -5703,17 +6183,27 @@ function resolveAiBody(config, opts, stream) {
|
|
|
5703
6183
|
stream
|
|
5704
6184
|
};
|
|
5705
6185
|
if (opts.maxTokens !== void 0) {
|
|
5706
|
-
if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0) {
|
|
5707
|
-
throw new NeetruError(
|
|
6186
|
+
if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0 || opts.maxTokens > MAX_TOKENS_CAP) {
|
|
6187
|
+
throw new NeetruError(
|
|
6188
|
+
"validation_failed",
|
|
6189
|
+
`ai: maxTokens must be a positive integer <= ${MAX_TOKENS_CAP}`
|
|
6190
|
+
);
|
|
5708
6191
|
}
|
|
5709
6192
|
body.maxTokens = opts.maxTokens;
|
|
5710
6193
|
}
|
|
5711
6194
|
if (opts.temperature !== void 0) {
|
|
5712
|
-
if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature)) {
|
|
5713
|
-
throw new NeetruError(
|
|
6195
|
+
if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature) || opts.temperature < MIN_TEMPERATURE || opts.temperature > MAX_TEMPERATURE) {
|
|
6196
|
+
throw new NeetruError(
|
|
6197
|
+
"validation_failed",
|
|
6198
|
+
`ai: temperature must be a finite number between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
|
|
6199
|
+
);
|
|
5714
6200
|
}
|
|
5715
6201
|
body.temperature = opts.temperature;
|
|
5716
6202
|
}
|
|
6203
|
+
if (opts.responseFormat !== void 0) {
|
|
6204
|
+
validateResponseFormat(opts.responseFormat);
|
|
6205
|
+
body.responseFormat = opts.responseFormat;
|
|
6206
|
+
}
|
|
5717
6207
|
return body;
|
|
5718
6208
|
}
|
|
5719
6209
|
async function safeJson2(res) {
|
|
@@ -6139,7 +6629,18 @@ var MockSupport = class {
|
|
|
6139
6629
|
severity: input.severity ?? "normal",
|
|
6140
6630
|
status: "open",
|
|
6141
6631
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6142
|
-
productSlug: input.productSlug
|
|
6632
|
+
productSlug: input.productSlug,
|
|
6633
|
+
// 3.4 (product-feedback): espelha o contrato — campos novos nullable,
|
|
6634
|
+
// populados a partir do input quando fornecidos.
|
|
6635
|
+
pageUrl: typeof input.pageUrl === "string" ? input.pageUrl : null,
|
|
6636
|
+
screenshotUrl: typeof input.screenshotBase64 === "string" ? input.screenshotBase64 : null,
|
|
6637
|
+
resolvedAt: null,
|
|
6638
|
+
resolvedNotifiedAt: null,
|
|
6639
|
+
satisfactionAttempts: 0,
|
|
6640
|
+
lastSatisfactionAttemptAt: null,
|
|
6641
|
+
satisfiedAt: null,
|
|
6642
|
+
rating: null,
|
|
6643
|
+
satisfactionComment: null
|
|
6143
6644
|
};
|
|
6144
6645
|
this._tickets.push(ticket);
|
|
6145
6646
|
return ticket;
|
|
@@ -6150,6 +6651,62 @@ var MockSupport = class {
|
|
|
6150
6651
|
}
|
|
6151
6652
|
return [...this._tickets];
|
|
6152
6653
|
}
|
|
6654
|
+
_find(ticketId) {
|
|
6655
|
+
if (!ticketId || typeof ticketId !== "string") {
|
|
6656
|
+
throw new NeetruError("validation_failed", "ticketId is required");
|
|
6657
|
+
}
|
|
6658
|
+
const ticket = this._tickets.find((t) => t.id === ticketId);
|
|
6659
|
+
if (!ticket) {
|
|
6660
|
+
throw new NeetruError("not_found", `ticket ${ticketId} not found`);
|
|
6661
|
+
}
|
|
6662
|
+
return ticket;
|
|
6663
|
+
}
|
|
6664
|
+
/** 3.4: espelha `markResolvedSeen` — idempotente (só seta se ainda null). */
|
|
6665
|
+
async markResolvedSeen(ticketId) {
|
|
6666
|
+
const ticket = this._find(ticketId);
|
|
6667
|
+
if (ticket.resolvedNotifiedAt === null) {
|
|
6668
|
+
ticket.resolvedNotifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
/** 3.4: espelha `recordSatisfactionAttempt` — incrementa contador. */
|
|
6672
|
+
async recordSatisfactionAttempt(ticketId) {
|
|
6673
|
+
const ticket = this._find(ticketId);
|
|
6674
|
+
ticket.satisfactionAttempts += 1;
|
|
6675
|
+
ticket.lastSatisfactionAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6676
|
+
}
|
|
6677
|
+
/** 3.4: espelha `submitSatisfaction` — terminal, seta `satisfiedAt`. */
|
|
6678
|
+
async submitSatisfaction(input) {
|
|
6679
|
+
if (!input || typeof input !== "object") {
|
|
6680
|
+
throw new NeetruError("validation_failed", "input is required");
|
|
6681
|
+
}
|
|
6682
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
6683
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
6684
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
if (typeof input.comment === "string" && input.comment.length > 2e3) {
|
|
6688
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6689
|
+
}
|
|
6690
|
+
const ticket = this._find(input.ticketId);
|
|
6691
|
+
ticket.satisfiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6692
|
+
ticket.rating = input.rating ?? null;
|
|
6693
|
+
ticket.satisfactionComment = input.comment ?? null;
|
|
6694
|
+
}
|
|
6695
|
+
/** 3.4: espelha `contestTicket` — reabre e zera o ciclo de satisfação. */
|
|
6696
|
+
async contestTicket(ticketId, opts) {
|
|
6697
|
+
if (typeof opts?.comment === "string" && opts.comment.length > 2e3) {
|
|
6698
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6699
|
+
}
|
|
6700
|
+
const ticket = this._find(ticketId);
|
|
6701
|
+
ticket.status = "open";
|
|
6702
|
+
ticket.resolvedAt = null;
|
|
6703
|
+
ticket.resolvedNotifiedAt = null;
|
|
6704
|
+
ticket.satisfactionAttempts = 0;
|
|
6705
|
+
ticket.lastSatisfactionAttemptAt = null;
|
|
6706
|
+
ticket.satisfiedAt = null;
|
|
6707
|
+
ticket.rating = null;
|
|
6708
|
+
ticket.satisfactionComment = null;
|
|
6709
|
+
}
|
|
6153
6710
|
/** Test helper. */
|
|
6154
6711
|
__reset() {
|
|
6155
6712
|
this._tickets = [];
|
|
@@ -6181,9 +6738,15 @@ var MockEntitlements = class {
|
|
|
6181
6738
|
};
|
|
6182
6739
|
var MockAi = class {
|
|
6183
6740
|
_calls = [];
|
|
6741
|
+
/** Extrai texto de um content string OU array multimodal (imagens ignoradas). */
|
|
6742
|
+
_contentToText(content) {
|
|
6743
|
+
if (typeof content === "string") return content;
|
|
6744
|
+
if (!Array.isArray(content)) return "";
|
|
6745
|
+
return content.filter((p) => !!p && p.type === "text").map((p) => p.text).join(" ");
|
|
6746
|
+
}
|
|
6184
6747
|
_reply(opts) {
|
|
6185
6748
|
const last = [...opts.messages].reverse().find((m) => m.role === "user") ?? opts.messages[opts.messages.length - 1];
|
|
6186
|
-
const content = last?.content
|
|
6749
|
+
const content = this._contentToText(last?.content);
|
|
6187
6750
|
return content ? `mock-reply: ${content}` : "mock-reply";
|
|
6188
6751
|
}
|
|
6189
6752
|
async chat(opts) {
|
|
@@ -6195,7 +6758,7 @@ var MockAi = class {
|
|
|
6195
6758
|
}
|
|
6196
6759
|
this._calls.push({ model: opts.model, messages: opts.messages, stream: false });
|
|
6197
6760
|
const text = this._reply(opts);
|
|
6198
|
-
const promptTokens = opts.messages.reduce((n, m) => n + (m.content
|
|
6761
|
+
const promptTokens = opts.messages.reduce((n, m) => n + this._contentToText(m.content).length, 0);
|
|
6199
6762
|
const completionTokens = text.length;
|
|
6200
6763
|
return {
|
|
6201
6764
|
choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
|