@neetru/sdk 1.1.1 → 1.2.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +33 -3
  2. package/README.md +135 -159
  3. package/dist/auth.cjs +482 -42
  4. package/dist/auth.cjs.map +1 -1
  5. package/dist/auth.d.cts +1 -1
  6. package/dist/auth.d.ts +1 -1
  7. package/dist/auth.mjs +482 -42
  8. package/dist/auth.mjs.map +1 -1
  9. package/dist/catalog.cjs +63 -24
  10. package/dist/catalog.cjs.map +1 -1
  11. package/dist/catalog.d.cts +2 -2
  12. package/dist/catalog.d.ts +2 -2
  13. package/dist/catalog.mjs +63 -24
  14. package/dist/catalog.mjs.map +1 -1
  15. package/dist/checkout.cjs +60 -18
  16. package/dist/checkout.cjs.map +1 -1
  17. package/dist/checkout.d.cts +1 -1
  18. package/dist/checkout.d.ts +1 -1
  19. package/dist/checkout.mjs +60 -18
  20. package/dist/checkout.mjs.map +1 -1
  21. package/dist/db.cjs +66 -25
  22. package/dist/db.cjs.map +1 -1
  23. package/dist/db.d.cts +1 -1
  24. package/dist/db.d.ts +1 -1
  25. package/dist/db.mjs +66 -25
  26. package/dist/db.mjs.map +1 -1
  27. package/dist/entitlements.cjs +101 -24
  28. package/dist/entitlements.cjs.map +1 -1
  29. package/dist/entitlements.d.cts +11 -5
  30. package/dist/entitlements.d.ts +11 -5
  31. package/dist/entitlements.mjs +101 -24
  32. package/dist/entitlements.mjs.map +1 -1
  33. package/dist/index.cjs +487 -43
  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 +484 -44
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/mocks.cjs +3 -2
  40. package/dist/mocks.cjs.map +1 -1
  41. package/dist/mocks.d.cts +4 -2
  42. package/dist/mocks.d.ts +4 -2
  43. package/dist/mocks.mjs +3 -2
  44. package/dist/mocks.mjs.map +1 -1
  45. package/dist/notifications.cjs +296 -0
  46. package/dist/notifications.cjs.map +1 -0
  47. package/dist/notifications.d.cts +1 -0
  48. package/dist/notifications.d.ts +1 -0
  49. package/dist/notifications.mjs +293 -0
  50. package/dist/notifications.mjs.map +1 -0
  51. package/dist/react.cjs +7 -3
  52. package/dist/react.cjs.map +1 -1
  53. package/dist/react.d.cts +1 -1
  54. package/dist/react.d.ts +1 -1
  55. package/dist/react.mjs +7 -3
  56. package/dist/react.mjs.map +1 -1
  57. package/dist/support.cjs +60 -18
  58. package/dist/support.cjs.map +1 -1
  59. package/dist/support.d.cts +1 -1
  60. package/dist/support.d.ts +1 -1
  61. package/dist/support.mjs +60 -18
  62. package/dist/support.mjs.map +1 -1
  63. package/dist/telemetry.cjs +130 -19
  64. package/dist/telemetry.cjs.map +1 -1
  65. package/dist/telemetry.d.cts +17 -1
  66. package/dist/telemetry.d.ts +17 -1
  67. package/dist/telemetry.mjs +130 -19
  68. package/dist/telemetry.mjs.map +1 -1
  69. package/dist/{types-BA53dd8S.d.cts → types-CQAfwqUS.d.cts} +172 -8
  70. package/dist/{types-BA53dd8S.d.ts → types-CQAfwqUS.d.ts} +172 -8
  71. package/dist/usage.cjs +60 -18
  72. package/dist/usage.cjs.map +1 -1
  73. package/dist/usage.d.cts +1 -1
  74. package/dist/usage.d.ts +1 -1
  75. package/dist/usage.mjs +60 -18
  76. package/dist/usage.mjs.map +1 -1
  77. package/dist/webhooks.cjs +316 -0
  78. package/dist/webhooks.cjs.map +1 -0
  79. package/dist/webhooks.d.cts +1 -0
  80. package/dist/webhooks.d.ts +1 -0
  81. package/dist/webhooks.mjs +312 -0
  82. package/dist/webhooks.mjs.map +1 -0
  83. package/package.json +12 -2
