@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.
- package/CHANGELOG.md +33 -3
- package/README.md +135 -159
- package/dist/auth.cjs +486 -40
- 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 +486 -40
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.cjs +64 -21
- 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 +64 -21
- package/dist/catalog.mjs.map +1 -1
- package/dist/checkout.cjs +61 -15
- 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 +61 -15
- package/dist/checkout.mjs.map +1 -1
- package/dist/db.cjs +67 -22
- 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 +67 -22
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.cjs +102 -21
- 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 +102 -21
- package/dist/entitlements.mjs.map +1 -1
- package/dist/index.cjs +491 -41
- 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 +488 -42
- 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 +61 -15
- 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 +61 -15
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.cjs +131 -16
- 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 +131 -16
- 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 +61 -15
- 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 +61 -15
- 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 +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
|
|
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
|
-
}
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
183
|
+
async list(_opts = {}) {
|
|
138
184
|
const raw = await httpRequest(config, {
|
|
139
185
|
method: "GET",
|
|
140
|
-
path: "/api/v1/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-${++
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|