@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.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
|
|
67
|
-
if (
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
181
|
+
async list(_opts = {}) {
|
|
136
182
|
const raw = await httpRequest(config, {
|
|
137
183
|
method: "GET",
|
|
138
|
-
path: "/api/v1/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-${++
|
|
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
|
|
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
|
-
|
|
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,18 +1745,20 @@ 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
|
}
|
|
1308
1754
|
|
|
1309
1755
|
// src/index.ts
|
|
1310
|
-
var VERSION = "1.
|
|
1756
|
+
var VERSION = "1.2.0";
|
|
1311
1757
|
function initNeetru(config) {
|
|
1312
1758
|
const { apiUrl, baseUrl, ...rest } = config;
|
|
1313
1759
|
return createNeetruClient({ ...rest, baseUrl: baseUrl ?? apiUrl });
|
|
1314
1760
|
}
|
|
1315
1761
|
|
|
1316
|
-
export { DEFAULT_BASE_URL, DEV_FIXTURE_USER, MockAuth, MockCheckout, MockDb, MockEntitlements, MockSupport, MockUsage, NeetruError, VERSION, createCheckoutNamespace, createNeetruClient, initNeetru };
|
|
1762
|
+
export { DEFAULT_BASE_URL, DEV_FIXTURE_USER, MockAuth, MockCheckout, MockDb, MockEntitlements, MockNotifications, MockSupport, MockUsage, MockWebhooks, NeetruError, VERSION, createCheckoutNamespace, createNeetruClient, createNotificationsNamespace, createWebhooksNamespace, initNeetru };
|
|
1317
1763
|
//# sourceMappingURL=index.mjs.map
|
|
1318
1764
|
//# sourceMappingURL=index.mjs.map
|