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