@neetru/sdk 1.1.0 → 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 +486 -40
  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 +486 -40
  8. package/dist/auth.mjs.map +1 -1
  9. package/dist/catalog.cjs +64 -21
  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 +64 -21
  14. package/dist/catalog.mjs.map +1 -1
  15. package/dist/checkout.cjs +61 -15
  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 +61 -15
  20. package/dist/checkout.mjs.map +1 -1
  21. package/dist/db.cjs +67 -22
  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 +67 -22
  26. package/dist/db.mjs.map +1 -1
  27. package/dist/entitlements.cjs +102 -21
  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 +102 -21
  32. package/dist/entitlements.mjs.map +1 -1
  33. package/dist/index.cjs +491 -41
  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 +488 -42
  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 +61 -15
  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 +61 -15
  62. package/dist/support.mjs.map +1 -1
  63. package/dist/telemetry.cjs +131 -16
  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 +131 -16
  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 +61 -15
  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 +61 -15
  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 +22 -6
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,20 +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
- }
73
- let res;
74
- try {
75
- res = await config.fetch(url, init);
76
- } catch (err) {
77
- const message = err instanceof Error ? err.message : "fetch failed";
78
- throw new NeetruError("network_error", `Network error: ${message}`);
79
97
  }
80
- const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
81
- if (!res.ok) {
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;
119
+ }
82
120
  const body = await safeJson(res);
83
121
  let code = statusToCode(res.status);
84
122
  let message = `HTTP ${res.status}`;
@@ -91,10 +129,18 @@ async function httpRequest(config, opts) {
91
129
  if (typeof errField.message === "string") message = errField.message;
92
130
  }
93
131
  }
94
- 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;
95
142
  }
96
- const parsed = await safeJson(res);
97
- return parsed;
143
+ throw lastError ?? new NeetruError("unknown", "unexpected httpRequest exit");
98
144
  }
99
145
 
100
146
  // src/catalog.ts
