@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.
- package/CHANGELOG.md +33 -3
- package/README.md +135 -159
- package/dist/auth.cjs +482 -42
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.mjs +482 -42
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.cjs +63 -24
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +2 -2
- package/dist/catalog.d.ts +2 -2
- package/dist/catalog.mjs +63 -24
- package/dist/catalog.mjs.map +1 -1
- package/dist/checkout.cjs +60 -18
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/checkout.mjs +60 -18
- package/dist/checkout.mjs.map +1 -1
- package/dist/db.cjs +66 -25
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.mjs +66 -25
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.cjs +101 -24
- package/dist/entitlements.cjs.map +1 -1
- package/dist/entitlements.d.cts +11 -5
- package/dist/entitlements.d.ts +11 -5
- package/dist/entitlements.mjs +101 -24
- package/dist/entitlements.mjs.map +1 -1
- package/dist/index.cjs +487 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +484 -44
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +3 -2
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +4 -2
- package/dist/mocks.d.ts +4 -2
- package/dist/mocks.mjs +3 -2
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.cjs +296 -0
- package/dist/notifications.cjs.map +1 -0
- package/dist/notifications.d.cts +1 -0
- package/dist/notifications.d.ts +1 -0
- package/dist/notifications.mjs +293 -0
- package/dist/notifications.mjs.map +1 -0
- package/dist/react.cjs +7 -3
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs +7 -3
- package/dist/react.mjs.map +1 -1
- package/dist/support.cjs +60 -18
- package/dist/support.cjs.map +1 -1
- package/dist/support.d.cts +1 -1
- package/dist/support.d.ts +1 -1
- package/dist/support.mjs +60 -18
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.cjs +130 -19
- package/dist/telemetry.cjs.map +1 -1
- package/dist/telemetry.d.cts +17 -1
- package/dist/telemetry.d.ts +17 -1
- package/dist/telemetry.mjs +130 -19
- package/dist/telemetry.mjs.map +1 -1
- package/dist/{types-BA53dd8S.d.cts → types-CQAfwqUS.d.cts} +172 -8
- package/dist/{types-BA53dd8S.d.ts → types-CQAfwqUS.d.ts} +172 -8
- package/dist/usage.cjs +60 -18
- package/dist/usage.cjs.map +1 -1
- package/dist/usage.d.cts +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/usage.mjs +60 -18
- package/dist/usage.mjs.map +1 -1
- package/dist/webhooks.cjs +316 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +1 -0
- package/dist/webhooks.d.ts +1 -0
- package/dist/webhooks.mjs +312 -0
- package/dist/webhooks.mjs.map +1 -0
- 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
|
|
69
|
-
if (
|
|
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
|
-
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
183
|
+
async list(_opts = {}) {
|
|
142
184
|
const raw = await httpRequest(config, {
|
|
143
185
|
method: "GET",
|
|
144
|
-
path: "/api/v1/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-${++
|
|
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
|
-
|
|
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.
|
|
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
|