@neetru/sdk 3.1.7 → 3.1.10
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/dist/ai.cjs +304 -0
- package/dist/ai.cjs.map +1 -0
- package/dist/ai.d.cts +3 -0
- package/dist/ai.d.ts +3 -0
- package/dist/ai.mjs +302 -0
- package/dist/ai.mjs.map +1 -0
- package/dist/auth.cjs +348 -23
- 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 +348 -23
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.d.cts +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/db-react.d.cts +1 -1
- package/dist/db-react.d.ts +1 -1
- package/dist/db.cjs +55 -16
- 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 +55 -16
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.d.cts +1 -1
- package/dist/entitlements.d.ts +1 -1
- package/dist/firestore-compat.cjs +4 -9
- package/dist/firestore-compat.cjs.map +1 -1
- package/dist/firestore-compat.d.cts +2 -2
- package/dist/firestore-compat.d.ts +2 -2
- package/dist/firestore-compat.mjs +4 -9
- package/dist/firestore-compat.mjs.map +1 -1
- package/dist/index.cjs +351 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +350 -25
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +52 -0
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +20 -2
- package/dist/mocks.d.ts +20 -2
- package/dist/mocks.mjs +52 -1
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.d.cts +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/support.d.cts +1 -1
- package/dist/support.d.ts +1 -1
- package/dist/telemetry.cjs +5 -1
- 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 +5 -1
- package/dist/telemetry.mjs.map +1 -1
- package/dist/{types-BXvGHppn.d.cts → types-Cog9S-tC.d.cts} +103 -3
- package/dist/{types-CQVak2a3.d.ts → types-dagUxKYz.d.ts} +103 -3
- package/dist/usage.cjs +2 -2
- 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 +2 -2
- package/dist/usage.mjs.map +1 -1
- package/dist/webhooks.d.cts +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/package.json +6 -1
package/dist/auth.d.cts
CHANGED
package/dist/auth.d.ts
CHANGED
package/dist/auth.mjs
CHANGED
|
@@ -446,7 +446,7 @@ __export(sql_client_exports, {
|
|
|
446
446
|
function mapEnvToCore(sdkEnv) {
|
|
447
447
|
if (sdkEnv === "workspace") return "staging";
|
|
448
448
|
if (sdkEnv === "prod") return "production";
|
|
449
|
-
return "dev";
|
|
449
|
+
return "dev-local";
|
|
450
450
|
}
|
|
451
451
|
function createHttpLeaseFetcher(config) {
|
|
452
452
|
return async (opts) => {
|
|
@@ -955,7 +955,11 @@ function createTelemetryNamespace(config) {
|
|
|
955
955
|
path: "/api/sdk/v1/telemetry/event",
|
|
956
956
|
body,
|
|
957
957
|
requireAuth: true,
|
|
958
|
-
retries: 1
|
|
958
|
+
retries: 1,
|
|
959
|
+
// caça-bruxas: retry sem Idempotency-Key duplicava o evento se o
|
|
960
|
+
// servidor já tinha persistido antes da falha transiente. Mesmo
|
|
961
|
+
// padrão do usage.report (key estável por chamada, reusada no retry).
|
|
962
|
+
idempotencyKey: true
|
|
959
963
|
});
|
|
960
964
|
} catch (err) {
|
|
961
965
|
if (typeof console !== "undefined") {
|
|
@@ -1159,8 +1163,8 @@ function createUsageNamespace(config) {
|
|
|
1159
1163
|
if (!resource || typeof resource !== "string") {
|
|
1160
1164
|
throw new NeetruError("validation_failed", "resource is required");
|
|
1161
1165
|
}
|
|
1162
|
-
if (!Number.isFinite(qty) || qty <= 0) {
|
|
1163
|
-
throw new NeetruError("validation_failed", "qty must be positive integer");
|
|
1166
|
+
if (!Number.isFinite(qty) || qty <= 0 || !Number.isInteger(qty)) {
|
|
1167
|
+
throw new NeetruError("validation_failed", "qty must be a positive integer");
|
|
1164
1168
|
}
|
|
1165
1169
|
const productId = options?.productId ?? config.productId;
|
|
1166
1170
|
const tenantId = options?.tenantId ?? config.tenantId;
|
|
@@ -1409,7 +1413,12 @@ function evaluateFilter(data, filter) {
|
|
|
1409
1413
|
if (!Array.isArray(value)) return true;
|
|
1410
1414
|
return !value.includes(fieldValue);
|
|
1411
1415
|
default:
|
|
1412
|
-
|
|
1416
|
+
if (op === "array-contains-any") {
|
|
1417
|
+
return Array.isArray(fieldValue) && Array.isArray(value) && value.some((v) => fieldValue.includes(v));
|
|
1418
|
+
}
|
|
1419
|
+
throw new Error(
|
|
1420
|
+
`QueryEngine: operador de filtro '${String(op)}' n\xE3o suportado em leitura offline/cache. Use um operador suportado (==, !=, <, <=, >, >=, array-contains, array-contains-any, in, not-in) ou fa\xE7a a query server-side.`
|
|
1421
|
+
);
|
|
1413
1422
|
}
|
|
1414
1423
|
}
|
|
1415
1424
|
function matchesAllFilters(data, filters) {
|
|
@@ -1939,7 +1948,7 @@ var MutationQueue = class {
|
|
|
1939
1948
|
* - `{ removed: true }` — a mutação existente deve ser deletada (add+remove)
|
|
1940
1949
|
* - `null` — não é possível coalescir
|
|
1941
1950
|
*/
|
|
1942
|
-
tryCoalesce(existing, newOp, newPayload, newBatchId) {
|
|
1951
|
+
tryCoalesce(existing, newOp, newPayload, newBatchId, newUpsert) {
|
|
1943
1952
|
if (existing.batchId !== newBatchId) return null;
|
|
1944
1953
|
if (existing.status === "inflight") return null;
|
|
1945
1954
|
const existingOp = existing.op;
|
|
@@ -1947,10 +1956,12 @@ var MutationQueue = class {
|
|
|
1947
1956
|
return { removed: true };
|
|
1948
1957
|
}
|
|
1949
1958
|
if (existingOp === "update" && newOp === "update") {
|
|
1959
|
+
const mergedUpsert = existing.upsert || newUpsert;
|
|
1950
1960
|
return {
|
|
1951
1961
|
coalesced: {
|
|
1952
1962
|
...existing,
|
|
1953
|
-
payload: { ...existing.payload ?? {}, ...newPayload ?? {} }
|
|
1963
|
+
payload: { ...existing.payload ?? {}, ...newPayload ?? {} },
|
|
1964
|
+
...mergedUpsert ? { upsert: true } : {}
|
|
1954
1965
|
}
|
|
1955
1966
|
};
|
|
1956
1967
|
}
|
|
@@ -1999,13 +2010,13 @@ var MutationQueue = class {
|
|
|
1999
2010
|
* Atômico: a persistência final é uma única operação putMutation.
|
|
2000
2011
|
*/
|
|
2001
2012
|
async enqueue(params) {
|
|
2002
|
-
const { collection, op, payload, baseVersion, batchId } = params;
|
|
2013
|
+
const { collection, op, payload, baseVersion, batchId, upsert } = params;
|
|
2003
2014
|
const docId = params.docId ?? (op === "add" ? generateUUIDv4() : "");
|
|
2004
2015
|
await this.syncSeq();
|
|
2005
2016
|
const existingMutations = await this.store.listMutations({ collection, docId });
|
|
2006
2017
|
const lastQueued = existingMutations.filter((m) => m.status === "queued").sort((a, b) => b.seq - a.seq)[0];
|
|
2007
2018
|
if (lastQueued !== void 0) {
|
|
2008
|
-
const coalesceResult = this.tryCoalesce(lastQueued, op, payload, batchId);
|
|
2019
|
+
const coalesceResult = this.tryCoalesce(lastQueued, op, payload, batchId, upsert);
|
|
2009
2020
|
if (coalesceResult !== null) {
|
|
2010
2021
|
if ("removed" in coalesceResult) {
|
|
2011
2022
|
await this.store.deleteMutation(lastQueued.mutationId);
|
|
@@ -2043,7 +2054,9 @@ var MutationQueue = class {
|
|
|
2043
2054
|
attempts: 0,
|
|
2044
2055
|
lastError: null,
|
|
2045
2056
|
status: "queued",
|
|
2046
|
-
batchId
|
|
2057
|
+
batchId,
|
|
2058
|
+
// bug_0273e4dd: só persiste o flag quando verdadeiro (mantém o doc limpo).
|
|
2059
|
+
...upsert ? { upsert: true } : {}
|
|
2047
2060
|
};
|
|
2048
2061
|
await this.store.putMutation(mutation);
|
|
2049
2062
|
return mutation;
|
|
@@ -2372,10 +2385,15 @@ var SyncEngine = class {
|
|
|
2372
2385
|
await this._applyRejected(mut, res, busChanges);
|
|
2373
2386
|
}
|
|
2374
2387
|
}
|
|
2388
|
+
const processedIds = new Set(result.results.map((r) => r.mutationId));
|
|
2389
|
+
const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
|
|
2390
|
+
for (const mut of unprocessed) {
|
|
2391
|
+
await this._queue.markRetry(mut.mutationId, "partial_push: muta\xE7\xE3o n\xE3o processada no lote");
|
|
2392
|
+
}
|
|
2375
2393
|
if (busChanges.length > 0) {
|
|
2376
2394
|
this._bus.emit(busChanges);
|
|
2377
2395
|
}
|
|
2378
|
-
return
|
|
2396
|
+
return unprocessed.length === 0;
|
|
2379
2397
|
}
|
|
2380
2398
|
/** Atualiza o cache após confirmação de escrita (outcome=confirmed). */
|
|
2381
2399
|
async _applySyncedDoc(collection, docId, serverVersion, serverTimestamp, mut, busChanges) {
|
|
@@ -3682,7 +3700,7 @@ var DbCollectionRefImpl = class {
|
|
|
3682
3700
|
}]);
|
|
3683
3701
|
return { ok: true };
|
|
3684
3702
|
}
|
|
3685
|
-
async update(id, data) {
|
|
3703
|
+
async update(id, data, opts) {
|
|
3686
3704
|
assertValidId(id);
|
|
3687
3705
|
const existing = await this._store.getDoc(this._collection, id);
|
|
3688
3706
|
const rawData = data;
|
|
@@ -3695,7 +3713,9 @@ var DbCollectionRefImpl = class {
|
|
|
3695
3713
|
op: "update",
|
|
3696
3714
|
payload: rawData,
|
|
3697
3715
|
baseVersion: existing?.meta.serverVersion ?? null,
|
|
3698
|
-
batchId: null
|
|
3716
|
+
batchId: null,
|
|
3717
|
+
// bug_0273e4dd: propaga merge-upsert (setDoc({merge:true}) cria-se-ausente).
|
|
3718
|
+
upsert: opts?.upsert
|
|
3699
3719
|
});
|
|
3700
3720
|
await this._store.putDoc({
|
|
3701
3721
|
collection: this._collection,
|
|
@@ -3819,7 +3839,9 @@ var DbCollectionRefImpl = class {
|
|
|
3819
3839
|
op: "update",
|
|
3820
3840
|
payload: data,
|
|
3821
3841
|
baseVersion: existing?.meta.serverVersion ?? null,
|
|
3822
|
-
batchId
|
|
3842
|
+
batchId,
|
|
3843
|
+
// bug_0273e4dd: propaga merge-upsert (writeBatch.set(merge) cria-se-ausente).
|
|
3844
|
+
upsert: op.upsert
|
|
3823
3845
|
});
|
|
3824
3846
|
await this._store.putDoc({
|
|
3825
3847
|
collection,
|
|
@@ -4081,6 +4103,10 @@ async function createOfflineDocumentsNamespace(opts) {
|
|
|
4081
4103
|
connectivity,
|
|
4082
4104
|
periodicSyncIntervalMs: opts.periodicSyncIntervalMs ?? 5 * 60 * 1e3
|
|
4083
4105
|
});
|
|
4106
|
+
connectivity.start();
|
|
4107
|
+
if (!opts.singleTab && tabCoordinatorLike instanceof TabCoordinator) {
|
|
4108
|
+
tabCoordinatorLike.start();
|
|
4109
|
+
}
|
|
4084
4110
|
return new NeetruDbDocumentsImpl({
|
|
4085
4111
|
store,
|
|
4086
4112
|
queue,
|
|
@@ -4550,11 +4576,11 @@ function createRestSyncTransport(config) {
|
|
|
4550
4576
|
let raw;
|
|
4551
4577
|
if (m.op === "add") {
|
|
4552
4578
|
raw = await httpRequest2(config, {
|
|
4553
|
-
method: "
|
|
4554
|
-
path:
|
|
4579
|
+
method: "PUT",
|
|
4580
|
+
path: DATASTORE_DOC_ENDPOINT(m.collection, m.docId),
|
|
4555
4581
|
body: { data: m.payload ?? {} },
|
|
4556
4582
|
requireAuth: true,
|
|
4557
|
-
retries:
|
|
4583
|
+
retries: 2,
|
|
4558
4584
|
headers: tenantHeaders
|
|
4559
4585
|
});
|
|
4560
4586
|
} else if (m.op === "set") {
|
|
@@ -4570,7 +4596,7 @@ function createRestSyncTransport(config) {
|
|
|
4570
4596
|
raw = await httpRequest2(config, {
|
|
4571
4597
|
method: "PATCH",
|
|
4572
4598
|
path: DATASTORE_DOC_ENDPOINT(m.collection, m.docId),
|
|
4573
|
-
body: { data: m.payload ?? {} },
|
|
4599
|
+
body: { data: m.payload ?? {}, ...m.upsert ? { upsert: true } : {} },
|
|
4574
4600
|
requireAuth: true,
|
|
4575
4601
|
retries: 2,
|
|
4576
4602
|
headers: tenantHeaders
|
|
@@ -4593,7 +4619,24 @@ function createRestSyncTransport(config) {
|
|
|
4593
4619
|
});
|
|
4594
4620
|
} catch (err) {
|
|
4595
4621
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4596
|
-
|
|
4622
|
+
const code = err?.code;
|
|
4623
|
+
const status = err?.status;
|
|
4624
|
+
const isPermanent = code === "forbidden" || code === "unauthorized" || code === "not_found" || code === "conflict" || code === "validation_failed" || typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
4625
|
+
if (isPermanent) {
|
|
4626
|
+
const reason = code === "forbidden" || code === "unauthorized" ? "rejected_permission" : "rejected_validation";
|
|
4627
|
+
results.push({
|
|
4628
|
+
mutationId: m.mutationId,
|
|
4629
|
+
outcome: "rejected",
|
|
4630
|
+
reason,
|
|
4631
|
+
serverVersion: null,
|
|
4632
|
+
serverData: null
|
|
4633
|
+
});
|
|
4634
|
+
continue;
|
|
4635
|
+
}
|
|
4636
|
+
if (results.length === 0) {
|
|
4637
|
+
throw new NeetruDbError("db_unavailable", `pushMutations falhou: ${msg}`);
|
|
4638
|
+
}
|
|
4639
|
+
break;
|
|
4597
4640
|
}
|
|
4598
4641
|
}
|
|
4599
4642
|
return { results };
|
|
@@ -5454,6 +5497,235 @@ var MockNotifications = class {
|
|
|
5454
5497
|
}
|
|
5455
5498
|
};
|
|
5456
5499
|
|
|
5500
|
+
// src/ai.ts
|
|
5501
|
+
init_errors();
|
|
5502
|
+
init_http();
|
|
5503
|
+
var AI_CHAT_PATH = "/api/sdk/v1/ai/chat";
|
|
5504
|
+
var AI_NONSTREAM_TIMEOUT_MS = 12e4;
|
|
5505
|
+
function resolveAiBody(config, opts, stream) {
|
|
5506
|
+
if (!opts || typeof opts !== "object") {
|
|
5507
|
+
throw new NeetruError("validation_failed", "ai: options object is required");
|
|
5508
|
+
}
|
|
5509
|
+
if (typeof opts.model !== "string" || opts.model.length === 0) {
|
|
5510
|
+
throw new NeetruError("validation_failed", "ai: model is required");
|
|
5511
|
+
}
|
|
5512
|
+
if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
|
|
5513
|
+
throw new NeetruError("validation_failed", "ai: messages must be a non-empty array");
|
|
5514
|
+
}
|
|
5515
|
+
for (const m of opts.messages) {
|
|
5516
|
+
if (!m || typeof m !== "object" || typeof m.role !== "string" || m.role.length === 0 || typeof m.content !== "string") {
|
|
5517
|
+
throw new NeetruError(
|
|
5518
|
+
"validation_failed",
|
|
5519
|
+
"ai: each message must be { role: string, content: string }"
|
|
5520
|
+
);
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
const productId = opts.productId ?? config.productId;
|
|
5524
|
+
const tenantId = opts.tenantId ?? config.tenantId;
|
|
5525
|
+
if (!productId) {
|
|
5526
|
+
throw new NeetruError(
|
|
5527
|
+
"validation_failed",
|
|
5528
|
+
"ai: productId required (pass to options or set on createNeetruClient)"
|
|
5529
|
+
);
|
|
5530
|
+
}
|
|
5531
|
+
if (!tenantId) {
|
|
5532
|
+
throw new NeetruError(
|
|
5533
|
+
"validation_failed",
|
|
5534
|
+
"ai: tenantId required (pass to options or set on createNeetruClient)"
|
|
5535
|
+
);
|
|
5536
|
+
}
|
|
5537
|
+
assertValidSdkId("productId", productId);
|
|
5538
|
+
assertValidSdkId("tenantId", tenantId);
|
|
5539
|
+
const body = {
|
|
5540
|
+
productId,
|
|
5541
|
+
tenantId,
|
|
5542
|
+
model: opts.model,
|
|
5543
|
+
messages: opts.messages,
|
|
5544
|
+
stream
|
|
5545
|
+
};
|
|
5546
|
+
if (opts.maxTokens !== void 0) {
|
|
5547
|
+
if (!Number.isInteger(opts.maxTokens) || opts.maxTokens <= 0) {
|
|
5548
|
+
throw new NeetruError("validation_failed", "ai: maxTokens must be a positive integer");
|
|
5549
|
+
}
|
|
5550
|
+
body.maxTokens = opts.maxTokens;
|
|
5551
|
+
}
|
|
5552
|
+
if (opts.temperature !== void 0) {
|
|
5553
|
+
if (typeof opts.temperature !== "number" || !Number.isFinite(opts.temperature)) {
|
|
5554
|
+
throw new NeetruError("validation_failed", "ai: temperature must be a finite number");
|
|
5555
|
+
}
|
|
5556
|
+
body.temperature = opts.temperature;
|
|
5557
|
+
}
|
|
5558
|
+
return body;
|
|
5559
|
+
}
|
|
5560
|
+
async function safeJson2(res) {
|
|
5561
|
+
try {
|
|
5562
|
+
const text = await res.text();
|
|
5563
|
+
if (!text) return void 0;
|
|
5564
|
+
return JSON.parse(text);
|
|
5565
|
+
} catch {
|
|
5566
|
+
return void 0;
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
function statusToCode2(status) {
|
|
5570
|
+
if (status === 401) return "unauthorized";
|
|
5571
|
+
if (status === 403) return "forbidden";
|
|
5572
|
+
if (status === 404) return "not_found";
|
|
5573
|
+
if (status === 409) return "conflict";
|
|
5574
|
+
if (status === 422 || status === 400) return "validation_failed";
|
|
5575
|
+
if (status === 429) return "rate_limited";
|
|
5576
|
+
if (status >= 500) return "server_error";
|
|
5577
|
+
return "unknown";
|
|
5578
|
+
}
|
|
5579
|
+
function joinUrl(baseUrl, path) {
|
|
5580
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
5581
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
5582
|
+
return `${base}${p}`;
|
|
5583
|
+
}
|
|
5584
|
+
async function aiFetch(config, body, signal, stream) {
|
|
5585
|
+
if (!config.apiKey) {
|
|
5586
|
+
throw new NeetruError(
|
|
5587
|
+
"missing_api_key",
|
|
5588
|
+
"ai requires an apiKey. Pass it to createNeetruClient({ apiKey }) or set NEETRU_API_KEY env var."
|
|
5589
|
+
);
|
|
5590
|
+
}
|
|
5591
|
+
const url = joinUrl(config.baseUrl, AI_CHAT_PATH);
|
|
5592
|
+
const headers = {
|
|
5593
|
+
"content-type": "application/json",
|
|
5594
|
+
accept: stream ? "text/event-stream" : "application/json",
|
|
5595
|
+
authorization: `Bearer ${config.apiKey}`
|
|
5596
|
+
};
|
|
5597
|
+
const init = { method: "POST", headers, body: JSON.stringify(body) };
|
|
5598
|
+
if (stream) {
|
|
5599
|
+
if (signal) init.signal = signal;
|
|
5600
|
+
} else {
|
|
5601
|
+
init.signal = signal ?? AbortSignal.timeout(AI_NONSTREAM_TIMEOUT_MS);
|
|
5602
|
+
}
|
|
5603
|
+
let res;
|
|
5604
|
+
try {
|
|
5605
|
+
res = await config.fetch(url, init);
|
|
5606
|
+
} catch (err) {
|
|
5607
|
+
const message = err instanceof DOMException && err.name === "TimeoutError" ? `Network error: timeout after ${AI_NONSTREAM_TIMEOUT_MS / 1e3}s` : err instanceof DOMException && err.name === "AbortError" ? "Network error: request aborted" : `Network error: ${err instanceof Error ? err.message : "fetch failed"}`;
|
|
5608
|
+
throw new NeetruError("network_error", message);
|
|
5609
|
+
}
|
|
5610
|
+
const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
|
|
5611
|
+
if (!res.ok) {
|
|
5612
|
+
const parsed = await safeJson2(res);
|
|
5613
|
+
let code = statusToCode2(res.status);
|
|
5614
|
+
let message = `HTTP ${res.status}`;
|
|
5615
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
5616
|
+
const errField = parsed.error;
|
|
5617
|
+
if (typeof errField === "string") {
|
|
5618
|
+
message = errField;
|
|
5619
|
+
} else if (errField && typeof errField === "object") {
|
|
5620
|
+
if (typeof errField.code === "string") code = errField.code;
|
|
5621
|
+
if (typeof errField.message === "string") message = errField.message;
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
throw new NeetruError(code, message, res.status, requestId);
|
|
5625
|
+
}
|
|
5626
|
+
const coreVersion = res.headers.get("x-neetru-version");
|
|
5627
|
+
if (coreVersion) checkCoreVersionCompat(coreVersion);
|
|
5628
|
+
return res;
|
|
5629
|
+
}
|
|
5630
|
+
function readMeta(res) {
|
|
5631
|
+
return {
|
|
5632
|
+
provider: res.headers.get("x-neetru-provider") ?? "",
|
|
5633
|
+
usageWarning: res.headers.get("x-neetru-ai-usage-warning") === "1"
|
|
5634
|
+
};
|
|
5635
|
+
}
|
|
5636
|
+
function parseSseData(rawEvent) {
|
|
5637
|
+
const dataLines = [];
|
|
5638
|
+
for (const line of rawEvent.split("\n")) {
|
|
5639
|
+
const trimmed = line.replace(/\r$/, "");
|
|
5640
|
+
if (trimmed.startsWith("data:")) {
|
|
5641
|
+
let v = trimmed.slice(5);
|
|
5642
|
+
if (v.startsWith(" ")) v = v.slice(1);
|
|
5643
|
+
dataLines.push(v);
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
if (dataLines.length === 0) return null;
|
|
5647
|
+
const payload = dataLines.join("\n");
|
|
5648
|
+
if (payload === "[DONE]") return { done: true };
|
|
5649
|
+
try {
|
|
5650
|
+
const obj = JSON.parse(payload);
|
|
5651
|
+
const content = obj?.choices?.[0]?.delta?.content;
|
|
5652
|
+
if (typeof content === "string" && content.length > 0) return { content };
|
|
5653
|
+
return null;
|
|
5654
|
+
} catch {
|
|
5655
|
+
return null;
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
function createAiNamespace(config) {
|
|
5659
|
+
return {
|
|
5660
|
+
async chat(opts) {
|
|
5661
|
+
const body = resolveAiBody(config, opts, false);
|
|
5662
|
+
const res = await aiFetch(config, body, opts.signal, false);
|
|
5663
|
+
const { provider: providerHeader, usageWarning } = readMeta(res);
|
|
5664
|
+
const raw = await safeJson2(res);
|
|
5665
|
+
if (!raw || raw.ok !== true || !Array.isArray(raw.choices)) {
|
|
5666
|
+
throw new NeetruError("invalid_response", "ai.chat response missing ok/choices");
|
|
5667
|
+
}
|
|
5668
|
+
return {
|
|
5669
|
+
choices: raw.choices,
|
|
5670
|
+
usage: raw.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
5671
|
+
model: raw.model ?? opts.model,
|
|
5672
|
+
provider: raw.provider ?? providerHeader ?? "",
|
|
5673
|
+
usageWarning
|
|
5674
|
+
};
|
|
5675
|
+
},
|
|
5676
|
+
stream(opts) {
|
|
5677
|
+
const body = resolveAiBody(config, opts, true);
|
|
5678
|
+
const responsePromise = aiFetch(config, body, opts.signal, true);
|
|
5679
|
+
const meta = responsePromise.then(readMeta);
|
|
5680
|
+
meta.catch(() => {
|
|
5681
|
+
});
|
|
5682
|
+
async function* iterate() {
|
|
5683
|
+
const res = await responsePromise;
|
|
5684
|
+
const stream = res.body;
|
|
5685
|
+
if (!stream) {
|
|
5686
|
+
throw new NeetruError("invalid_response", "ai.stream: response has no body");
|
|
5687
|
+
}
|
|
5688
|
+
const reader = stream.getReader();
|
|
5689
|
+
const decoder = new TextDecoder();
|
|
5690
|
+
let buffer = "";
|
|
5691
|
+
try {
|
|
5692
|
+
while (true) {
|
|
5693
|
+
const { done, value } = await reader.read();
|
|
5694
|
+
if (done) break;
|
|
5695
|
+
buffer += decoder.decode(value, { stream: true });
|
|
5696
|
+
let sep;
|
|
5697
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
5698
|
+
const rawEvent = buffer.slice(0, sep);
|
|
5699
|
+
buffer = buffer.slice(sep + 2);
|
|
5700
|
+
const parsed = parseSseData(rawEvent);
|
|
5701
|
+
if (parsed && "done" in parsed) return;
|
|
5702
|
+
if (parsed && "content" in parsed) yield parsed.content;
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
const tail = buffer.trim();
|
|
5706
|
+
if (tail) {
|
|
5707
|
+
const parsed = parseSseData(tail);
|
|
5708
|
+
if (parsed && "content" in parsed) yield parsed.content;
|
|
5709
|
+
}
|
|
5710
|
+
} finally {
|
|
5711
|
+
try {
|
|
5712
|
+
reader.releaseLock();
|
|
5713
|
+
} catch {
|
|
5714
|
+
}
|
|
5715
|
+
try {
|
|
5716
|
+
await stream.cancel();
|
|
5717
|
+
} catch {
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
return {
|
|
5722
|
+
meta,
|
|
5723
|
+
[Symbol.asyncIterator]: iterate
|
|
5724
|
+
};
|
|
5725
|
+
}
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5457
5729
|
// src/mocks.ts
|
|
5458
5730
|
init_errors();
|
|
5459
5731
|
var DEV_FIXTURE_USER = Object.freeze({
|
|
@@ -5739,6 +6011,57 @@ var MockEntitlements = class {
|
|
|
5739
6011
|
this._denied.clear();
|
|
5740
6012
|
}
|
|
5741
6013
|
};
|
|
6014
|
+
var MockAi = class {
|
|
6015
|
+
_calls = [];
|
|
6016
|
+
_reply(opts) {
|
|
6017
|
+
const last = [...opts.messages].reverse().find((m) => m.role === "user") ?? opts.messages[opts.messages.length - 1];
|
|
6018
|
+
const content = last?.content ?? "";
|
|
6019
|
+
return content ? `mock-reply: ${content}` : "mock-reply";
|
|
6020
|
+
}
|
|
6021
|
+
async chat(opts) {
|
|
6022
|
+
if (!opts || typeof opts.model !== "string" || opts.model.length === 0) {
|
|
6023
|
+
throw new NeetruError("validation_failed", "ai: model is required");
|
|
6024
|
+
}
|
|
6025
|
+
if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
|
|
6026
|
+
throw new NeetruError("validation_failed", "ai: messages must be a non-empty array");
|
|
6027
|
+
}
|
|
6028
|
+
this._calls.push({ model: opts.model, messages: opts.messages, stream: false });
|
|
6029
|
+
const text = this._reply(opts);
|
|
6030
|
+
const promptTokens = opts.messages.reduce((n, m) => n + (m.content?.length ?? 0), 0);
|
|
6031
|
+
const completionTokens = text.length;
|
|
6032
|
+
return {
|
|
6033
|
+
choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
|
|
6034
|
+
usage: {
|
|
6035
|
+
prompt_tokens: promptTokens,
|
|
6036
|
+
completion_tokens: completionTokens,
|
|
6037
|
+
total_tokens: promptTokens + completionTokens
|
|
6038
|
+
},
|
|
6039
|
+
model: opts.model,
|
|
6040
|
+
provider: "mock",
|
|
6041
|
+
usageWarning: false
|
|
6042
|
+
};
|
|
6043
|
+
}
|
|
6044
|
+
stream(opts) {
|
|
6045
|
+
if (!opts || typeof opts.model !== "string" || opts.model.length === 0) {
|
|
6046
|
+
throw new NeetruError("validation_failed", "ai: model is required");
|
|
6047
|
+
}
|
|
6048
|
+
if (!Array.isArray(opts.messages) || opts.messages.length === 0) {
|
|
6049
|
+
throw new NeetruError("validation_failed", "ai: messages must be a non-empty array");
|
|
6050
|
+
}
|
|
6051
|
+
this._calls.push({ model: opts.model, messages: opts.messages, stream: true });
|
|
6052
|
+
const text = this._reply(opts);
|
|
6053
|
+
const parts = text.match(/\S+\s*/g) ?? [text];
|
|
6054
|
+
const meta = Promise.resolve({ provider: "mock", usageWarning: false });
|
|
6055
|
+
async function* iterate() {
|
|
6056
|
+
for (const p of parts) yield p;
|
|
6057
|
+
}
|
|
6058
|
+
return { meta, [Symbol.asyncIterator]: iterate };
|
|
6059
|
+
}
|
|
6060
|
+
/** Test helper — histórico de chamadas. */
|
|
6061
|
+
__getCalls() {
|
|
6062
|
+
return this._calls;
|
|
6063
|
+
}
|
|
6064
|
+
};
|
|
5742
6065
|
|
|
5743
6066
|
// src/auth.ts
|
|
5744
6067
|
function readEnvApiKey() {
|
|
@@ -6109,15 +6432,15 @@ function createOidcAuthNamespace(config) {
|
|
|
6109
6432
|
const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
|
|
6110
6433
|
const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
|
|
6111
6434
|
const issuer = idpOrigin;
|
|
6112
|
-
const jwksUrl = `${idpOrigin}/api/v1/oauth/.well-known/jwks
|
|
6435
|
+
const jwksUrl = `${idpOrigin}/api/v1/oauth/.well-known/jwks`;
|
|
6113
6436
|
const cacheTtlMs = options?.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
|
|
6114
6437
|
const audienceFromOptions = options?.audience;
|
|
6115
6438
|
const parsedKey = tryParseApiKey(config.apiKey);
|
|
6116
|
-
const audience = audienceFromOptions ?? parsedKey?.keyId;
|
|
6439
|
+
const audience = audienceFromOptions ?? config.oidcClientId ?? parsedKey?.keyId;
|
|
6117
6440
|
if (!audience) {
|
|
6118
6441
|
throw new NeetruError(
|
|
6119
6442
|
"invalid_config",
|
|
6120
|
-
"verifyToken requires an audience: provide apiKey on the client or pass options.audience. Calling jwtVerify without an audience allows tokens issued for other OIDC clients to pass."
|
|
6443
|
+
"verifyToken requires an audience: provide oidcClientId/apiKey on the client or pass options.audience. Calling jwtVerify without an audience allows tokens issued for other OIDC clients to pass."
|
|
6121
6444
|
);
|
|
6122
6445
|
}
|
|
6123
6446
|
const jwks = _getJwks(jwksUrl, cacheTtlMs);
|
|
@@ -6180,6 +6503,7 @@ function createNeetruClient(config = {}) {
|
|
|
6180
6503
|
const db = config.mocks?.db ?? createNeetruDb(resolved, config.db);
|
|
6181
6504
|
const webhooks = config.mocks?.webhooks ?? (isDev ? new MockWebhooks() : createWebhooksNamespace(resolved));
|
|
6182
6505
|
const notifications = config.mocks?.notifications ?? (isDev ? new MockNotifications() : createNotificationsNamespace(resolved));
|
|
6506
|
+
const ai = config.mocks?.ai ?? (isDev ? new MockAi() : createAiNamespace(resolved));
|
|
6183
6507
|
const client = Object.freeze({
|
|
6184
6508
|
config: resolved,
|
|
6185
6509
|
auth,
|
|
@@ -6191,7 +6515,8 @@ function createNeetruClient(config = {}) {
|
|
|
6191
6515
|
db,
|
|
6192
6516
|
checkout: createCheckoutNamespace(resolved),
|
|
6193
6517
|
webhooks,
|
|
6194
|
-
notifications
|
|
6518
|
+
notifications,
|
|
6519
|
+
ai
|
|
6195
6520
|
});
|
|
6196
6521
|
return client;
|
|
6197
6522
|
}
|