@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/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,20 +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
- }
71
- let res;
72
- try {
73
- res = await config.fetch(url, init);
74
- } catch (err) {
75
- const message = err instanceof Error ? err.message : "fetch failed";
76
- throw new NeetruError("network_error", `Network error: ${message}`);
77
95
  }
78
- const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
79
- if (!res.ok) {
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;
117
+ }
80
118
  const body = await safeJson(res);
81
119
  let code = statusToCode(res.status);
82
120
  let message = `HTTP ${res.status}`;
@@ -89,10 +127,18 @@ async function httpRequest(config, opts) {
89
127
  if (typeof errField.message === "string") message = errField.message;
90
128
  }
91
129
  }
92
- 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;
93
140
  }
94
- const parsed = await safeJson(res);
95
- return parsed;
141
+ throw lastError ?? new NeetruError("unknown", "unexpected httpRequest exit");
96
142
  }
97
143
 
98
144
  // src/catalog.ts
@@ -132,12 +178,10 @@ function createCatalogNamespace(config) {
132
178
  * Lista produtos publicados. Por default só `published=true`; staff
133
179
  * pode passar `includeDrafts: true` (requer Bearer com role admin/operator).
134
180
  */
135
- async list(opts = {}) {
181
+ async list(_opts = {}) {
136
182
  const raw = await httpRequest(config, {
137
183
  method: "GET",
138
- path: "/api/v1/cli/catalog",
139
- query: opts.includeDrafts ? { drafts: "true" } : void 0,
140
- requireAuth: true
184
+ path: "/api/sdk/v1/catalog"
141
185
  });
142
186
  if (!raw || !Array.isArray(raw.products)) {
143
187
  throw new NeetruError(
@@ -161,8 +205,7 @@ function createCatalogNamespace(config) {
161
205
  }
162
206
  const raw = await httpRequest(config, {
163
207
  method: "GET",
164
- path: `/api/v1/cli/catalog/${encodeURIComponent(slug)}`,
165
- requireAuth: true
208
+ path: `/api/sdk/v1/catalog/${encodeURIComponent(slug)}`
166
209
  });
167
210
  if (!raw || !raw.product) {
168
211
  throw new NeetruError(
@@ -191,30 +234,65 @@ function toEntitlementCheck(raw) {
191
234
  reason: typeof r.reason === "string" ? r.reason : void 0
192
235
  };
193
236
  }
237
+ var CACHE_TTL_MS = 6e4;
238
+ var CACHE_MAX = 100;
194
239
  function createEntitlementsNamespace(config) {
195
- 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 = {}) {
196
263
  if (!productSlug) throw new NeetruError("validation_failed", "productSlug is required");
197
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
+ }
198
270
  const raw = await httpRequest(config, {
199
271
  method: "GET",
200
272
  path: "/api/v1/sdk/entitlements/check",
201
273
  query: { slug: productSlug, feature },
202
274
  requireAuth: true
203
275
  });
204
- return toEntitlementCheck(raw);
276
+ const result = toEntitlementCheck(raw);
277
+ writeCache(key, result);
278
+ return result;
205
279
  }
206
280
  return {
207
281
  /**
208
282
  * Verifica se o caller pode usar `feature` no produto `productSlug`.
209
- * Retorno simples: `true` libera, `false` bloqueia.
283
+ * Retorno simples: `true` libera, `false` bloqueia. Cache 60s automático.
210
284
  *
211
285
  * Use `checkDetailed` se precisar do `reason` pra mensagem de upgrade.
212
286
  */
213
- async check(productSlug, feature) {
214
- const result = await checkDetailed(productSlug, feature);
287
+ async check(productSlug, feature, opts) {
288
+ const result = await checkDetailed(productSlug, feature, opts);
215
289
  return result.allowed;
216
290
  },
217
- checkDetailed
291
+ checkDetailed,
292
+ /** Test helper: limpa o cache LRU. */
293
+ __resetCache() {
294
+ cache.clear();
295
+ }
218
296
  };
219
297
  }
220
298
 
@@ -236,7 +314,44 @@ function consoleFor(level) {
236
314
  return console.log.bind(console);
237
315
  }
238
316
  }
317
+ var TRACK_FLUSH_MS = 500;
318
+ var TRACK_MAX_QUEUE = 50;
239
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
+ }
240
355
  return {
241
356
  /**
242
357
  * Persiste um evento de uso. Lança `NeetruError` em qualquer falha
@@ -276,6 +391,38 @@ function createTelemetryNamespace(config) {
276
391
  }
277
392
  return { ok: true, eventId: raw.eventId };
278
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
+ },
279
426
  /**
280
427
  * Registra um log estruturado per-product (Sprint 6).
281
428
  *
@@ -327,7 +474,7 @@ function createTelemetryNamespace(config) {
327
474
  if (cid) headers["x-correlation-id"] = cid;
328
475
  const raw = await httpRequest(config, {
329
476
  method: "POST",
330
- path: "/sdk/v1/telemetry/log",
477
+ path: "/api/sdk/v1/telemetry/log",
331
478
  body,
332
479
  requireAuth: true,
333
480
  headers
@@ -588,8 +735,7 @@ function createDbNamespace(config) {
588
735
  if (config.tenantId) headers["x-neetru-tenant"] = config.tenantId;
589
736
  return {
590
737
  async list(opts) {
591
- if (opts?.limit !== void 0) opts.limit;
592
- let path = `/sdk/v1/datastore/${name}`;
738
+ let path = `/api/sdk/v1/datastore/${name}`;
593
739
  const params = new URLSearchParams();
594
740
  if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
595
741
  if (opts?.where && opts.where.length > 0) {
@@ -621,7 +767,7 @@ function createDbNamespace(config) {
621
767
  try {
622
768
  const raw = await httpRequest(config, {
623
769
  method: "GET",
624
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
770
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
625
771
  requireAuth: true,
626
772
  headers
627
773
  });
@@ -637,7 +783,7 @@ function createDbNamespace(config) {
637
783
  }
638
784
  const raw = await httpRequest(config, {
639
785
  method: "POST",
640
- path: `/sdk/v1/datastore/${name}`,
786
+ path: `/api/sdk/v1/datastore/${name}`,
641
787
  body: { data },
642
788
  requireAuth: true,
643
789
  headers
@@ -653,7 +799,7 @@ function createDbNamespace(config) {
653
799
  }
654
800
  await httpRequest(config, {
655
801
  method: "PUT",
656
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
802
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
657
803
  body: { data },
658
804
  requireAuth: true,
659
805
  headers
@@ -666,7 +812,7 @@ function createDbNamespace(config) {
666
812
  }
667
813
  await httpRequest(config, {
668
814
  method: "PATCH",
669
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
815
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
670
816
  body: { data },
671
817
  requireAuth: true,
672
818
  headers
@@ -679,7 +825,7 @@ function createDbNamespace(config) {
679
825
  }
680
826
  await httpRequest(config, {
681
827
  method: "DELETE",
682
- path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
828
+ path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
683
829
  requireAuth: true,
684
830
  headers
685
831
  });
@@ -865,6 +1011,297 @@ function createCheckoutNamespace(config) {
865
1011
  return createHttpCheckoutNamespace(config);
866
1012
  }
867
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
+
868
1305
  // src/mocks.ts
869
1306
  var DEV_FIXTURE_USER = Object.freeze({
870
1307
  uid: "dev-fixture-uid-0001",
@@ -1000,14 +1437,14 @@ var MockUsage = class {
1000
1437
  this._counters.clear();
1001
1438
  }
1002
1439
  };
1003
- var mockTicketSeq = 0;
1004
1440
  var MockSupport = class {
1005
1441
  _tickets = [];
1442
+ _ticketSeq = 0;
1006
1443
  async createTicket(input) {
1007
1444
  if (!input?.subject) throw new Error("subject required");
1008
1445
  if (!input?.message) throw new Error("message required");
1009
1446
  const ticket = {
1010
- id: `mock-ticket-${++mockTicketSeq}`,
1447
+ id: `mock-ticket-${++this._ticketSeq}`,
1011
1448
  subject: input.subject,
1012
1449
  message: input.message,
1013
1450
  severity: input.severity ?? "normal",
@@ -1024,6 +1461,7 @@ var MockSupport = class {
1024
1461
  /** Test helper. */
1025
1462
  __reset() {
1026
1463
  this._tickets = [];
1464
+ this._ticketSeq = 0;
1027
1465
  }
1028
1466
  };
1029
1467
  var MockEntitlements = class {
@@ -1222,7 +1660,9 @@ function createOidcAuthNamespace(config) {
1222
1660
  const redirectUri = options?.redirectUri ?? globalThis.location.origin;
1223
1661
  const scope = options?.scope ?? "openid profile email";
1224
1662
  const state = options?.postLoginRedirect ?? globalThis.location.href;
1225
- const url = new URL("/oauth/authorize", config.baseUrl.replace(/\/api$/, ""));
1663
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
1664
+ const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
1665
+ const url = new URL("/api/v1/oauth/authorize", idpOrigin);
1226
1666
  url.searchParams.set("response_type", "code");
1227
1667
  url.searchParams.set("redirect_uri", redirectUri);
1228
1668
  url.searchParams.set("scope", scope);
@@ -1244,7 +1684,9 @@ function createOidcAuthNamespace(config) {
1244
1684
  if (storage) storage.removeItem(STORAGE_KEY);
1245
1685
  cachedUser = null;
1246
1686
  try {
1247
- 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`, {
1248
1690
  method: "POST",
1249
1691
  headers: { "content-type": "application/json" }
1250
1692
  });
@@ -1292,6 +1734,8 @@ function createNeetruClient(config = {}) {
1292
1734
  const support = config.mocks?.support ?? (isDev ? new MockSupport() : createSupportNamespace(resolved));
1293
1735
  const entitlements = config.mocks?.entitlements ?? (isDev ? new MockEntitlements() : createEntitlementsNamespace(resolved));
1294
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));
1295
1739
  const client = Object.freeze({
1296
1740
  config: resolved,
1297
1741
  auth,
@@ -1301,7 +1745,9 @@ function createNeetruClient(config = {}) {
1301
1745
  usage,
1302
1746
  support,
1303
1747
  db,
1304
- checkout: createCheckoutNamespace(resolved)
1748
+ checkout: createCheckoutNamespace(resolved),
1749
+ webhooks,
1750
+ notifications
1305
1751
  });
1306
1752
  return client;
1307
1753
  }