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