@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/index.mjs
CHANGED
|
@@ -19,12 +19,22 @@ var init_errors = __esm({
|
|
|
19
19
|
code;
|
|
20
20
|
status;
|
|
21
21
|
requestId;
|
|
22
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -366,6 +382,7 @@ var init_resilient_pool = __esm({
|
|
|
366
382
|
|
|
367
383
|
// src/db/sql/lease.ts
|
|
368
384
|
function hasConnectionChanged(oldLease, newLease) {
|
|
385
|
+
if (!oldLease) return true;
|
|
369
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;
|
|
370
387
|
}
|
|
371
388
|
function mapPoolError(err) {
|
|
@@ -397,6 +414,10 @@ var init_lease = __esm({
|
|
|
397
414
|
RENEWAL_THRESHOLD = 0.8;
|
|
398
415
|
MIN_RENEWAL_DELAY_MS = 3e4;
|
|
399
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
|
+
*/
|
|
400
421
|
_lease;
|
|
401
422
|
_pool;
|
|
402
423
|
_orm;
|
|
@@ -408,6 +429,12 @@ var init_lease = __esm({
|
|
|
408
429
|
* `_reactiveReLease`.
|
|
409
430
|
*/
|
|
410
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;
|
|
411
438
|
_deps;
|
|
412
439
|
constructor(lease, pool, orm, deps) {
|
|
413
440
|
this._lease = lease;
|
|
@@ -417,7 +444,7 @@ var init_lease = __esm({
|
|
|
417
444
|
...deps,
|
|
418
445
|
now: deps.now ?? (() => Date.now())
|
|
419
446
|
};
|
|
420
|
-
this._scheduleRenewal();
|
|
447
|
+
if (this._lease) this._scheduleRenewal();
|
|
421
448
|
}
|
|
422
449
|
// ─── Superfície pública (NeetruSqlClient) ──────────────────────────────────
|
|
423
450
|
get orm() {
|
|
@@ -455,6 +482,7 @@ var init_lease = __esm({
|
|
|
455
482
|
* usa `MIN_RENEWAL_DELAY_MS` como fallback seguro.
|
|
456
483
|
*/
|
|
457
484
|
_calcRenewalDelayMs() {
|
|
485
|
+
if (!this._lease) return MIN_RENEWAL_DELAY_MS;
|
|
458
486
|
const expiresAtMs = Date.parse(this._lease.expiresAt);
|
|
459
487
|
if (!Number.isFinite(expiresAtMs)) return MIN_RENEWAL_DELAY_MS;
|
|
460
488
|
const now = this._deps.now();
|
|
@@ -478,6 +506,8 @@ var init_lease = __esm({
|
|
|
478
506
|
}
|
|
479
507
|
async _doRenew() {
|
|
480
508
|
if (this._closed) return;
|
|
509
|
+
const currentLease = this._lease;
|
|
510
|
+
if (!currentLease) return;
|
|
481
511
|
try {
|
|
482
512
|
const withDb = (o) => {
|
|
483
513
|
if (this._deps.database !== void 0) o.database = this._deps.database;
|
|
@@ -485,7 +515,7 @@ var init_lease = __esm({
|
|
|
485
515
|
};
|
|
486
516
|
let newLease;
|
|
487
517
|
try {
|
|
488
|
-
newLease = await this._deps.fetchLease(withDb({ leaseId:
|
|
518
|
+
newLease = await this._deps.fetchLease(withDb({ leaseId: currentLease.leaseId }));
|
|
489
519
|
} catch (renewErr) {
|
|
490
520
|
newLease = await this._deps.fetchLease(withDb({}));
|
|
491
521
|
}
|
|
@@ -545,6 +575,71 @@ var init_lease = __esm({
|
|
|
545
575
|
this._reLeaseInFlight = null;
|
|
546
576
|
}
|
|
547
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
|
+
}
|
|
548
643
|
/**
|
|
549
644
|
* Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
|
|
550
645
|
*
|
|
@@ -628,10 +723,19 @@ function createHttpLeaseFetcher(config) {
|
|
|
628
723
|
retries: 2
|
|
629
724
|
});
|
|
630
725
|
} catch (err) {
|
|
726
|
+
const verb = isRenewal ? "renovar" : "obter";
|
|
631
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
|
+
}
|
|
632
736
|
throw new NeetruDbError(
|
|
633
737
|
"db_unavailable",
|
|
634
|
-
`Falha ao ${
|
|
738
|
+
`Falha ao ${verb} lease SQL: ${msg}`
|
|
635
739
|
);
|
|
636
740
|
}
|
|
637
741
|
return parseLease(raw);
|
|
@@ -703,8 +807,64 @@ async function createDrizzleHandle(pool, schema) {
|
|
|
703
807
|
);
|
|
704
808
|
return drizzle(pool, { schema });
|
|
705
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
|
+
}
|
|
706
845
|
async function createSqlClientWithDeps(deps, options) {
|
|
707
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
|
+
}
|
|
708
868
|
let lease;
|
|
709
869
|
try {
|
|
710
870
|
if (options?.database !== void 0) {
|
|
@@ -757,8 +917,8 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
757
917
|
`[client.db.sql] requer pg + drizzle-orm como peerDependencies do servidor. Instale: npm install pg drizzle-orm. (${err.message})`
|
|
758
918
|
);
|
|
759
919
|
}
|
|
920
|
+
const Pool = pgMod.Pool ?? pgMod.default?.Pool;
|
|
760
921
|
const createPool = (lease) => {
|
|
761
|
-
const Pool = pgMod.Pool ?? pgMod.default?.Pool;
|
|
762
922
|
const ssl = lease.sslca ? {
|
|
763
923
|
ca: lease.sslca,
|
|
764
924
|
cert: lease.clientCert ?? void 0,
|
|
@@ -785,7 +945,14 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
785
945
|
fetchLease,
|
|
786
946
|
createPool,
|
|
787
947
|
createDrizzle,
|
|
788
|
-
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
|
|
789
956
|
},
|
|
790
957
|
options
|
|
791
958
|
);
|
|
@@ -794,6 +961,7 @@ var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
|
|
|
794
961
|
var init_sql_client = __esm({
|
|
795
962
|
"src/db/sql/sql-client.ts"() {
|
|
796
963
|
init_db_errors();
|
|
964
|
+
init_errors();
|
|
797
965
|
init_lease();
|
|
798
966
|
init_resilient_pool();
|
|
799
967
|
LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
|
|
@@ -1414,6 +1582,7 @@ function toTicket(raw) {
|
|
|
1414
1582
|
if (typeof r.id !== "string") {
|
|
1415
1583
|
throw new NeetruError("invalid_response", "Ticket missing id");
|
|
1416
1584
|
}
|
|
1585
|
+
const firstAttachment = Array.isArray(r.attachments) && typeof r.attachments[0] === "string" ? r.attachments[0] : void 0;
|
|
1417
1586
|
return {
|
|
1418
1587
|
id: r.id,
|
|
1419
1588
|
subject: typeof r.subject === "string" ? r.subject : "",
|
|
@@ -1422,9 +1591,28 @@ function toTicket(raw) {
|
|
|
1422
1591
|
status: VALID_STATUSES.includes(r.status) ? r.status : "open",
|
|
1423
1592
|
createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1424
1593
|
updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
|
|
1425
|
-
productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0
|
|
1594
|
+
productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0,
|
|
1595
|
+
pageUrl: typeof r.pageUrl === "string" ? r.pageUrl : null,
|
|
1596
|
+
screenshotUrl: typeof r.screenshotUrl === "string" ? r.screenshotUrl : firstAttachment ?? null,
|
|
1597
|
+
resolvedAt: typeof r.resolvedAt === "string" ? r.resolvedAt : null,
|
|
1598
|
+
resolvedNotifiedAt: typeof r.resolvedNotifiedAt === "string" ? r.resolvedNotifiedAt : null,
|
|
1599
|
+
satisfactionAttempts: typeof r.satisfactionAttempts === "number" && Number.isFinite(r.satisfactionAttempts) ? r.satisfactionAttempts : 0,
|
|
1600
|
+
lastSatisfactionAttemptAt: typeof r.lastSatisfactionAttemptAt === "string" ? r.lastSatisfactionAttemptAt : null,
|
|
1601
|
+
satisfiedAt: typeof r.satisfiedAt === "string" ? r.satisfiedAt : null,
|
|
1602
|
+
rating: typeof r.rating === "number" && Number.isFinite(r.rating) ? r.rating : null,
|
|
1603
|
+
satisfactionComment: typeof r.satisfactionComment === "string" ? r.satisfactionComment : null
|
|
1426
1604
|
};
|
|
1427
1605
|
}
|
|
1606
|
+
function assertTicketId(ticketId) {
|
|
1607
|
+
if (!ticketId || typeof ticketId !== "string") {
|
|
1608
|
+
throw new NeetruError("validation_failed", "ticketId is required");
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function assertValidComment(comment) {
|
|
1612
|
+
if (typeof comment === "string" && comment.length > 2e3) {
|
|
1613
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1428
1616
|
function resolveProductId(config, ...candidates) {
|
|
1429
1617
|
for (const c of candidates) {
|
|
1430
1618
|
if (typeof c === "string" && c.length > 0) {
|
|
@@ -1481,6 +1669,15 @@ function createSupportNamespace(config) {
|
|
|
1481
1669
|
if (input.metadata && typeof input.metadata === "object") {
|
|
1482
1670
|
body.metadata = input.metadata;
|
|
1483
1671
|
}
|
|
1672
|
+
if (typeof input.pageUrl === "string") {
|
|
1673
|
+
body.pageUrl = input.pageUrl;
|
|
1674
|
+
}
|
|
1675
|
+
if (typeof input.screenshotBase64 === "string") {
|
|
1676
|
+
body.screenshotBase64 = input.screenshotBase64;
|
|
1677
|
+
}
|
|
1678
|
+
if (typeof input.consoleLogs === "string") {
|
|
1679
|
+
body.consoleLogs = input.consoleLogs;
|
|
1680
|
+
}
|
|
1484
1681
|
const raw = await httpRequest(config, {
|
|
1485
1682
|
method: "POST",
|
|
1486
1683
|
path: "/api/sdk/v1/support/tickets",
|
|
@@ -1506,6 +1703,74 @@ function createSupportNamespace(config) {
|
|
|
1506
1703
|
});
|
|
1507
1704
|
const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
|
|
1508
1705
|
return list.map(toTicket);
|
|
1706
|
+
},
|
|
1707
|
+
/**
|
|
1708
|
+
* 3.4: marca a notificação de resolução como vista (`resolvedNotifiedAt`,
|
|
1709
|
+
* idempotente no Core). Endpoint canonical:
|
|
1710
|
+
* `POST /api/sdk/v1/support/tickets/{id}/resolved-seen`.
|
|
1711
|
+
*/
|
|
1712
|
+
async markResolvedSeen(ticketId) {
|
|
1713
|
+
assertTicketId(ticketId);
|
|
1714
|
+
await httpRequest(config, {
|
|
1715
|
+
method: "POST",
|
|
1716
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/resolved-seen`,
|
|
1717
|
+
requireAuth: true
|
|
1718
|
+
});
|
|
1719
|
+
},
|
|
1720
|
+
/**
|
|
1721
|
+
* 3.4: registra uma tentativa de exibição do modal de satisfação
|
|
1722
|
+
* (`satisfactionAttempts += 1`). Chamar no SHOW do modal, não na
|
|
1723
|
+
* resposta. Endpoint canonical:
|
|
1724
|
+
* `POST /api/sdk/v1/support/tickets/{id}/satisfaction-attempt`.
|
|
1725
|
+
*/
|
|
1726
|
+
async recordSatisfactionAttempt(ticketId) {
|
|
1727
|
+
assertTicketId(ticketId);
|
|
1728
|
+
await httpRequest(config, {
|
|
1729
|
+
method: "POST",
|
|
1730
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/satisfaction-attempt`,
|
|
1731
|
+
requireAuth: true
|
|
1732
|
+
});
|
|
1733
|
+
},
|
|
1734
|
+
/**
|
|
1735
|
+
* 3.4: envia (ou pula) a avaliação de satisfação (CSAT 1-5). Endpoint
|
|
1736
|
+
* canonical: `POST /api/sdk/v1/support/tickets/{id}/satisfaction`.
|
|
1737
|
+
*/
|
|
1738
|
+
async submitSatisfaction(input) {
|
|
1739
|
+
if (!input || typeof input !== "object") {
|
|
1740
|
+
throw new NeetruError("validation_failed", "input is required");
|
|
1741
|
+
}
|
|
1742
|
+
assertTicketId(input.ticketId);
|
|
1743
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
1744
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
1745
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
assertValidComment(input.comment);
|
|
1749
|
+
await httpRequest(config, {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(input.ticketId)}/satisfaction`,
|
|
1752
|
+
body: {
|
|
1753
|
+
rating: input.rating ?? null,
|
|
1754
|
+
comment: input.comment ?? null,
|
|
1755
|
+
skipped: input.skipped
|
|
1756
|
+
},
|
|
1757
|
+
requireAuth: true
|
|
1758
|
+
});
|
|
1759
|
+
},
|
|
1760
|
+
/**
|
|
1761
|
+
* 3.4: contesta um ticket resolvido — reabre e zera o ciclo de
|
|
1762
|
+
* satisfação/notificação. Endpoint canonical:
|
|
1763
|
+
* `POST /api/sdk/v1/support/tickets/{id}/contest`.
|
|
1764
|
+
*/
|
|
1765
|
+
async contestTicket(ticketId, opts) {
|
|
1766
|
+
assertTicketId(ticketId);
|
|
1767
|
+
assertValidComment(opts?.comment);
|
|
1768
|
+
await httpRequest(config, {
|
|
1769
|
+
method: "POST",
|
|
1770
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/contest`,
|
|
1771
|
+
body: { comment: opts?.comment ?? void 0 },
|
|
1772
|
+
requireAuth: true
|
|
1773
|
+
});
|
|
1509
1774
|
}
|
|
1510
1775
|
};
|
|
1511
1776
|
}
|
|
@@ -4937,7 +5202,20 @@ function buildTicketProvider(config, opts) {
|
|
|
4937
5202
|
return ticket;
|
|
4938
5203
|
};
|
|
4939
5204
|
}
|
|
5205
|
+
var _hasWarnedMissingDbId = false;
|
|
5206
|
+
function warnMissingDbIdOnce(config, dbOpts) {
|
|
5207
|
+
if (_hasWarnedMissingDbId) return;
|
|
5208
|
+
if (dbOpts.dbId) return;
|
|
5209
|
+
if (config.env === "prod") return;
|
|
5210
|
+
if (typeof window === "undefined") return;
|
|
5211
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
5212
|
+
_hasWarnedMissingDbId = true;
|
|
5213
|
+
console.warn(
|
|
5214
|
+
"[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."
|
|
5215
|
+
);
|
|
5216
|
+
}
|
|
4940
5217
|
function createNeetruDb(config, dbOpts = {}) {
|
|
5218
|
+
warnMissingDbIdOnce(config, dbOpts);
|
|
4941
5219
|
let resolvedTransport;
|
|
4942
5220
|
if (dbOpts.transport !== void 0) {
|
|
4943
5221
|
resolvedTransport = dbOpts.transport;
|
|
@@ -5756,6 +6034,199 @@ init_errors();
|
|
|
5756
6034
|
init_http();
|
|
5757
6035
|
var AI_CHAT_PATH = "/api/sdk/v1/ai/chat";
|
|
5758
6036
|
var AI_NONSTREAM_TIMEOUT_MS = 12e4;
|
|
6037
|
+
var MAX_TEXT_PART_LEN = 32768;
|
|
6038
|
+
var MAX_CONTENT_PARTS = 32;
|
|
6039
|
+
var MAX_DATA_URL_LEN = 4194304;
|
|
6040
|
+
var MAX_HTTPS_URL_LEN = 2048;
|
|
6041
|
+
var DATA_IMAGE_URL_RE = /^data:image\/(?:png|jpe?g|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
|
|
6042
|
+
var HTTPS_URL_RE = /^https:\/\/[^\s]+$/;
|
|
6043
|
+
var IMAGE_DETAIL_VALUES = /* @__PURE__ */ new Set(["low", "high", "auto"]);
|
|
6044
|
+
var AI_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
|
|
6045
|
+
var MAX_MESSAGES = 100;
|
|
6046
|
+
var MAX_MODEL_LEN = 128;
|
|
6047
|
+
var MAX_TOKENS_CAP = 32768;
|
|
6048
|
+
var MIN_TEMPERATURE = 0;
|
|
6049
|
+
var MAX_TEMPERATURE = 2;
|
|
6050
|
+
var MAX_TOTAL_CONTENT_PARTS = 200;
|
|
6051
|
+
var MAX_TOTAL_IMAGES = 20;
|
|
6052
|
+
var MAX_TOTAL_IMAGE_BYTES = 12 * 1024 * 1024;
|
|
6053
|
+
var MAX_SCHEMA_JSON_LEN = 5e4;
|
|
6054
|
+
var MAX_SCHEMA_DEPTH = 16;
|
|
6055
|
+
var MAX_SCHEMA_KEYS = 500;
|
|
6056
|
+
var FORBIDDEN_SCHEMA_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
6057
|
+
var RESPONSE_FORMAT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
6058
|
+
function fail(msg) {
|
|
6059
|
+
throw new NeetruError("validation_failed", msg);
|
|
6060
|
+
}
|
|
6061
|
+
function isStrictBase64Payload(b64) {
|
|
6062
|
+
if (b64.length === 0 || b64.length % 4 !== 0) return false;
|
|
6063
|
+
const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
6064
|
+
const body = pad > 0 ? b64.slice(0, b64.length - pad) : b64;
|
|
6065
|
+
return !body.includes("=");
|
|
6066
|
+
}
|
|
6067
|
+
function dataUrlDecodedBytes(url) {
|
|
6068
|
+
const comma = url.indexOf(",");
|
|
6069
|
+
if (comma === -1) return 0;
|
|
6070
|
+
const b64 = url.slice(comma + 1);
|
|
6071
|
+
const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
6072
|
+
return Math.max(0, Math.floor(b64.length * 3 / 4) - pad);
|
|
6073
|
+
}
|
|
6074
|
+
function checkAggregateMultimodalLimits(messages) {
|
|
6075
|
+
let totalParts = 0;
|
|
6076
|
+
let totalImages = 0;
|
|
6077
|
+
let totalImageBytes = 0;
|
|
6078
|
+
for (const m of messages) {
|
|
6079
|
+
const content = m?.content;
|
|
6080
|
+
if (!Array.isArray(content)) continue;
|
|
6081
|
+
for (const part of content) {
|
|
6082
|
+
totalParts++;
|
|
6083
|
+
if (totalParts > MAX_TOTAL_CONTENT_PARTS) {
|
|
6084
|
+
return `ai: total content parts exceeds ${MAX_TOTAL_CONTENT_PARTS} per request`;
|
|
6085
|
+
}
|
|
6086
|
+
if (part && typeof part === "object" && part.type === "image_url") {
|
|
6087
|
+
totalImages++;
|
|
6088
|
+
if (totalImages > MAX_TOTAL_IMAGES) {
|
|
6089
|
+
return `ai: total images exceeds ${MAX_TOTAL_IMAGES} per request`;
|
|
6090
|
+
}
|
|
6091
|
+
const url = part.image_url?.url;
|
|
6092
|
+
if (typeof url === "string" && url.startsWith("data:")) {
|
|
6093
|
+
totalImageBytes += dataUrlDecodedBytes(url);
|
|
6094
|
+
if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
|
|
6095
|
+
return `ai: aggregate image bytes exceeds ${MAX_TOTAL_IMAGE_BYTES} per request`;
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
}
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
return null;
|
|
6102
|
+
}
|
|
6103
|
+
function validateStructuredSchema(schema) {
|
|
6104
|
+
if (schema === void 0) return null;
|
|
6105
|
+
if (schema === null || typeof schema !== "object") {
|
|
6106
|
+
return "ai: responseFormat json_schema.schema must be an object";
|
|
6107
|
+
}
|
|
6108
|
+
let serialized;
|
|
6109
|
+
try {
|
|
6110
|
+
serialized = JSON.stringify(schema);
|
|
6111
|
+
} catch {
|
|
6112
|
+
return "ai: responseFormat json_schema.schema is not serializable";
|
|
6113
|
+
}
|
|
6114
|
+
if (serialized === void 0) return "ai: responseFormat json_schema.schema is invalid";
|
|
6115
|
+
if (serialized.length > MAX_SCHEMA_JSON_LEN) {
|
|
6116
|
+
return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_JSON_LEN} serialized chars`;
|
|
6117
|
+
}
|
|
6118
|
+
let keyCount = 0;
|
|
6119
|
+
const walk = (node, depth) => {
|
|
6120
|
+
if (depth > MAX_SCHEMA_DEPTH) {
|
|
6121
|
+
return `ai: responseFormat json_schema.schema exceeds max depth ${MAX_SCHEMA_DEPTH}`;
|
|
6122
|
+
}
|
|
6123
|
+
if (Array.isArray(node)) {
|
|
6124
|
+
for (const item of node) {
|
|
6125
|
+
const e = walk(item, depth + 1);
|
|
6126
|
+
if (e) return e;
|
|
6127
|
+
}
|
|
6128
|
+
return null;
|
|
6129
|
+
}
|
|
6130
|
+
if (node && typeof node === "object") {
|
|
6131
|
+
for (const key of Object.getOwnPropertyNames(node)) {
|
|
6132
|
+
if (FORBIDDEN_SCHEMA_KEYS.has(key)) {
|
|
6133
|
+
return `ai: responseFormat json_schema.schema has forbidden key: ${key}`;
|
|
6134
|
+
}
|
|
6135
|
+
keyCount++;
|
|
6136
|
+
if (keyCount > MAX_SCHEMA_KEYS) {
|
|
6137
|
+
return `ai: responseFormat json_schema.schema exceeds ${MAX_SCHEMA_KEYS} keys`;
|
|
6138
|
+
}
|
|
6139
|
+
const e = walk(node[key], depth + 1);
|
|
6140
|
+
if (e) return e;
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
return null;
|
|
6144
|
+
};
|
|
6145
|
+
return walk(schema, 0);
|
|
6146
|
+
}
|
|
6147
|
+
function validateImageUrl(url) {
|
|
6148
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
6149
|
+
fail("ai: image_url.url must be a non-empty string");
|
|
6150
|
+
}
|
|
6151
|
+
const u = url;
|
|
6152
|
+
if (u.startsWith("data:")) {
|
|
6153
|
+
if (u.length > MAX_DATA_URL_LEN) fail(`ai: image_url.url (data:) exceeds ${MAX_DATA_URL_LEN} chars`);
|
|
6154
|
+
if (!DATA_IMAGE_URL_RE.test(u)) {
|
|
6155
|
+
fail("ai: image_url.url data: must be data:image/<png|jpeg|jpg|gif|webp>;base64,<...>");
|
|
6156
|
+
}
|
|
6157
|
+
const b64 = u.slice(u.indexOf(",") + 1);
|
|
6158
|
+
if (!isStrictBase64Payload(b64)) {
|
|
6159
|
+
fail("ai: image_url.url data: has invalid base64 (length/padding)");
|
|
6160
|
+
}
|
|
6161
|
+
return;
|
|
6162
|
+
}
|
|
6163
|
+
if (u.startsWith("https://")) {
|
|
6164
|
+
if (u.length > MAX_HTTPS_URL_LEN) fail(`ai: image_url.url (https:) exceeds ${MAX_HTTPS_URL_LEN} chars`);
|
|
6165
|
+
if (!HTTPS_URL_RE.test(u)) fail("ai: image_url.url https: is invalid");
|
|
6166
|
+
return;
|
|
6167
|
+
}
|
|
6168
|
+
fail("ai: image_url.url must be a data:image/...;base64 or https:// URL (other schemes are rejected)");
|
|
6169
|
+
}
|
|
6170
|
+
function validateAiContent(content, role) {
|
|
6171
|
+
if (typeof content === "string") {
|
|
6172
|
+
if (content.length === 0) fail("ai: message content string must be non-empty");
|
|
6173
|
+
if (content.length > MAX_TEXT_PART_LEN) fail(`ai: message content exceeds ${MAX_TEXT_PART_LEN} chars`);
|
|
6174
|
+
return;
|
|
6175
|
+
}
|
|
6176
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
6177
|
+
fail("ai: message content must be a non-empty string or array of parts");
|
|
6178
|
+
}
|
|
6179
|
+
if (content.length > MAX_CONTENT_PARTS) fail(`ai: message content has more than ${MAX_CONTENT_PARTS} parts`);
|
|
6180
|
+
for (const part of content) {
|
|
6181
|
+
if (!part || typeof part !== "object") fail("ai: each content part must be an object");
|
|
6182
|
+
const type = part.type;
|
|
6183
|
+
if (type === "text") {
|
|
6184
|
+
const text = part.text;
|
|
6185
|
+
if (typeof text !== "string" || text.length === 0) fail("ai: text part must have non-empty { text: string }");
|
|
6186
|
+
if (text.length > MAX_TEXT_PART_LEN) fail(`ai: text part exceeds ${MAX_TEXT_PART_LEN} chars`);
|
|
6187
|
+
} else if (type === "image_url") {
|
|
6188
|
+
if (role !== "user") {
|
|
6189
|
+
fail(`ai: image_url is only allowed in role 'user' messages (got '${role}')`);
|
|
6190
|
+
}
|
|
6191
|
+
const iu = part.image_url;
|
|
6192
|
+
if (!iu || typeof iu !== "object") fail("ai: image_url part must have { image_url: { url } }");
|
|
6193
|
+
validateImageUrl(iu.url);
|
|
6194
|
+
const detail = iu.detail;
|
|
6195
|
+
if (detail !== void 0 && (typeof detail !== "string" || !IMAGE_DETAIL_VALUES.has(detail))) {
|
|
6196
|
+
fail("ai: image_url.detail must be 'low' | 'high' | 'auto'");
|
|
6197
|
+
}
|
|
6198
|
+
} else {
|
|
6199
|
+
fail("ai: content part type must be 'text' or 'image_url'");
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
function validateResponseFormat(rf) {
|
|
6204
|
+
if (!rf || typeof rf !== "object") fail("ai: responseFormat must be an object");
|
|
6205
|
+
const type = rf.type;
|
|
6206
|
+
if (type === "text" || type === "json_object") return;
|
|
6207
|
+
if (type === "json_schema") {
|
|
6208
|
+
const js = rf.json_schema;
|
|
6209
|
+
if (!js || typeof js !== "object") fail("ai: responseFormat json_schema requires { json_schema: { name } }");
|
|
6210
|
+
const jso = js;
|
|
6211
|
+
const name = jso.name;
|
|
6212
|
+
if (typeof name !== "string" || name.length === 0) fail("ai: responseFormat json_schema.name is required");
|
|
6213
|
+
if (name.length > 64) fail("ai: responseFormat json_schema.name exceeds 64 chars");
|
|
6214
|
+
if (!RESPONSE_FORMAT_NAME_RE.test(name)) fail("ai: responseFormat json_schema.name must match a-zA-Z0-9_-");
|
|
6215
|
+
if (jso.description !== void 0) {
|
|
6216
|
+
if (typeof jso.description !== "string") fail("ai: responseFormat json_schema.description must be a string");
|
|
6217
|
+
if (jso.description.length > 1e3) fail("ai: responseFormat json_schema.description exceeds 1000 chars");
|
|
6218
|
+
}
|
|
6219
|
+
if (jso.strict !== void 0 && typeof jso.strict !== "boolean") {
|
|
6220
|
+
fail("ai: responseFormat json_schema.strict must be a boolean");
|
|
6221
|
+
}
|
|
6222
|
+
if (jso.schema !== void 0) {
|
|
6223
|
+
const err = validateStructuredSchema(jso.schema);
|
|
6224
|
+
if (err) fail(err);
|
|
6225
|
+
}
|
|
6226
|
+
return;
|
|
6227
|
+
}
|
|
6228
|
+
fail("ai: responseFormat.type must be 'text' | 'json_object' | 'json_schema'");
|
|
6229
|
+
}
|
|
5759
6230
|
function resolveAiBody(config, opts, stream) {
|
|
5760
6231
|
if (!opts || typeof opts !== "object") {
|
|
5761
6232
|
throw new NeetruError("validation_failed", "ai: options object is required");
|
|
@@ -5763,17 +6234,26 @@ function resolveAiBody(config, opts, stream) {
|
|
|
5763
6234
|
if (typeof opts.model !== "string" || opts.model.length === 0) {
|
|
5764
6235
|
throw new NeetruError("validation_failed", "ai: model is required");
|
|
5765
6236
|
}
|
|
6237
|
+
if (opts.model.length > MAX_MODEL_LEN) {
|
|
6238
|
+
throw new NeetruError("validation_failed", `ai: model exceeds ${MAX_MODEL_LEN} chars`);
|
|
6239
|
+
}
|
|
5766
6240
|
if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
|
|
5767
6241
|
throw new NeetruError("validation_failed", "ai: messages must be a non-empty array");
|
|
5768
6242
|
}
|
|
6243
|
+
if (opts.messages.length > MAX_MESSAGES) {
|
|
6244
|
+
throw new NeetruError("validation_failed", `ai: messages exceeds ${MAX_MESSAGES} per request`);
|
|
6245
|
+
}
|
|
5769
6246
|
for (const m of opts.messages) {
|
|
5770
|
-
if (!m || typeof m !== "object" || typeof m.role !== "string" || m.role
|
|
6247
|
+
if (!m || typeof m !== "object" || typeof m.role !== "string" || !AI_ROLES.has(m.role)) {
|
|
5771
6248
|
throw new NeetruError(
|
|
5772
6249
|
"validation_failed",
|
|
5773
|
-
"ai: each message must be
|
|
6250
|
+
"ai: each message role must be 'system' | 'user' | 'assistant'"
|
|
5774
6251
|
);
|
|
5775
6252
|
}
|
|
6253
|
+
validateAiContent(m.content, m.role);
|
|
5776
6254
|
}
|
|
6255
|
+
const aggErr = checkAggregateMultimodalLimits(opts.messages);
|
|
6256
|
+
if (aggErr) throw new NeetruError("validation_failed", aggErr);
|
|
5777
6257
|
const productId = opts.productId ?? config.productId;
|
|
5778
6258
|
const tenantId = opts.tenantId ?? config.tenantId;
|
|
5779
6259
|
if (!productId) {
|
|
@@ -5798,17 +6278,27 @@ function resolveAiBody(config, opts, stream) {
|
|
|
5798
6278
|
stream
|
|
5799
6279
|
};
|
|
5800
6280
|
if (opts.maxTokens !== void 0) {
|
|
5801
|
-
if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0) {
|
|
5802
|
-
throw new NeetruError(
|
|
6281
|
+
if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0 || opts.maxTokens > MAX_TOKENS_CAP) {
|
|
6282
|
+
throw new NeetruError(
|
|
6283
|
+
"validation_failed",
|
|
6284
|
+
`ai: maxTokens must be a positive integer <= ${MAX_TOKENS_CAP}`
|
|
6285
|
+
);
|
|
5803
6286
|
}
|
|
5804
6287
|
body.maxTokens = opts.maxTokens;
|
|
5805
6288
|
}
|
|
5806
6289
|
if (opts.temperature !== void 0) {
|
|
5807
|
-
if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature)) {
|
|
5808
|
-
throw new NeetruError(
|
|
6290
|
+
if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature) || opts.temperature < MIN_TEMPERATURE || opts.temperature > MAX_TEMPERATURE) {
|
|
6291
|
+
throw new NeetruError(
|
|
6292
|
+
"validation_failed",
|
|
6293
|
+
`ai: temperature must be a finite number between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
|
|
6294
|
+
);
|
|
5809
6295
|
}
|
|
5810
6296
|
body.temperature = opts.temperature;
|
|
5811
6297
|
}
|
|
6298
|
+
if (opts.responseFormat !== void 0) {
|
|
6299
|
+
validateResponseFormat(opts.responseFormat);
|
|
6300
|
+
body.responseFormat = opts.responseFormat;
|
|
6301
|
+
}
|
|
5812
6302
|
return body;
|
|
5813
6303
|
}
|
|
5814
6304
|
async function safeJson2(res) {
|
|
@@ -6235,7 +6725,18 @@ var MockSupport = class {
|
|
|
6235
6725
|
severity: input.severity ?? "normal",
|
|
6236
6726
|
status: "open",
|
|
6237
6727
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6238
|
-
productSlug: input.productSlug
|
|
6728
|
+
productSlug: input.productSlug,
|
|
6729
|
+
// 3.4 (product-feedback): espelha o contrato — campos novos nullable,
|
|
6730
|
+
// populados a partir do input quando fornecidos.
|
|
6731
|
+
pageUrl: typeof input.pageUrl === "string" ? input.pageUrl : null,
|
|
6732
|
+
screenshotUrl: typeof input.screenshotBase64 === "string" ? input.screenshotBase64 : null,
|
|
6733
|
+
resolvedAt: null,
|
|
6734
|
+
resolvedNotifiedAt: null,
|
|
6735
|
+
satisfactionAttempts: 0,
|
|
6736
|
+
lastSatisfactionAttemptAt: null,
|
|
6737
|
+
satisfiedAt: null,
|
|
6738
|
+
rating: null,
|
|
6739
|
+
satisfactionComment: null
|
|
6239
6740
|
};
|
|
6240
6741
|
this._tickets.push(ticket);
|
|
6241
6742
|
return ticket;
|
|
@@ -6246,6 +6747,62 @@ var MockSupport = class {
|
|
|
6246
6747
|
}
|
|
6247
6748
|
return [...this._tickets];
|
|
6248
6749
|
}
|
|
6750
|
+
_find(ticketId) {
|
|
6751
|
+
if (!ticketId || typeof ticketId !== "string") {
|
|
6752
|
+
throw new NeetruError("validation_failed", "ticketId is required");
|
|
6753
|
+
}
|
|
6754
|
+
const ticket = this._tickets.find((t) => t.id === ticketId);
|
|
6755
|
+
if (!ticket) {
|
|
6756
|
+
throw new NeetruError("not_found", `ticket ${ticketId} not found`);
|
|
6757
|
+
}
|
|
6758
|
+
return ticket;
|
|
6759
|
+
}
|
|
6760
|
+
/** 3.4: espelha `markResolvedSeen` — idempotente (só seta se ainda null). */
|
|
6761
|
+
async markResolvedSeen(ticketId) {
|
|
6762
|
+
const ticket = this._find(ticketId);
|
|
6763
|
+
if (ticket.resolvedNotifiedAt === null) {
|
|
6764
|
+
ticket.resolvedNotifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
/** 3.4: espelha `recordSatisfactionAttempt` — incrementa contador. */
|
|
6768
|
+
async recordSatisfactionAttempt(ticketId) {
|
|
6769
|
+
const ticket = this._find(ticketId);
|
|
6770
|
+
ticket.satisfactionAttempts += 1;
|
|
6771
|
+
ticket.lastSatisfactionAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6772
|
+
}
|
|
6773
|
+
/** 3.4: espelha `submitSatisfaction` — terminal, seta `satisfiedAt`. */
|
|
6774
|
+
async submitSatisfaction(input) {
|
|
6775
|
+
if (!input || typeof input !== "object") {
|
|
6776
|
+
throw new NeetruError("validation_failed", "input is required");
|
|
6777
|
+
}
|
|
6778
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
6779
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
6780
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
6781
|
+
}
|
|
6782
|
+
}
|
|
6783
|
+
if (typeof input.comment === "string" && input.comment.length > 2e3) {
|
|
6784
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6785
|
+
}
|
|
6786
|
+
const ticket = this._find(input.ticketId);
|
|
6787
|
+
ticket.satisfiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6788
|
+
ticket.rating = input.rating ?? null;
|
|
6789
|
+
ticket.satisfactionComment = input.comment ?? null;
|
|
6790
|
+
}
|
|
6791
|
+
/** 3.4: espelha `contestTicket` — reabre e zera o ciclo de satisfação. */
|
|
6792
|
+
async contestTicket(ticketId, opts) {
|
|
6793
|
+
if (typeof opts?.comment === "string" && opts.comment.length > 2e3) {
|
|
6794
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6795
|
+
}
|
|
6796
|
+
const ticket = this._find(ticketId);
|
|
6797
|
+
ticket.status = "open";
|
|
6798
|
+
ticket.resolvedAt = null;
|
|
6799
|
+
ticket.resolvedNotifiedAt = null;
|
|
6800
|
+
ticket.satisfactionAttempts = 0;
|
|
6801
|
+
ticket.lastSatisfactionAttemptAt = null;
|
|
6802
|
+
ticket.satisfiedAt = null;
|
|
6803
|
+
ticket.rating = null;
|
|
6804
|
+
ticket.satisfactionComment = null;
|
|
6805
|
+
}
|
|
6249
6806
|
/** Test helper. */
|
|
6250
6807
|
__reset() {
|
|
6251
6808
|
this._tickets = [];
|
|
@@ -6591,9 +7148,15 @@ var MockDb = class {
|
|
|
6591
7148
|
};
|
|
6592
7149
|
var MockAi = class {
|
|
6593
7150
|
_calls = [];
|
|
7151
|
+
/** Extrai texto de um content string OU array multimodal (imagens ignoradas). */
|
|
7152
|
+
_contentToText(content) {
|
|
7153
|
+
if (typeof content === "string") return content;
|
|
7154
|
+
if (!Array.isArray(content)) return "";
|
|
7155
|
+
return content.filter((p) => !!p && p.type === "text").map((p) => p.text).join(" ");
|
|
7156
|
+
}
|
|
6594
7157
|
_reply(opts) {
|
|
6595
7158
|
const last = [...opts.messages].reverse().find((m) => m.role === "user") ?? opts.messages[opts.messages.length - 1];
|
|
6596
|
-
const content = last?.content
|
|
7159
|
+
const content = this._contentToText(last?.content);
|
|
6597
7160
|
return content ? `mock-reply: ${content}` : "mock-reply";
|
|
6598
7161
|
}
|
|
6599
7162
|
async chat(opts) {
|
|
@@ -6605,7 +7168,7 @@ var MockAi = class {
|
|
|
6605
7168
|
}
|
|
6606
7169
|
this._calls.push({ model: opts.model, messages: opts.messages, stream: false });
|
|
6607
7170
|
const text = this._reply(opts);
|
|
6608
|
-
const promptTokens = opts.messages.reduce((n, m) => n + (m.content
|
|
7171
|
+
const promptTokens = opts.messages.reduce((n, m) => n + this._contentToText(m.content).length, 0);
|
|
6609
7172
|
const completionTokens = text.length;
|
|
6610
7173
|
return {
|
|
6611
7174
|
choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
|
|
@@ -7276,7 +7839,7 @@ function createNeetruClient(config = {}) {
|
|
|
7276
7839
|
init_errors();
|
|
7277
7840
|
init_db_errors();
|
|
7278
7841
|
init_http();
|
|
7279
|
-
var VERSION = "3.
|
|
7842
|
+
var VERSION = "3.4.0";
|
|
7280
7843
|
|
|
7281
7844
|
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 };
|
|
7282
7845
|
//# sourceMappingURL=index.mjs.map
|