@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.
Files changed (69) hide show
  1. package/dist/ai.cjs +304 -0
  2. package/dist/ai.cjs.map +1 -0
  3. package/dist/ai.d.cts +3 -0
  4. package/dist/ai.d.ts +3 -0
  5. package/dist/ai.mjs +302 -0
  6. package/dist/ai.mjs.map +1 -0
  7. package/dist/auth.cjs +348 -23
  8. package/dist/auth.cjs.map +1 -1
  9. package/dist/auth.d.cts +1 -1
  10. package/dist/auth.d.ts +1 -1
  11. package/dist/auth.mjs +348 -23
  12. package/dist/auth.mjs.map +1 -1
  13. package/dist/catalog.d.cts +1 -1
  14. package/dist/catalog.d.ts +1 -1
  15. package/dist/checkout.d.cts +1 -1
  16. package/dist/checkout.d.ts +1 -1
  17. package/dist/db-react.d.cts +1 -1
  18. package/dist/db-react.d.ts +1 -1
  19. package/dist/db.cjs +55 -16
  20. package/dist/db.cjs.map +1 -1
  21. package/dist/db.d.cts +1 -1
  22. package/dist/db.d.ts +1 -1
  23. package/dist/db.mjs +55 -16
  24. package/dist/db.mjs.map +1 -1
  25. package/dist/entitlements.d.cts +1 -1
  26. package/dist/entitlements.d.ts +1 -1
  27. package/dist/firestore-compat.cjs +4 -9
  28. package/dist/firestore-compat.cjs.map +1 -1
  29. package/dist/firestore-compat.d.cts +2 -2
  30. package/dist/firestore-compat.d.ts +2 -2
  31. package/dist/firestore-compat.mjs +4 -9
  32. package/dist/firestore-compat.mjs.map +1 -1
  33. package/dist/index.cjs +351 -24
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +3 -3
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.mjs +350 -25
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/mocks.cjs +52 -0
  40. package/dist/mocks.cjs.map +1 -1
  41. package/dist/mocks.d.cts +20 -2
  42. package/dist/mocks.d.ts +20 -2
  43. package/dist/mocks.mjs +52 -1
  44. package/dist/mocks.mjs.map +1 -1
  45. package/dist/notifications.d.cts +1 -1
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/react.cjs.map +1 -1
  48. package/dist/react.d.cts +1 -1
  49. package/dist/react.d.ts +1 -1
  50. package/dist/react.mjs.map +1 -1
  51. package/dist/support.d.cts +1 -1
  52. package/dist/support.d.ts +1 -1
  53. package/dist/telemetry.cjs +5 -1
  54. package/dist/telemetry.cjs.map +1 -1
  55. package/dist/telemetry.d.cts +1 -1
  56. package/dist/telemetry.d.ts +1 -1
  57. package/dist/telemetry.mjs +5 -1
  58. package/dist/telemetry.mjs.map +1 -1
  59. package/dist/{types-BXvGHppn.d.cts → types-Cog9S-tC.d.cts} +103 -3
  60. package/dist/{types-CQVak2a3.d.ts → types-dagUxKYz.d.ts} +103 -3
  61. package/dist/usage.cjs +2 -2
  62. package/dist/usage.cjs.map +1 -1
  63. package/dist/usage.d.cts +1 -1
  64. package/dist/usage.d.ts +1 -1
  65. package/dist/usage.mjs +2 -2
  66. package/dist/usage.mjs.map +1 -1
  67. package/dist/webhooks.d.cts +1 -1
  68. package/dist/webhooks.d.ts +1 -1
  69. package/package.json +6 -1
package/dist/auth.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as NeetruClientConfig, B as NeetruClient } from './types-BXvGHppn.cjs';
1
+ import { S as NeetruClientConfig, R as NeetruClient } from './types-Cog9S-tC.cjs';
2
2
  import 'drizzle-orm/node-postgres';
3
3
  import './errors.cjs';
4
4
 
package/dist/auth.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as NeetruClientConfig, B as NeetruClient } from './types-CQVak2a3.js';
1
+ import { S as NeetruClientConfig, R as NeetruClient } from './types-dagUxKYz.js';
2
2
  import 'drizzle-orm/node-postgres';
3
3
  import './errors.js';
4
4
 
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
- return false;
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 true;
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: "POST",
4554
- path: DATASTORE_COLLECTION_ENDPOINT(m.collection),
4579
+ method: "PUT",
4580
+ path: DATASTORE_DOC_ENDPOINT(m.collection, m.docId),
4555
4581
  body: { data: m.payload ?? {} },
4556
4582
  requireAuth: true,
4557
- retries: 0,
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
- throw new NeetruDbError("db_unavailable", `pushMutations falhou: ${msg}`);
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.json`;
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
  }