@@ -134,12 +180,10 @@ function createCatalogNamespace(config) {
134
180
  * Lista produtos publicados. Por default só `published=true`; staff
135
181
  * pode passar `includeDrafts: true` (requer Bearer com role admin/operator).
136
182
  */
137
- async list(opts = {}) {
183
+ async list(_opts = {}) {
138
184
  const raw = await httpRequest(config, {
139
185
  method: "GET",
140
- path: "/api/v1/cli/catalog",
141
- query: opts.includeDrafts ? { drafts: "true" } : void 0,
142
- requireAuth: true
186
+ path: "/api/sdk/v1/catalog"
143
187
  });
144
188
  if (!raw || !Array.isArray(raw.products)) {
145
189
  throw new NeetruError(
@@ -163,8 +207,7 @@ function createCatalogNamespace(config) {
163
207
  }
164
208
  const raw = await httpRequest(config, {
165
209
  method: "GET",
166
- path: `/api/v1/cli/catalog/${encodeURIComponent(slug)}`,
167
- requireAuth: true
210
+ path: `/api/sdk/v1/catalog/${encodeURIComponent(slug)}`
168
211
  });
169
212
  if (!raw || !raw.product) {
170
213
  throw new NeetruError(
@@ -193,30 +236,65 @@ function toEntitlementCheck(raw) {
193
236
  reason: typeof r.reason === "string" ? r.reason : void 0
194
237
  };
195
238
  }
239
+ var CACHE_TTL_MS = 6e4;
240
+ var CACHE_MAX = 100;
196
241
  function createEntitlementsNamespace(config) {
197
- 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 = {}) {
198
265
  if (!productSlug) throw new NeetruError("validation_failed", "productSlug is required");
199
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
+ }
200
272
  const raw = await httpRequest(config, {
201
273
  method: "GET",
202
274
  path: "/api/v1/sdk/entitlements/check",
203
275
  query: { slug: productSlug, feature },
204
276
  requireAuth: true
205
277
  });
206
- return toEntitlementCheck(raw);
278
+ const result = toEntitlementCheck(raw);
279
+ writeCache(key, result);
280
+ return result;
207
281
  }
208
282
  return {
209
283
  /**
210
284
  * Verifica se o caller pode usar `feature` no produto `productSlug`.
211
- * Retorno simples: `true` libera, `false` bloqueia.
285
+ * Retorno simples: `true` libera, `false` bloqueia. Cache 60s automático.
212
286
  *
213
287
  * Use `checkDetailed` se precisar do `reason` pra mensagem de upgrade.
214
288
  */
215
- async check(productSlug, feature) {
216
- const result = await checkDetailed(productSlug, feature);
289
+ async check(productSlug, feature, opts) {
290
+ const result = await checkDetailed(productSlug, feature, opts);
217
291
  return result.allowed;
218
292
  },
219
- checkDetailed
293
+ checkDetailed,
294
+ /** Test helper: limpa o cache LRU. */
295
+ __resetCache() {
296
+ cache.clear();
297
+ }
220
298
  };
221
299
  }
222
300
 
@@ -238,7 +316,44 @@ function consoleFor(level) {
238
316
  return console.log.bind(console);
239
317
  }
240
318
  }
319
+ var TRACK_FLUSH_MS = 500;
320
+ var TRACK_MAX_QUEUE = 50;
241
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
+ }
242
357
  return {
243
358
  /**
244
359
  * Persiste um evento de uso. Lança `NeetruError` em qualquer falha
@@ -278,6 +393,38 @@ function createTelemetryNamespace(config) {
278
393
  }
279
394
  return { ok: true, eventId: raw.eventId };
280
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
+ },
281
428
  /**
282
429
  * Registra um log estruturado per-product (Sprint 6).
283
430
  *
@@ -329,7 +476,7 @@ function createTelemetryNamespace(config) {
329
476
  if (cid) headers["x-correlation-id"] = cid;
330
477
  const raw = await httpRequest(config, {
331
478
  method: "POST",
332
- path: "/sdk/v1/telemetry/log",
479
+ path: "/api/sdk/v1/telemetry/log",
333
480
  body,
334
481
  requireAuth: true,
335
482
  headers
@@ -590,8 +737,7 @@ function createDbNamespace(config) {
590
737
  if (config.tenantId) headers["x-neetru-tenant"] = config.tenantId;
591
738
  return {
592
739
  async list(opts) {
593
- if (opts?.limit !== void 0) opts.limit;
594
- let path = `/sdk/v1/datastore/${name}`;
740
+ let path = `/api/sdk/v1/datastore/${name}`;
595
741
  const params = new URLSearchParams();
596
742
  if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
597
743
  if (opts?.where && opts.where.length > 0) {
@@ -623,7 +769,7 @@ function createDbNamespace(config) {
623
769
  try {
624
770
  const raw = await httpRequest(config, {
625
771
  method: "GET",
626
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
772
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
627
773
  requireAuth: true,
628
774
  headers
629
775
  });
@@ -639,7 +785,7 @@ function createDbNamespace(config) {
639
785
  }
640
786
  const raw = await httpRequest(config, {
641
787
  method: "POST",
642
- path: `/sdk/v1/datastore/${name}`,
788
+ path: `/api/sdk/v1/datastore/${name}`,
643
789
  body: { data },
644
790
  requireAuth: true,
645
791
  headers
@@ -655,7 +801,7 @@ function createDbNamespace(config) {
655
801
  }
656
802
  await httpRequest(config, {
657
803
  method: "PUT",
658
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
804
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
659
805
  body: { data },
660
806
  requireAuth: true,
661
807
  headers
@@ -668,7 +814,7 @@ function createDbNamespace(config) {
668
814
  }
669
815
  await httpRequest(config, {
670
816
  method: "PATCH",
671
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
817
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
672
818
  body: { data },
673
819
  requireAuth: true,
674
820
  headers
@@ -681,7 +827,7 @@ function createDbNamespace(config) {
681
827
  }
682
828
  await httpRequest(config, {
683
829
  method: "DELETE",
684
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
830
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
685
831
  requireAuth: true,
686
832
  headers
687
833
  });
@@ -867,6 +1013,297 @@ function createCheckoutNamespace(config) {
867
1013
  return createHttpCheckoutNamespace(config);
868
1014
  }
869
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
+
870
1307
  // src/mocks.ts
871
1308
  var DEV_FIXTURE_USER = Object.freeze({
872
1309
  uid: "dev-fixture-uid-0001",
@@ -1002,14 +1439,14 @@ var MockUsage = class {
1002
1439
  this._counters.clear();
1003
1440
  }
1004
1441
  };
1005
- var mockTicketSeq = 0;
1006
1442
  var MockSupport = class {
1007
1443
  _tickets = [];
1444
+ _ticketSeq = 0;
1008
1445
  async createTicket(input) {
1009
1446
  if (!input?.subject) throw new Error("subject required");
1010
1447
  if (!input?.message) throw new Error("message required");
1011
1448
  const ticket = {
1012
- id: `mock-ticket-${++mockTicketSeq}`,
1449
+ id: `mock-ticket-${++this._ticketSeq}`,
1013
1450
  subject: input.subject,
1014
1451
  message: input.message,
1015
1452
  severity: input.severity ?? "normal",
@@ -1026,6 +1463,7 @@ var MockSupport = class {
1026
1463
  /** Test helper. */
1027
1464
  __reset() {
1028
1465
  this._tickets = [];
1466
+ this._ticketSeq = 0;
1029
1467
  }
1030
1468
  };
1031
1469
  var MockEntitlements = class {
@@ -1224,7 +1662,9 @@ function createOidcAuthNamespace(config) {
1224
1662
  const redirectUri = options?.redirectUri ?? globalThis.location.origin;
1225
1663
  const scope = options?.scope ?? "openid profile email";
1226
1664
  const state = options?.postLoginRedirect ?? globalThis.location.href;
1227
- const url = new URL("/oauth/authorize", config.baseUrl.replace(/\/api$/, ""));
1665
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
1666
+ const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
1667
+ const url = new URL("/api/v1/oauth/authorize", idpOrigin);
1228
1668
  url.searchParams.set("response_type", "code");
1229
1669
  url.searchParams.set("redirect_uri", redirectUri);
1230
1670
  url.searchParams.set("scope", scope);
@@ -1246,7 +1686,9 @@ function createOidcAuthNamespace(config) {
1246
1686
  if (storage) storage.removeItem(STORAGE_KEY);
1247
1687
  cachedUser = null;
1248
1688
  try {
1249
- 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`, {
1250
1692
  method: "POST",
1251
1693
  headers: { "content-type": "application/json" }
1252
1694
  });
@@ -1294,6 +1736,8 @@ function createNeetruClient(config = {}) {
1294
1736
  const support = config.mocks?.support ?? (isDev ? new MockSupport() : createSupportNamespace(resolved));
1295
1737
  const entitlements = config.mocks?.entitlements ?? (isDev ? new MockEntitlements() : createEntitlementsNamespace(resolved));
1296
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));
1297
1741
  const client = Object.freeze({
1298
1742
  config: resolved,
1299
1743
  auth,
@@ -1303,13 +1747,15 @@ function createNeetruClient(config = {}) {
1303
1747
  usage,
1304
1748
  support,
1305
1749
  db,
1306
- checkout: createCheckoutNamespace(resolved)
1750
+ checkout: createCheckoutNamespace(resolved),
1751
+ webhooks,
1752
+ notifications
1307
1753
  });
1308
1754
  return client;
1309
1755
  }
1310
1756
 
1311
1757
  // src/index.ts
1312
- var VERSION = "1.1.0";
1758
+ var VERSION = "1.2.0";
1313
1759
  function initNeetru(config) {
1314
1760
  const { apiUrl, baseUrl, ...rest } = config;
1315
1761
  return createNeetruClient({ ...rest, baseUrl: baseUrl ?? apiUrl });
@@ -1321,12 +1767,16 @@ exports.MockAuth = MockAuth;
1321
1767
  exports.MockCheckout = MockCheckout;
1322
1768
  exports.MockDb = MockDb;
1323
1769
  exports.MockEntitlements = MockEntitlements;
1770
+ exports.MockNotifications = MockNotifications;
1324
1771
  exports.MockSupport = MockSupport;
1325
1772
  exports.MockUsage = MockUsage;
1773
+ exports.MockWebhooks = MockWebhooks;
1326
1774
  exports.NeetruError = NeetruError;
1327
1775
  exports.VERSION = VERSION;
1328
1776
  exports.createCheckoutNamespace = createCheckoutNamespace;
1329
1777
  exports.createNeetruClient = createNeetruClient;
1778
+ exports.createNotificationsNamespace = createNotificationsNamespace;
1779
+ exports.createWebhooksNamespace = createWebhooksNamespace;
1330
1780
  exports.initNeetru = initNeetru;
1331
1781
  //# sourceMappingURL=index.cjs.map
1332
1782
  //# sourceMappingURL=index.cjs.map