package/dist/auth.mjs CHANGED
@@ -17,6 +17,31 @@ var NeetruError = class _NeetruError extends Error {
17
17
  var DEFAULT_BASE_URL = "https://api.neetru.com";
18
18
 
19
19
  // src/http.ts
20
+ var DEFAULT_RETRIES = 2;
21
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
22
+ "rate_limited",
23
+ "server_error",
24
+ "network_error"
25
+ ]);
26
+ function backoffMs(attempt) {
27
+ const base = 200 * Math.pow(4, attempt);
28
+ const jitter = base * 0.2 * (Math.random() * 2 - 1);
29
+ return Math.max(50, Math.round(base + jitter));
30
+ }
31
+ function parseRetryAfter(value) {
32
+ if (!value) return null;
33
+ const secs = Number(value);
34
+ if (Number.isFinite(secs) && secs >= 0) return Math.round(secs * 1e3);
35
+ const dateMs = Date.parse(value);
36
+ if (Number.isFinite(dateMs)) {
37
+ const delta = dateMs - Date.now();
38
+ if (delta > 0) return delta;
39
+ }
40
+ return null;
41
+ }
42
+ function sleep(ms) {
43
+ return new Promise((resolve) => setTimeout(resolve, ms));
44
+ }
20
45
  function statusToCode(status) {
21
46
  if (status === 401) return "unauthorized";
22
47
  if (status === 403) return "forbidden";
@@ -50,6 +75,7 @@ async function safeJson(res) {
50
75
  async function httpRequest(config, opts) {
51
76
  const method = opts.method ?? "GET";
52
77
  const url = buildUrl(config.baseUrl, opts.path, opts.query);
78
+ const maxRetries = opts.retries ?? DEFAULT_RETRIES;
53
79
  const headers = {
54
80
  accept: "application/json",
55
81
  ...opts.headers
@@ -63,24 +89,32 @@ async function httpRequest(config, opts) {
63
89
  }
64
90
  headers.authorization = `Bearer ${config.apiKey}`;
65
91
  }
66
- const init = { method, headers };
67
- if (opts.body !== void 0 && method !== "GET" && method !== "DELETE") {
92
+ const bodyString = opts.body !== void 0 && method !== "GET" && method !== "DELETE" ? JSON.stringify(opts.body) : void 0;
93
+ if (bodyString !== void 0) {
68
94
  headers["content-type"] = "application/json";
69
- init.body = JSON.stringify(opts.body);
70
95
  }
71
- init.signal = AbortSignal.timeout(3e4);
72
- let res;
73
- try {
74
- res = await config.fetch(url, init);
75
- } catch (err) {
76
- if (err instanceof DOMException && err.name === "TimeoutError") {
77
- throw new NeetruError("network_error", "Network error: timeout after 30s");
96
+ let lastError = null;
97
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
98
+ const init = { method, headers };
99
+ if (bodyString !== void 0) init.body = bodyString;
100
+ init.signal = AbortSignal.timeout(3e4);
101
+ let res;
102
+ try {
103
+ res = await config.fetch(url, init);
104
+ } catch (err2) {
105
+ const message2 = err2 instanceof DOMException && err2.name === "TimeoutError" ? "Network error: timeout after 30s" : `Network error: ${err2 instanceof Error ? err2.message : "fetch failed"}`;
106
+ lastError = new NeetruError("network_error", message2);
107
+ if (attempt < maxRetries) {
108
+ await sleep(backoffMs(attempt));
109
+ continue;
110
+ }
111
+ throw lastError;
112
+ }
113
+ const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
114
+ if (res.ok) {
115
+ const parsed = await safeJson(res);
116
+ return parsed;
78
117
  }
79
- const message = err instanceof Error ? err.message : "fetch failed";
80
- throw new NeetruError("network_error", `Network error: ${message}`);
81
- }
82
- const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
83
- if (!res.ok) {
84
118
  const body = await safeJson(res);
85
119
  let code = statusToCode(res.status);
86
120
  let message = `HTTP ${res.status}`;
@@ -93,10 +127,18 @@ async function httpRequest(config, opts) {
93
127
  if (typeof errField.message === "string") message = errField.message;
94
128
  }
95
129
  }
96
- throw new NeetruError(code, message, res.status, requestId);
130
+ const err = new NeetruError(code, message, res.status, requestId);
131
+ lastError = err;
132
+ const isRetryable = RETRYABLE_CODES.has(code);
133
+ if (isRetryable && attempt < maxRetries) {
134
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
135
+ const delay = retryAfter ?? backoffMs(attempt);
136
+ await sleep(delay);
137
+ continue;
138
+ }
139
+ throw err;
97
140
  }
98
- const parsed = await safeJson(res);
99
- return parsed;
141
+ throw lastError ?? new NeetruError("unknown", "unexpected httpRequest exit");
100
142
  }
101
143
 
102
144
  // src/catalog.ts
@@ -136,12 +178,10 @@ function createCatalogNamespace(config) {
136
178
  * Lista produtos publicados. Por default só `published=true`; staff
137
179
  * pode passar `includeDrafts: true` (requer Bearer com role admin/operator).
138
180
  */
139
- async list(opts = {}) {
181
+ async list(_opts = {}) {
140
182
  const raw = await httpRequest(config, {
141
183
  method: "GET",
142
- path: "/api/v1/cli/catalog",
143
- query: opts.includeDrafts ? { drafts: "true" } : void 0,
144
- requireAuth: true
184
+ path: "/api/sdk/v1/catalog"
145
185
  });
146
186
  if (!raw || !Array.isArray(raw.products)) {
147
187
  throw new NeetruError(
@@ -165,8 +205,7 @@ function createCatalogNamespace(config) {
165
205
  }
166
206
  const raw = await httpRequest(config, {
167
207
  method: "GET",
168
- path: `/api/v1/cli/catalog/${encodeURIComponent(slug)}`,
169
- requireAuth: true
208
+ path: `/api/sdk/v1/catalog/${encodeURIComponent(slug)}`
170
209
  });
171
210
  if (!raw || !raw.product) {
172
211
  throw new NeetruError(
@@ -195,30 +234,65 @@ function toEntitlementCheck(raw) {
195
234
  reason: typeof r.reason === "string" ? r.reason : void 0
196
235
  };
197
236
  }
237
+ var CACHE_TTL_MS = 6e4;
238
+ var CACHE_MAX = 100;
198
239
  function createEntitlementsNamespace(config) {
199
- async function checkDetailed(productSlug, feature) {
240
+ const cache = /* @__PURE__ */ new Map();
241
+ function cacheKey(productSlug, feature) {
242
+ return `${productSlug}::${feature}`;
243
+ }
244
+ function readCache(key) {
245
+ const entry = cache.get(key);
246
+ if (!entry) return null;
247
+ if (entry.expiresAt < Date.now()) {
248
+ cache.delete(key);
249
+ return null;
250
+ }
251
+ cache.delete(key);
252
+ cache.set(key, entry);
253
+ return entry.value;
254
+ }
255
+ function writeCache(key, value) {
256
+ if (cache.size >= CACHE_MAX) {
257
+ const oldest = cache.keys().next().value;
258
+ if (oldest !== void 0) cache.delete(oldest);
259
+ }
260
+ cache.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });
261
+ }
262
+ async function checkDetailed(productSlug, feature, opts = {}) {
200
263
  if (!productSlug) throw new NeetruError("validation_failed", "productSlug is required");
201
264
  if (!feature) throw new NeetruError("validation_failed", "feature is required");
265
+ const key = cacheKey(productSlug, feature);
266
+ if (!opts.cacheBust) {
267
+ const cached = readCache(key);
268
+ if (cached) return cached;
269
+ }
202
270
  const raw = await httpRequest(config, {
203
271
  method: "GET",
204
272
  path: "/api/v1/sdk/entitlements/check",
205
273
  query: { slug: productSlug, feature },
206
274
  requireAuth: true
207
275
  });
208
- return toEntitlementCheck(raw);
276
+ const result = toEntitlementCheck(raw);
277
+ writeCache(key, result);
278
+ return result;
209
279
  }
210
280
  return {
211
281
  /**
212
282
  * Verifica se o caller pode usar `feature` no produto `productSlug`.
213
- * Retorno simples: `true` libera, `false` bloqueia.
283
+ * Retorno simples: `true` libera, `false` bloqueia. Cache 60s automático.
214
284
  *
215
285
  * Use `checkDetailed` se precisar do `reason` pra mensagem de upgrade.
216
286
  */
217
- async check(productSlug, feature) {
218
- const result = await checkDetailed(productSlug, feature);
287
+ async check(productSlug, feature, opts) {
288
+ const result = await checkDetailed(productSlug, feature, opts);
219
289
  return result.allowed;
220
290
  },
221
- checkDetailed
291
+ checkDetailed,
292
+ /** Test helper: limpa o cache LRU. */
293
+ __resetCache() {
294
+ cache.clear();
295
+ }
222
296
  };
223
297
  }
224
298
 
@@ -240,7 +314,44 @@ function consoleFor(level) {
240
314
  return console.log.bind(console);
241
315
  }
242
316
  }
317
+ var TRACK_FLUSH_MS = 500;
318
+ var TRACK_MAX_QUEUE = 50;
243
319
  function createTelemetryNamespace(config) {
320
+ const queue = [];
321
+ let flushTimer = null;
322
+ async function drainQueue() {
323
+ if (queue.length === 0) return;
324
+ const batch = queue.splice(0, queue.length);
325
+ flushTimer = null;
326
+ await Promise.allSettled(
327
+ batch.map(async (ev) => {
328
+ try {
329
+ const body = { name: ev.name };
330
+ if (ev.properties && typeof ev.properties === "object") {
331
+ body.properties = ev.properties;
332
+ }
333
+ if (ev.timestamp) body.timestamp = ev.timestamp;
334
+ await httpRequest(config, {
335
+ method: "POST",
336
+ path: "/api/v1/sdk/telemetry/event",
337
+ body,
338
+ requireAuth: true,
339
+ retries: 1
340
+ });
341
+ } catch (err) {
342
+ if (typeof console !== "undefined") {
343
+ console.warn("[neetru-sdk] telemetry.track flush failed for event", ev.name, err);
344
+ }
345
+ }
346
+ })
347
+ );
348
+ }
349
+ function scheduleFlush() {
350
+ if (flushTimer) return;
351
+ flushTimer = setTimeout(() => {
352
+ void drainQueue();
353
+ }, TRACK_FLUSH_MS);
354
+ }
244
355
  return {
245
356
  /**
246
357
  * Persiste um evento de uso. Lança `NeetruError` em qualquer falha
@@ -280,6 +391,38 @@ function createTelemetryNamespace(config) {
280
391
  }
281
392
  return { ok: true, eventId: raw.eventId };
282
393
  },
394
+ /**
395
+ * Fire-and-forget: enqueue + flush 500ms debounce. Não retorna `eventId`
396
+ * — falhas são logadas como warning. Ideal pra alta frequência.
397
+ *
398
+ * @example
399
+ * ```ts
400
+ * client.telemetry.track({ name: 'page_view', properties: { path: '/' } });
401
+ * // segue execução; flush async em background
402
+ * ```
403
+ */
404
+ track(input) {
405
+ if (!input || typeof input !== "object") return;
406
+ if (typeof input.name !== "string" || input.name.length === 0) return;
407
+ if (input.name.length > 128) return;
408
+ queue.push(input);
409
+ if (queue.length >= TRACK_MAX_QUEUE) {
410
+ void drainQueue();
411
+ } else {
412
+ scheduleFlush();
413
+ }
414
+ },
415
+ /**
416
+ * Força flush imediato da queue de `track()`. Resolva antes de
417
+ * page unload / SSR boundary pra não perder eventos.
418
+ */
419
+ async flush() {
420
+ if (flushTimer) {
421
+ clearTimeout(flushTimer);
422
+ flushTimer = null;
423
+ }
424
+ await drainQueue();
425
+ },
283
426
  /**
284
427
  * Registra um log estruturado per-product (Sprint 6).
285
428
  *
@@ -331,7 +474,7 @@ function createTelemetryNamespace(config) {
331
474
  if (cid) headers["x-correlation-id"] = cid;
332
475
  const raw = await httpRequest(config, {
333
476
  method: "POST",
334
- path: "/sdk/v1/telemetry/log",
477
+ path: "/api/sdk/v1/telemetry/log",
335
478
  body,
336
479
  requireAuth: true,
337
480
  headers
@@ -592,8 +735,7 @@ function createDbNamespace(config) {
592
735
  if (config.tenantId) headers["x-neetru-tenant"] = config.tenantId;
593
736
  return {
594
737
  async list(opts) {
595
- if (opts?.limit !== void 0) opts.limit;
596
- let path = `/sdk/v1/datastore/${name}`;
738
+ let path = `/api/sdk/v1/datastore/${name}`;
597
739
  const params = new URLSearchParams();
598
740
  if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
599
741
  if (opts?.where && opts.where.length > 0) {
@@ -625,7 +767,7 @@ function createDbNamespace(config) {
625
767
  try {
626
768
  const raw = await httpRequest(config, {
627
769
  method: "GET",
628
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
770
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
629
771
  requireAuth: true,
630
772
  headers
631
773
  });
@@ -641,7 +783,7 @@ function createDbNamespace(config) {
641
783
  }
642
784
  const raw = await httpRequest(config, {
643
785
  method: "POST",
644
- path: `/sdk/v1/datastore/${name}`,
786
+ path: `/api/sdk/v1/datastore/${name}`,
645
787
  body: { data },
646
788
  requireAuth: true,
647
789
  headers
@@ -657,7 +799,7 @@ function createDbNamespace(config) {
657
799
  }
658
800
  await httpRequest(config, {
659
801
  method: "PUT",
660
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
802
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
661
803
  body: { data },
662
804
  requireAuth: true,
663
805
  headers
@@ -670,7 +812,7 @@ function createDbNamespace(config) {
670
812
  }
671
813
  await httpRequest(config, {
672
814
  method: "PATCH",
673
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
815
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
674
816
  body: { data },
675
817
  requireAuth: true,
676
818
  headers
@@ -683,7 +825,7 @@ function createDbNamespace(config) {
683
825
  }
684
826
  await httpRequest(config, {
685
827
  method: "DELETE",
686
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
828
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
687
829
  requireAuth: true,
688
830
  headers
689
831
  });
@@ -869,6 +1011,297 @@ function createCheckoutNamespace(config) {
869
1011
  return createHttpCheckoutNamespace(config);
870
1012
  }
871
1013
 
1014
+ // src/webhooks.ts
1015
+ var VALID_EVENTS = [
1016
+ "subscription.activated",
1017
+ "subscription.cancelled",
1018
+ "subscription.payment_failed",
1019
+ "subscription.trial_ending",
1020
+ "usage.quota_exceeded",
1021
+ "account.suspended",
1022
+ "account.reactivated",
1023
+ "support.ticket_replied"
1024
+ ];
1025
+ function toEndpoint(raw) {
1026
+ if (!raw || typeof raw !== "object") {
1027
+ throw new NeetruError("invalid_response", "Webhook response is not an object");
1028
+ }
1029
+ const r = raw;
1030
+ if (typeof r.id !== "string") {
1031
+ throw new NeetruError("invalid_response", "Webhook missing id");
1032
+ }
1033
+ return {
1034
+ id: r.id,
1035
+ url: typeof r.url === "string" ? r.url : "",
1036
+ events: Array.isArray(r.events) ? r.events.filter(
1037
+ (e) => VALID_EVENTS.includes(e)
1038
+ ) : [],
1039
+ hasSecret: r.hasSecret === true,
1040
+ status: r.status === "active" || r.status === "degraded" || r.status === "disabled" ? r.status : "active",
1041
+ lastDeliveryAt: typeof r.lastDeliveryAt === "string" ? r.lastDeliveryAt : void 0,
1042
+ consecutiveFailures: typeof r.consecutiveFailures === "number" ? r.consecutiveFailures : void 0,
1043
+ createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString()
1044
+ };
1045
+ }
1046
+ function validateInput(input) {
1047
+ if (!input.url || typeof input.url !== "string") {
1048
+ throw new NeetruError("validation_failed", "url \xE9 obrigat\xF3ria");
1049
+ }
1050
+ try {
1051
+ const parsed = new URL(input.url);
1052
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
1053
+ throw new Error("invalid protocol");
1054
+ }
1055
+ } catch {
1056
+ throw new NeetruError("validation_failed", `url inv\xE1lida: ${input.url}`);
1057
+ }
1058
+ if (!Array.isArray(input.events) || input.events.length === 0) {
1059
+ throw new NeetruError("validation_failed", "events deve ter pelo menos 1 evento");
1060
+ }
1061
+ for (const ev of input.events) {
1062
+ if (!VALID_EVENTS.includes(ev)) {
1063
+ throw new NeetruError("validation_failed", `evento desconhecido: ${ev}`);
1064
+ }
1065
+ }
1066
+ if (input.secret !== void 0 && input.secret.length < 16) {
1067
+ throw new NeetruError("validation_failed", "secret deve ter \u226516 chars (recomendado 32+)");
1068
+ }
1069
+ }
1070
+ function createWebhooksNamespace(config) {
1071
+ return {
1072
+ async register(input) {
1073
+ validateInput(input);
1074
+ const raw = await httpRequest(config, {
1075
+ method: "POST",
1076
+ path: "/api/sdk/v1/webhooks",
1077
+ body: input,
1078
+ requireAuth: true
1079
+ });
1080
+ return toEndpoint(raw);
1081
+ },
1082
+ async list() {
1083
+ const raw = await httpRequest(config, {
1084
+ method: "GET",
1085
+ path: "/api/sdk/v1/webhooks",
1086
+ requireAuth: true
1087
+ });
1088
+ const list = Array.isArray(raw?.endpoints) ? raw.endpoints : [];
1089
+ return list.map(toEndpoint);
1090
+ },
1091
+ async unregister(id) {
1092
+ if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
1093
+ await httpRequest(config, {
1094
+ method: "DELETE",
1095
+ path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}`,
1096
+ requireAuth: true
1097
+ });
1098
+ return { ok: true };
1099
+ },
1100
+ async test(id) {
1101
+ if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
1102
+ const raw = await httpRequest(config, {
1103
+ method: "POST",
1104
+ path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}/test`,
1105
+ requireAuth: true
1106
+ });
1107
+ if (!raw || typeof raw !== "object") {
1108
+ return { ok: false, error: "invalid response" };
1109
+ }
1110
+ const r = raw;
1111
+ return {
1112
+ ok: r.ok === true,
1113
+ statusCode: typeof r.statusCode === "number" ? r.statusCode : void 0,
1114
+ durationMs: typeof r.durationMs === "number" ? r.durationMs : void 0,
1115
+ error: typeof r.error === "string" ? r.error : void 0
1116
+ };
1117
+ }
1118
+ };
1119
+ }
1120
+ var MockWebhooks = class {
1121
+ endpoints = /* @__PURE__ */ new Map();
1122
+ nextId = 1;
1123
+ async register(input) {
1124
+ validateInput(input);
1125
+ const id = `mock_wh_${this.nextId++}`;
1126
+ const endpoint = {
1127
+ id,
1128
+ url: input.url,
1129
+ events: input.events,
1130
+ hasSecret: !!input.secret,
1131
+ status: "active",
1132
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1133
+ };
1134
+ this.endpoints.set(id, endpoint);
1135
+ return endpoint;
1136
+ }
1137
+ async list() {
1138
+ return [...this.endpoints.values()];
1139
+ }
1140
+ async unregister(id) {
1141
+ if (!this.endpoints.has(id)) {
1142
+ throw new NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
1143
+ }
1144
+ this.endpoints.delete(id);
1145
+ return { ok: true };
1146
+ }
1147
+ async test(id) {
1148
+ if (!this.endpoints.has(id)) {
1149
+ throw new NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
1150
+ }
1151
+ return { ok: true, statusCode: 200, durationMs: 42 };
1152
+ }
1153
+ };
1154
+
1155
+ // src/notifications.ts
1156
+ var VALID_SEVERITIES2 = [
1157
+ "info",
1158
+ "success",
1159
+ "warning",
1160
+ "error"
1161
+ ];
1162
+ function toNotification(raw) {
1163
+ if (!raw || typeof raw !== "object") {
1164
+ throw new NeetruError("invalid_response", "Notification response is not an object");
1165
+ }
1166
+ const r = raw;
1167
+ if (typeof r.id !== "string") {
1168
+ throw new NeetruError("invalid_response", "Notification missing id");
1169
+ }
1170
+ const sev = VALID_SEVERITIES2.includes(r.severity) ? r.severity : "info";
1171
+ return {
1172
+ id: r.id,
1173
+ userId: typeof r.userId === "string" ? r.userId : "",
1174
+ kind: typeof r.kind === "string" ? r.kind : "unknown",
1175
+ severity: sev,
1176
+ title: typeof r.title === "string" ? r.title : "",
1177
+ body: typeof r.body === "string" ? r.body : void 0,
1178
+ link: typeof r.link === "string" ? r.link : void 0,
1179
+ metadata: r.metadata && typeof r.metadata === "object" ? r.metadata : void 0,
1180
+ createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
1181
+ readAt: typeof r.readAt === "string" ? r.readAt : void 0,
1182
+ dismissedAt: typeof r.dismissedAt === "string" ? r.dismissedAt : void 0
1183
+ };
1184
+ }
1185
+ function validateInput2(input) {
1186
+ if (!input.userId) throw new NeetruError("validation_failed", "userId obrigat\xF3rio");
1187
+ if (!input.kind) throw new NeetruError("validation_failed", "kind obrigat\xF3rio");
1188
+ if (!input.title) throw new NeetruError("validation_failed", "title obrigat\xF3rio");
1189
+ if (input.severity && !VALID_SEVERITIES2.includes(input.severity)) {
1190
+ throw new NeetruError(
1191
+ "validation_failed",
1192
+ `severity inv\xE1lida: ${input.severity} (use ${VALID_SEVERITIES2.join("|")})`
1193
+ );
1194
+ }
1195
+ if (input.title.length > 200) {
1196
+ throw new NeetruError("validation_failed", "title m\xE1x 200 chars");
1197
+ }
1198
+ if (input.body && input.body.length > 2e3) {
1199
+ throw new NeetruError("validation_failed", "body m\xE1x 2000 chars");
1200
+ }
1201
+ }
1202
+ function createNotificationsNamespace(config) {
1203
+ return {
1204
+ async send(input) {
1205
+ validateInput2(input);
1206
+ const raw = await httpRequest(config, {
1207
+ method: "POST",
1208
+ path: "/api/sdk/v1/notifications",
1209
+ body: input,
1210
+ requireAuth: true
1211
+ });
1212
+ return toNotification(raw);
1213
+ },
1214
+ async list(userId, options) {
1215
+ if (!userId) throw new NeetruError("validation_failed", "userId obrigat\xF3rio");
1216
+ const params = new URLSearchParams();
1217
+ if (options?.includeDismissed) params.set("includeDismissed", "true");
1218
+ if (options?.onlyUnread) params.set("onlyUnread", "true");
1219
+ if (options?.limit) {
1220
+ params.set("limit", Math.min(Math.max(1, options.limit), 200).toString());
1221
+ }
1222
+ const qs = params.toString();
1223
+ const raw = await httpRequest(config, {
1224
+ method: "GET",
1225
+ path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}${qs ? `?${qs}` : ""}`,
1226
+ requireAuth: true
1227
+ });
1228
+ const list = Array.isArray(raw?.notifications) ? raw.notifications : [];
1229
+ return list.map(toNotification);
1230
+ },
1231
+ async markRead(id) {
1232
+ if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
1233
+ await httpRequest(config, {
1234
+ method: "POST",
1235
+ path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/read`,
1236
+ requireAuth: true
1237
+ });
1238
+ return { ok: true };
1239
+ },
1240
+ async dismiss(id) {
1241
+ if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
1242
+ await httpRequest(config, {
1243
+ method: "POST",
1244
+ path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/dismiss`,
1245
+ requireAuth: true
1246
+ });
1247
+ return { ok: true };
1248
+ }
1249
+ };
1250
+ }
1251
+ var MockNotifications = class {
1252
+ notifications = /* @__PURE__ */ new Map();
1253
+ nextId = 1;
1254
+ async send(input) {
1255
+ validateInput2(input);
1256
+ if (input.fingerprint) {
1257
+ const dayAgo = Date.now() - 864e5;
1258
+ for (const existing of this.notifications.values()) {
1259
+ const meta = existing.metadata ?? {};
1260
+ if (meta.fingerprint === input.fingerprint && existing.userId === input.userId && new Date(existing.createdAt).getTime() > dayAgo) {
1261
+ return existing;
1262
+ }
1263
+ }
1264
+ }
1265
+ const id = `mock_notif_${this.nextId++}`;
1266
+ const notif = {
1267
+ id,
1268
+ userId: input.userId,
1269
+ kind: input.kind,
1270
+ severity: input.severity ?? "info",
1271
+ title: input.title,
1272
+ body: input.body,
1273
+ link: input.link,
1274
+ metadata: input.fingerprint ? { ...input.metadata ?? {}, fingerprint: input.fingerprint } : input.metadata,
1275
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1276
+ };
1277
+ this.notifications.set(id, notif);
1278
+ return notif;
1279
+ }
1280
+ async list(userId, options) {
1281
+ if (!userId) throw new NeetruError("validation_failed", "userId obrigat\xF3rio");
1282
+ const limit = Math.min(Math.max(1, options?.limit ?? 50), 200);
1283
+ return [...this.notifications.values()].filter((n) => n.userId === userId).filter((n) => options?.includeDismissed || !n.dismissedAt).filter((n) => !options?.onlyUnread || !n.readAt).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit);
1284
+ }
1285
+ async markRead(id) {
1286
+ const n = this.notifications.get(id);
1287
+ if (!n) throw new NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
1288
+ if (!n.readAt) {
1289
+ n.readAt = (/* @__PURE__ */ new Date()).toISOString();
1290
+ this.notifications.set(id, n);
1291
+ }
1292
+ return { ok: true };
1293
+ }
1294
+ async dismiss(id) {
1295
+ const n = this.notifications.get(id);
1296
+ if (!n) throw new NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
1297
+ if (!n.dismissedAt) {
1298
+ n.dismissedAt = (/* @__PURE__ */ new Date()).toISOString();
1299
+ this.notifications.set(id, n);
1300
+ }
1301
+ return { ok: true };
1302
+ }
1303
+ };
1304
+
872
1305
  // src/mocks.ts
873
1306
  var DEV_FIXTURE_USER = Object.freeze({
874
1307
  uid: "dev-fixture-uid-0001",
@@ -1004,14 +1437,14 @@ var MockUsage = class {
1004
1437
  this._counters.clear();
1005
1438
  }
1006
1439
  };
1007
- var mockTicketSeq = 0;
1008
1440
  var MockSupport = class {
1009
1441
  _tickets = [];
1442
+ _ticketSeq = 0;
1010
1443
  async createTicket(input) {
1011
1444
  if (!input?.subject) throw new Error("subject required");
1012
1445
  if (!input?.message) throw new Error("message required");
1013
1446
  const ticket = {
1014
- id: `mock-ticket-${++mockTicketSeq}`,
1447
+ id: `mock-ticket-${++this._ticketSeq}`,
1015
1448
  subject: input.subject,
1016
1449
  message: input.message,
1017
1450
  severity: input.severity ?? "normal",
@@ -1028,6 +1461,7 @@ var MockSupport = class {
1028
1461
  /** Test helper. */
1029
1462
  __reset() {
1030
1463
  this._tickets = [];
1464
+ this._ticketSeq = 0;
1031
1465
  }
1032
1466
  };
1033
1467
  var MockEntitlements = class {
@@ -1250,7 +1684,9 @@ function createOidcAuthNamespace(config) {
1250
1684
  if (storage) storage.removeItem(STORAGE_KEY);
1251
1685
  cachedUser = null;
1252
1686
  try {
1253
- await config.fetch(`${config.baseUrl}/oauth/revoke`, {
1687
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
1688
+ const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
1689
+ await config.fetch(`${idpOrigin}/api/v1/oauth/revoke`, {
1254
1690
  method: "POST",
1255
1691
  headers: { "content-type": "application/json" }
1256
1692
  });
@@ -1298,6 +1734,8 @@ function createNeetruClient(config = {}) {
1298
1734
  const support = config.mocks?.support ?? (isDev ? new MockSupport() : createSupportNamespace(resolved));
1299
1735
  const entitlements = config.mocks?.entitlements ?? (isDev ? new MockEntitlements() : createEntitlementsNamespace(resolved));
1300
1736
  const db = config.mocks?.db ?? (isDev ? new MockDb() : createDbNamespace(resolved));
1737
+ const webhooks = config.mocks?.webhooks ?? (isDev ? new MockWebhooks() : createWebhooksNamespace(resolved));
1738
+ const notifications = config.mocks?.notifications ?? (isDev ? new MockNotifications() : createNotificationsNamespace(resolved));
1301
1739
  const client = Object.freeze({
1302
1740
  config: resolved,
1303
1741
  auth,
@@ -1307,7 +1745,9 @@ function createNeetruClient(config = {}) {
1307
1745
  usage,
1308
1746
  support,
1309
1747
  db,
1310
- checkout: createCheckoutNamespace(resolved)
1748
+ checkout: createCheckoutNamespace(resolved),
1749
+ webhooks,
1750
+ notifications
1311
1751
  });
1312
1752
  return client;
1313
1753
  }