@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/auth.mjs
CHANGED
|
@@ -17,6 +17,31 @@ var NeetruError = class _NeetruError extends Error {
|
|
|
17
17
|
var DEFAULT_BASE_URL = "https://api.neetru.com";
|
|
18
18
|
|
|
19
19
|
// src/http.ts
|
|
20
|
+
var DEFAULT_RETRIES = 2;
|
|
21
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
22
|
+
"rate_limited",
|
|
23
|
+
"server_error",
|
|
24
|
+
"network_error"
|
|
25
|
+
]);
|
|
26
|
+
function backoffMs(attempt) {
|
|
27
|
+
const base = 200 * Math.pow(4, attempt);
|
|
28
|
+
const jitter = base * 0.2 * (Math.random() * 2 - 1);
|
|
29
|
+
return Math.max(50, Math.round(base + jitter));
|
|
30
|
+
}
|
|
31
|
+
function parseRetryAfter(value) {
|
|
32
|
+
if (!value) return null;
|
|
33
|
+
const secs = Number(value);
|
|
34
|
+
if (Number.isFinite(secs) && secs >= 0) return Math.round(secs * 1e3);
|
|
35
|
+
const dateMs = Date.parse(value);
|
|
36
|
+
if (Number.isFinite(dateMs)) {
|
|
37
|
+
const delta = dateMs - Date.now();
|
|
38
|
+
if (delta > 0) return delta;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
20
45
|
function statusToCode(status) {
|
|
21
46
|
if (status === 401) return "unauthorized";
|
|
22
47
|
if (status === 403) return "forbidden";
|
|
@@ -50,6 +75,7 @@ async function safeJson(res) {
|
|
|
50
75
|
async function httpRequest(config, opts) {
|
|
51
76
|
const method = opts.method ?? "GET";
|
|
52
77
|
const url = buildUrl(config.baseUrl, opts.path, opts.query);
|
|
78
|
+
const maxRetries = opts.retries ?? DEFAULT_RETRIES;
|
|
53
79
|
const headers = {
|
|
54
80
|
accept: "application/json",
|
|
55
81
|
...opts.headers
|
|
@@ -63,24 +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
95
|
}
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
78
117
|
}
|
|
79
|
-
const message = err instanceof Error ? err.message : "fetch failed";
|
|
80
|
-
throw new NeetruError("network_error", `Network error: ${message}`);
|
|
81
|
-
}
|
|
82
|
-
const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
|
|
83
|
-
if (!res.ok) {
|
|
84
118
|
const body = await safeJson(res);
|
|
85
119
|
let code = statusToCode(res.status);
|
|
86
120
|
let message = `HTTP ${res.status}`;
|
|
@@ -93,10 +127,18 @@ async function httpRequest(config, opts) {
|
|
|
93
127
|
if (typeof errField.message === "string") message = errField.message;
|
|
94
128
|
}
|
|
95
129
|
}
|
|
96
|
-
|
|
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;
|
|
97
140
|
}
|
|
98
|
-
|
|
99
|
-
return parsed;
|
|
141
|
+
throw lastError ?? new NeetruError("unknown", "unexpected httpRequest exit");
|
|
100
142
|
}
|
|
101
143
|
|
|
102
144
|
// src/catalog.ts
|
|
@@ -136,12 +178,10 @@ function createCatalogNamespace(config) {
|
|
|
136
178
|
* Lista produtos publicados. Por default só `published=true`; staff
|
|
137
179
|
* pode passar `includeDrafts: true` (requer Bearer com role admin/operator).
|
|
138
180
|
*/
|
|
139
|
-
async list(
|
|
181
|
+
async list(_opts = {}) {
|
|
140
182
|
const raw = await httpRequest(config, {
|
|
141
183
|
method: "GET",
|
|
142
|
-
path: "/api/v1/
|
|
143
|
-
query: opts.includeDrafts ? { drafts: "true" } : void 0,
|
|
144
|
-
requireAuth: true
|
|
184
|
+
path: "/api/sdk/v1/catalog"
|
|
145
185
|
});
|
|
146
186
|
if (!raw || !Array.isArray(raw.products)) {
|
|
147
187
|
throw new NeetruError(
|
|
@@ -165,8 +205,7 @@ function createCatalogNamespace(config) {
|
|
|
165
205
|
}
|
|
166
206
|
const raw = await httpRequest(config, {
|
|
167
207
|
method: "GET",
|
|
168
|
-
path: `/api/v1/
|
|
169
|
-
requireAuth: true
|
|
208
|
+
path: `/api/sdk/v1/catalog/${encodeURIComponent(slug)}`
|
|
170
209
|
});
|
|
171
210
|
if (!raw || !raw.product) {
|
|
172
211
|
throw new NeetruError(
|
|
@@ -195,30 +234,65 @@ function toEntitlementCheck(raw) {
|
|
|
195
234
|
reason: typeof r.reason === "string" ? r.reason : void 0
|
|
196
235
|
};
|
|
197
236
|
}
|
|
237
|
+
var CACHE_TTL_MS = 6e4;
|
|
238
|
+
var CACHE_MAX = 100;
|
|
198
239
|
function createEntitlementsNamespace(config) {
|
|
199
|
-
|
|
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 = {}) {
|
|
200
263
|
if (!productSlug) throw new NeetruError("validation_failed", "productSlug is required");
|
|
201
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
|
+
}
|
|
202
270
|
const raw = await httpRequest(config, {
|
|
203
271
|
method: "GET",
|
|
204
272
|
path: "/api/v1/sdk/entitlements/check",
|
|
205
273
|
query: { slug: productSlug, feature },
|
|
206
274
|
requireAuth: true
|
|
207
275
|
});
|
|
208
|
-
|
|
276
|
+
const result = toEntitlementCheck(raw);
|
|
277
|
+
writeCache(key, result);
|
|
278
|
+
return result;
|
|
209
279
|
}
|
|
210
280
|
return {
|
|
211
281
|
/**
|
|
212
282
|
* Verifica se o caller pode usar `feature` no produto `productSlug`.
|
|
213
|
-
* Retorno simples: `true` libera, `false` bloqueia.
|
|
283
|
+
* Retorno simples: `true` libera, `false` bloqueia. Cache 60s automático.
|
|
214
284
|
*
|
|
215
285
|
* Use `checkDetailed` se precisar do `reason` pra mensagem de upgrade.
|
|
216
286
|
*/
|
|
217
|
-
async check(productSlug, feature) {
|
|
218
|
-
const result = await checkDetailed(productSlug, feature);
|
|
287
|
+
async check(productSlug, feature, opts) {
|
|
288
|
+
const result = await checkDetailed(productSlug, feature, opts);
|
|
219
289
|
return result.allowed;
|
|
220
290
|
},
|
|
221
|
-
checkDetailed
|
|
291
|
+
checkDetailed,
|
|
292
|
+
/** Test helper: limpa o cache LRU. */
|
|
293
|
+
__resetCache() {
|
|
294
|
+
cache.clear();
|
|
295
|
+
}
|
|
222
296
|
};
|
|
223
297
|
}
|
|
224
298
|
|
|
@@ -240,7 +314,44 @@ function consoleFor(level) {
|
|
|
240
314
|
return console.log.bind(console);
|
|
241
315
|
}
|
|
242
316
|
}
|
|
317
|
+
var TRACK_FLUSH_MS = 500;
|
|
318
|
+
var TRACK_MAX_QUEUE = 50;
|
|
243
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
|
+
}
|
|
244
355
|
return {
|
|
245
356
|
/**
|
|
246
357
|
* Persiste um evento de uso. Lança `NeetruError` em qualquer falha
|
|
@@ -280,6 +391,38 @@ function createTelemetryNamespace(config) {
|
|
|
280
391
|
}
|
|
281
392
|
return { ok: true, eventId: raw.eventId };
|
|
282
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
|
+
},
|
|
283
426
|
/**
|
|
284
427
|
* Registra um log estruturado per-product (Sprint 6).
|
|
285
428
|
*
|
|
@@ -331,7 +474,7 @@ function createTelemetryNamespace(config) {
|
|
|
331
474
|
if (cid) headers["x-correlation-id"] = cid;
|
|
332
475
|
const raw = await httpRequest(config, {
|
|
333
476
|
method: "POST",
|
|
334
|
-
path: "/sdk/v1/telemetry/log",
|
|
477
|
+
path: "/api/sdk/v1/telemetry/log",
|
|
335
478
|
body,
|
|
336
479
|
requireAuth: true,
|
|
337
480
|
headers
|
|
@@ -592,8 +735,7 @@ function createDbNamespace(config) {
|
|
|
592
735
|
if (config.tenantId) headers["x-neetru-tenant"] = config.tenantId;
|
|
593
736
|
return {
|
|
594
737
|
async list(opts) {
|
|
595
|
-
|
|
596
|
-
let path = `/sdk/v1/datastore/${name}`;
|
|
738
|
+
let path = `/api/sdk/v1/datastore/${name}`;
|
|
597
739
|
const params = new URLSearchParams();
|
|
598
740
|
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
599
741
|
if (opts?.where && opts.where.length > 0) {
|
|
@@ -625,7 +767,7 @@ function createDbNamespace(config) {
|
|
|
625
767
|
try {
|
|
626
768
|
const raw = await httpRequest(config, {
|
|
627
769
|
method: "GET",
|
|
628
|
-
path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
770
|
+
path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
629
771
|
requireAuth: true,
|
|
630
772
|
headers
|
|
631
773
|
});
|
|
@@ -641,7 +783,7 @@ function createDbNamespace(config) {
|
|
|
641
783
|
}
|
|
642
784
|
const raw = await httpRequest(config, {
|
|
643
785
|
method: "POST",
|
|
644
|
-
path: `/sdk/v1/datastore/${name}`,
|
|
786
|
+
path: `/api/sdk/v1/datastore/${name}`,
|
|
645
787
|
body: { data },
|
|
646
788
|
requireAuth: true,
|
|
647
789
|
headers
|
|
@@ -657,7 +799,7 @@ function createDbNamespace(config) {
|
|
|
657
799
|
}
|
|
658
800
|
await httpRequest(config, {
|
|
659
801
|
method: "PUT",
|
|
660
|
-
path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
802
|
+
path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
661
803
|
body: { data },
|
|
662
804
|
requireAuth: true,
|
|
663
805
|
headers
|
|
@@ -670,7 +812,7 @@ function createDbNamespace(config) {
|
|
|
670
812
|
}
|
|
671
813
|
await httpRequest(config, {
|
|
672
814
|
method: "PATCH",
|
|
673
|
-
path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
815
|
+
path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
674
816
|
body: { data },
|
|
675
817
|
requireAuth: true,
|
|
676
818
|
headers
|
|
@@ -683,7 +825,7 @@ function createDbNamespace(config) {
|
|
|
683
825
|
}
|
|
684
826
|
await httpRequest(config, {
|
|
685
827
|
method: "DELETE",
|
|
686
|
-
path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
828
|
+
path: `/api/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
|
|
687
829
|
requireAuth: true,
|
|
688
830
|
headers
|
|
689
831
|
});
|
|
@@ -869,6 +1011,297 @@ function createCheckoutNamespace(config) {
|
|
|
869
1011
|
return createHttpCheckoutNamespace(config);
|
|
870
1012
|
}
|
|
871
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
|
+
|
|
872
1305
|
// src/mocks.ts
|
|
873
1306
|
var DEV_FIXTURE_USER = Object.freeze({
|
|
874
1307
|
uid: "dev-fixture-uid-0001",
|
|
@@ -1004,14 +1437,14 @@ var MockUsage = class {
|
|
|
1004
1437
|
this._counters.clear();
|
|
1005
1438
|
}
|
|
1006
1439
|
};
|
|
1007
|
-
var mockTicketSeq = 0;
|
|
1008
1440
|
var MockSupport = class {
|
|
1009
1441
|
_tickets = [];
|
|
1442
|
+
_ticketSeq = 0;
|
|
1010
1443
|
async createTicket(input) {
|
|
1011
1444
|
if (!input?.subject) throw new Error("subject required");
|
|
1012
1445
|
if (!input?.message) throw new Error("message required");
|
|
1013
1446
|
const ticket = {
|
|
1014
|
-
id: `mock-ticket-${++
|
|
1447
|
+
id: `mock-ticket-${++this._ticketSeq}`,
|
|
1015
1448
|
subject: input.subject,
|
|
1016
1449
|
message: input.message,
|
|
1017
1450
|
severity: input.severity ?? "normal",
|
|
@@ -1028,6 +1461,7 @@ var MockSupport = class {
|
|
|
1028
1461
|
/** Test helper. */
|
|
1029
1462
|
__reset() {
|
|
1030
1463
|
this._tickets = [];
|
|
1464
|
+
this._ticketSeq = 0;
|
|
1031
1465
|
}
|
|
1032
1466
|
};
|
|
1033
1467
|
var MockEntitlements = class {
|
|
@@ -1250,7 +1684,9 @@ function createOidcAuthNamespace(config) {
|
|
|
1250
1684
|
if (storage) storage.removeItem(STORAGE_KEY);
|
|
1251
1685
|
cachedUser = null;
|
|
1252
1686
|
try {
|
|
1253
|
-
|
|
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`, {
|
|
1254
1690
|
method: "POST",
|
|
1255
1691
|
headers: { "content-type": "application/json" }
|
|
1256
1692
|
});
|
|
@@ -1298,6 +1734,8 @@ function createNeetruClient(config = {}) {
|
|
|
1298
1734
|
const support = config.mocks?.support ?? (isDev ? new MockSupport() : createSupportNamespace(resolved));
|
|
1299
1735
|
const entitlements = config.mocks?.entitlements ?? (isDev ? new MockEntitlements() : createEntitlementsNamespace(resolved));
|
|
1300
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));
|
|
1301
1739
|
const client = Object.freeze({
|
|
1302
1740
|
config: resolved,
|
|
1303
1741
|
auth,
|
|
@@ -1307,7 +1745,9 @@ function createNeetruClient(config = {}) {
|
|
|
1307
1745
|
usage,
|
|
1308
1746
|
support,
|
|
1309
1747
|
db,
|
|
1310
|
-
checkout: createCheckoutNamespace(resolved)
|
|
1748
|
+
checkout: createCheckoutNamespace(resolved),
|
|
1749
|
+
webhooks,
|
|
1750
|
+
notifications
|
|
1311
1751
|
});
|
|
1312
1752
|
return client;
|
|
1313
1753
|
}
|