@neetru/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +218 -0
  3. package/dist/auth.cjs +1137 -0
  4. package/dist/auth.cjs.map +1 -0
  5. package/dist/auth.d.cts +25 -0
  6. package/dist/auth.d.ts +25 -0
  7. package/dist/auth.mjs +1135 -0
  8. package/dist/auth.mjs.map +1 -0
  9. package/dist/catalog.cjs +179 -0
  10. package/dist/catalog.cjs.map +1 -0
  11. package/dist/catalog.d.cts +17 -0
  12. package/dist/catalog.d.ts +17 -0
  13. package/dist/catalog.mjs +177 -0
  14. package/dist/catalog.mjs.map +1 -0
  15. package/dist/db.cjs +232 -0
  16. package/dist/db.cjs.map +1 -0
  17. package/dist/db.d.cts +8 -0
  18. package/dist/db.d.ts +8 -0
  19. package/dist/db.mjs +230 -0
  20. package/dist/db.mjs.map +1 -0
  21. package/dist/entitlements.cjs +140 -0
  22. package/dist/entitlements.cjs.map +1 -0
  23. package/dist/entitlements.d.cts +14 -0
  24. package/dist/entitlements.d.ts +14 -0
  25. package/dist/entitlements.mjs +138 -0
  26. package/dist/entitlements.mjs.map +1 -0
  27. package/dist/errors.cjs +20 -0
  28. package/dist/errors.cjs.map +1 -0
  29. package/dist/errors.d.cts +27 -0
  30. package/dist/errors.d.ts +27 -0
  31. package/dist/errors.mjs +18 -0
  32. package/dist/errors.mjs.map +1 -0
  33. package/dist/index.cjs +1154 -0
  34. package/dist/index.cjs.map +1 -0
  35. package/dist/index.d.cts +24 -0
  36. package/dist/index.d.ts +24 -0
  37. package/dist/index.mjs +1142 -0
  38. package/dist/index.mjs.map +1 -0
  39. package/dist/mocks.cjs +281 -0
  40. package/dist/mocks.cjs.map +1 -0
  41. package/dist/mocks.d.cts +138 -0
  42. package/dist/mocks.d.ts +138 -0
  43. package/dist/mocks.mjs +274 -0
  44. package/dist/mocks.mjs.map +1 -0
  45. package/dist/support.cjs +176 -0
  46. package/dist/support.cjs.map +1 -0
  47. package/dist/support.d.cts +5 -0
  48. package/dist/support.d.ts +5 -0
  49. package/dist/support.mjs +174 -0
  50. package/dist/support.mjs.map +1 -0
  51. package/dist/telemetry.cjs +225 -0
  52. package/dist/telemetry.cjs.map +1 -0
  53. package/dist/telemetry.d.cts +35 -0
  54. package/dist/telemetry.d.ts +35 -0
  55. package/dist/telemetry.mjs +223 -0
  56. package/dist/telemetry.mjs.map +1 -0
  57. package/dist/types-PKUaFtBY.d.cts +408 -0
  58. package/dist/types-PKUaFtBY.d.ts +408 -0
  59. package/dist/usage.cjs +235 -0
  60. package/dist/usage.cjs.map +1 -0
  61. package/dist/usage.d.cts +5 -0
  62. package/dist/usage.d.ts +5 -0
  63. package/dist/usage.mjs +233 -0
  64. package/dist/usage.mjs.map +1 -0
  65. package/package.json +79 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1142 @@
1
+ // src/errors.ts
2
+ var NeetruError = class _NeetruError extends Error {
3
+ code;
4
+ status;
5
+ requestId;
6
+ constructor(code, message, status, requestId) {
7
+ super(message);
8
+ this.name = "NeetruError";
9
+ this.code = code;
10
+ this.status = status;
11
+ this.requestId = requestId;
12
+ Object.setPrototypeOf(this, _NeetruError.prototype);
13
+ }
14
+ };
15
+
16
+ // src/types.ts
17
+ var DEFAULT_BASE_URL = "https://api.neetru.com";
18
+
19
+ // src/http.ts
20
+ function statusToCode(status) {
21
+ if (status === 401) return "unauthorized";
22
+ if (status === 403) return "forbidden";
23
+ if (status === 404) return "not_found";
24
+ if (status === 422 || status === 400) return "validation_failed";
25
+ if (status === 429) return "rate_limited";
26
+ if (status >= 500) return "server_error";
27
+ return "unknown";
28
+ }
29
+ function buildUrl(baseUrl, path, query) {
30
+ const base = baseUrl.replace(/\/+$/, "");
31
+ const p = path.startsWith("/") ? path : `/${path}`;
32
+ const url = new URL(`${base}${p}`);
33
+ if (query) {
34
+ for (const [k, v] of Object.entries(query)) {
35
+ if (v === void 0) continue;
36
+ url.searchParams.set(k, String(v));
37
+ }
38
+ }
39
+ return url.toString();
40
+ }
41
+ async function safeJson(res) {
42
+ const text = await res.text();
43
+ if (!text) return void 0;
44
+ try {
45
+ return JSON.parse(text);
46
+ } catch {
47
+ return void 0;
48
+ }
49
+ }
50
+ async function httpRequest(config, opts) {
51
+ const method = opts.method ?? "GET";
52
+ const url = buildUrl(config.baseUrl, opts.path, opts.query);
53
+ const headers = {
54
+ accept: "application/json",
55
+ ...opts.headers
56
+ };
57
+ if (opts.requireAuth) {
58
+ if (!config.apiKey) {
59
+ throw new NeetruError(
60
+ "missing_api_key",
61
+ "This operation requires an apiKey. Pass it to createNeetruClient({ apiKey }) or set NEETRU_API_KEY env var."
62
+ );
63
+ }
64
+ headers.authorization = `Bearer ${config.apiKey}`;
65
+ }
66
+ const init = { method, headers };
67
+ if (opts.body !== void 0 && method !== "GET" && method !== "DELETE") {
68
+ 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
+ }
78
+ const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
79
+ if (!res.ok) {
80
+ const body = await safeJson(res);
81
+ let code = statusToCode(res.status);
82
+ let message = `HTTP ${res.status}`;
83
+ if (body && typeof body === "object" && "error" in body) {
84
+ const errField = body.error;
85
+ if (typeof errField === "string") {
86
+ message = errField;
87
+ } else if (errField && typeof errField === "object") {
88
+ if (typeof errField.code === "string") code = errField.code;
89
+ if (typeof errField.message === "string") message = errField.message;
90
+ }
91
+ }
92
+ throw new NeetruError(code, message, res.status, requestId);
93
+ }
94
+ const parsed = await safeJson(res);
95
+ return parsed;
96
+ }
97
+
98
+ // src/catalog.ts
99
+ function toProduct(raw) {
100
+ if (!raw || typeof raw !== "object") {
101
+ throw new NeetruError("invalid_response", "Catalog response item is not an object");
102
+ }
103
+ const r = raw;
104
+ if (typeof r.slug !== "string" || !r.slug) {
105
+ throw new NeetruError("invalid_response", "Catalog product missing slug");
106
+ }
107
+ if (typeof r.name !== "string" || !r.name) {
108
+ throw new NeetruError("invalid_response", "Catalog product missing name");
109
+ }
110
+ const product = {
111
+ slug: r.slug,
112
+ name: r.name
113
+ };
114
+ if (typeof r.tagline === "string") product.tagline = r.tagline;
115
+ if (typeof r.description === "string") product.description = r.description;
116
+ if (r.status === "live" || r.status === "soon" || r.status === "beta") {
117
+ product.status = r.status;
118
+ }
119
+ if (typeof r.iconKey === "string") product.iconKey = r.iconKey;
120
+ if (typeof r.ctaHref === "string") product.ctaHref = r.ctaHref;
121
+ if (typeof r.ctaLabel === "string") product.ctaLabel = r.ctaLabel;
122
+ if (Array.isArray(r.plans)) {
123
+ product.plans = r.plans.filter(
124
+ (p) => typeof p === "object" && p !== null && typeof p.id === "string" && typeof p.name === "string"
125
+ );
126
+ }
127
+ return product;
128
+ }
129
+ function createCatalogNamespace(config) {
130
+ return {
131
+ /**
132
+ * Lista produtos publicados. Por default só `published=true`; staff
133
+ * pode passar `includeDrafts: true` (requer Bearer com role admin/operator).
134
+ */
135
+ async list(opts = {}) {
136
+ const raw = await httpRequest(config, {
137
+ method: "GET",
138
+ path: "/api/v1/cli/catalog",
139
+ query: opts.includeDrafts ? { drafts: "true" } : void 0,
140
+ requireAuth: true
141
+ });
142
+ if (!raw || !Array.isArray(raw.products)) {
143
+ throw new NeetruError(
144
+ "invalid_response",
145
+ "Catalog list response missing products array"
146
+ );
147
+ }
148
+ return {
149
+ products: raw.products.map(toProduct),
150
+ fetchedAt: typeof raw.fetchedAt === "string" ? raw.fetchedAt : (/* @__PURE__ */ new Date()).toISOString()
151
+ };
152
+ },
153
+ /**
154
+ * Busca produto único por slug.
155
+ *
156
+ * @throws {NeetruError} `not_found` se slug inexistente ou não publicado.
157
+ */
158
+ async get(slug) {
159
+ if (!slug || typeof slug !== "string") {
160
+ throw new NeetruError("validation_failed", "slug is required");
161
+ }
162
+ const raw = await httpRequest(config, {
163
+ method: "GET",
164
+ path: `/api/v1/cli/catalog/${encodeURIComponent(slug)}`,
165
+ requireAuth: true
166
+ });
167
+ if (!raw || !raw.product) {
168
+ throw new NeetruError(
169
+ "invalid_response",
170
+ "Catalog get response missing product"
171
+ );
172
+ }
173
+ return toProduct(raw.product);
174
+ }
175
+ };
176
+ }
177
+
178
+ // src/entitlements.ts
179
+ function toEntitlementCheck(raw) {
180
+ if (!raw || typeof raw !== "object") {
181
+ throw new NeetruError("invalid_response", "Entitlement response is not an object");
182
+ }
183
+ const r = raw;
184
+ if (typeof r.allowed !== "boolean") {
185
+ throw new NeetruError("invalid_response", "Entitlement response missing `allowed` boolean");
186
+ }
187
+ return {
188
+ allowed: r.allowed,
189
+ productSlug: typeof r.productSlug === "string" ? r.productSlug : "",
190
+ feature: typeof r.feature === "string" ? r.feature : "",
191
+ reason: typeof r.reason === "string" ? r.reason : void 0
192
+ };
193
+ }
194
+ function createEntitlementsNamespace(config) {
195
+ async function checkDetailed(productSlug, feature) {
196
+ if (!productSlug) throw new NeetruError("validation_failed", "productSlug is required");
197
+ if (!feature) throw new NeetruError("validation_failed", "feature is required");
198
+ const raw = await httpRequest(config, {
199
+ method: "GET",
200
+ path: "/api/v1/sdk/entitlements/check",
201
+ query: { slug: productSlug, feature },
202
+ requireAuth: true
203
+ });
204
+ return toEntitlementCheck(raw);
205
+ }
206
+ return {
207
+ /**
208
+ * Verifica se o caller pode usar `feature` no produto `productSlug`.
209
+ * Retorno simples: `true` libera, `false` bloqueia.
210
+ *
211
+ * Use `checkDetailed` se precisar do `reason` pra mensagem de upgrade.
212
+ */
213
+ async check(productSlug, feature) {
214
+ const result = await checkDetailed(productSlug, feature);
215
+ return result.allowed;
216
+ },
217
+ checkDetailed
218
+ };
219
+ }
220
+
221
+ // src/telemetry.ts
222
+ var VALID_LOG_LEVELS = ["debug", "info", "warn", "error", "fatal"];
223
+ function consoleFor(level) {
224
+ switch (level) {
225
+ case "debug":
226
+ return console.debug.bind(console);
227
+ case "info":
228
+ return console.info.bind(console);
229
+ case "warn":
230
+ return console.warn.bind(console);
231
+ case "error":
232
+ return console.error.bind(console);
233
+ case "fatal":
234
+ return console.error.bind(console);
235
+ default:
236
+ return console.log.bind(console);
237
+ }
238
+ }
239
+ function createTelemetryNamespace(config) {
240
+ return {
241
+ /**
242
+ * Persiste um evento de uso. Lança `NeetruError` em qualquer falha
243
+ * (incluindo rate-limit).
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * await client.telemetry.event({
248
+ * name: 'dashboard_opened',
249
+ * properties: { plan: 'pro', tab: 'overview' },
250
+ * });
251
+ * ```
252
+ */
253
+ async event(input) {
254
+ if (!input || typeof input !== "object") {
255
+ throw new NeetruError("validation_failed", "event input is required");
256
+ }
257
+ if (typeof input.name !== "string" || input.name.length === 0) {
258
+ throw new NeetruError("validation_failed", "event.name is required");
259
+ }
260
+ if (input.name.length > 128) {
261
+ throw new NeetruError("validation_failed", "event.name max 128 chars");
262
+ }
263
+ const body = { name: input.name };
264
+ if (input.properties && typeof input.properties === "object") {
265
+ body.properties = input.properties;
266
+ }
267
+ if (input.timestamp) body.timestamp = input.timestamp;
268
+ const raw = await httpRequest(config, {
269
+ method: "POST",
270
+ path: "/api/v1/sdk/telemetry/event",
271
+ body,
272
+ requireAuth: true
273
+ });
274
+ if (!raw || raw.ok !== true || typeof raw.eventId !== "string") {
275
+ throw new NeetruError("invalid_response", "Telemetry response missing eventId");
276
+ }
277
+ return { ok: true, eventId: raw.eventId };
278
+ },
279
+ /**
280
+ * Registra um log estruturado per-product (Sprint 6).
281
+ *
282
+ * - `NEETRU_ENV=dev`: console.{level}, retorna ack mock.
283
+ * - workspace/prod: HTTP POST com Bearer auth + correlationId no header.
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * await client.telemetry.log({
288
+ * level: 'error',
289
+ * message: 'Falha ao calcular total',
290
+ * metadata: { orderId: 'o-123' },
291
+ * });
292
+ * ```
293
+ */
294
+ async log(input) {
295
+ if (!input || typeof input !== "object") {
296
+ throw new NeetruError("validation_failed", "log input is required");
297
+ }
298
+ if (!VALID_LOG_LEVELS.includes(input.level)) {
299
+ throw new NeetruError("validation_failed", `level must be one of ${VALID_LOG_LEVELS.join(", ")}`);
300
+ }
301
+ if (typeof input.message !== "string" || input.message.length === 0) {
302
+ throw new NeetruError("validation_failed", "message is required");
303
+ }
304
+ if (input.message.length > 4e3) {
305
+ throw new NeetruError("validation_failed", "message max 4000 chars");
306
+ }
307
+ if (config.env === "dev") {
308
+ const fn = consoleFor(input.level);
309
+ fn(`[neetru-sdk] ${input.level}: ${input.message}`, input.metadata ?? {});
310
+ return { ok: true, mode: "mock" };
311
+ }
312
+ const body = {
313
+ level: input.level,
314
+ message: input.message
315
+ };
316
+ if (input.metadata) body.metadata = input.metadata;
317
+ if (input.productSlug) body.productSlug = input.productSlug;
318
+ let cid = input.correlationId;
319
+ if (!cid) {
320
+ try {
321
+ const g = globalThis.NEETRU_CORRELATION_ID;
322
+ if (typeof g === "string" && g.length > 0) cid = g;
323
+ } catch {
324
+ }
325
+ }
326
+ const headers = {};
327
+ if (cid) headers["x-correlation-id"] = cid;
328
+ const raw = await httpRequest(config, {
329
+ method: "POST",
330
+ path: "/sdk/v1/telemetry/log",
331
+ body,
332
+ requireAuth: true,
333
+ headers
334
+ });
335
+ if (!raw || raw.ok !== true) {
336
+ throw new NeetruError("invalid_response", "Telemetry log response missing ok");
337
+ }
338
+ return {
339
+ ok: true,
340
+ logId: typeof raw.logId === "string" ? raw.logId : void 0,
341
+ mode: "http"
342
+ };
343
+ }
344
+ };
345
+ }
346
+
347
+ // src/usage.ts
348
+ function toQuota(metric, raw) {
349
+ if (!raw || typeof raw !== "object") {
350
+ throw new NeetruError("invalid_response", "Quota response is not an object");
351
+ }
352
+ const r = raw;
353
+ if (typeof r.used !== "number" || typeof r.limit !== "number") {
354
+ throw new NeetruError("invalid_response", "Quota response missing used/limit numbers");
355
+ }
356
+ return {
357
+ metric: typeof r.metric === "string" ? r.metric : metric,
358
+ used: r.used,
359
+ limit: r.limit,
360
+ resetsAt: typeof r.resetsAt === "string" ? r.resetsAt : void 0,
361
+ plan: typeof r.plan === "string" ? r.plan : void 0
362
+ };
363
+ }
364
+ function createUsageNamespace(config) {
365
+ return {
366
+ /**
367
+ * Persiste um evento de usage. Em dev (mocks ativos via factory) só loga.
368
+ * Em workspace/prod chama POST /sdk/v1/usage/record.
369
+ */
370
+ async track(event, properties) {
371
+ if (!event || typeof event !== "string") {
372
+ throw new NeetruError("validation_failed", "event name is required");
373
+ }
374
+ if (event.length > 128) {
375
+ throw new NeetruError("validation_failed", "event name max 128 chars");
376
+ }
377
+ const body = { event };
378
+ if (properties && typeof properties === "object") body.properties = properties;
379
+ const raw = await httpRequest(config, {
380
+ method: "POST",
381
+ path: "/sdk/v1/usage/record",
382
+ body,
383
+ requireAuth: true
384
+ });
385
+ if (!raw || raw.ok !== true) {
386
+ throw new NeetruError("invalid_response", "Usage record response missing ok");
387
+ }
388
+ return { ok: true };
389
+ },
390
+ /**
391
+ * Lê quota atual de uma métrica. Cacheável (caller decide), SDK não cacheia.
392
+ */
393
+ async getQuota(metric) {
394
+ if (!metric || typeof metric !== "string") {
395
+ throw new NeetruError("validation_failed", "metric is required");
396
+ }
397
+ const raw = await httpRequest(config, {
398
+ method: "GET",
399
+ path: "/sdk/v1/usage/quota",
400
+ query: { metric },
401
+ requireAuth: true
402
+ });
403
+ return toQuota(metric, raw);
404
+ },
405
+ /**
406
+ * v0.3 — Reporta consumo metered. Hit no endpoint canônico Sprint 7.
407
+ */
408
+ async report(resource, qty = 1, options) {
409
+ if (!resource || typeof resource !== "string") {
410
+ throw new NeetruError("validation_failed", "resource is required");
411
+ }
412
+ if (!Number.isFinite(qty) || qty <= 0) {
413
+ throw new NeetruError("validation_failed", "qty must be positive integer");
414
+ }
415
+ const productId = options?.productId ?? config.productId;
416
+ const tenantId = options?.tenantId ?? config.tenantId;
417
+ if (!productId) {
418
+ throw new NeetruError(
419
+ "validation_failed",
420
+ "productId required (pass to options or set on createNeetruClient)"
421
+ );
422
+ }
423
+ if (!tenantId) {
424
+ throw new NeetruError(
425
+ "validation_failed",
426
+ "tenantId required (pass to options or set on createNeetruClient)"
427
+ );
428
+ }
429
+ const raw = await httpRequest(config, {
430
+ method: "POST",
431
+ path: "/sdk/v1/usage/record",
432
+ body: { productId, tenantId, resource, qty: Math.floor(qty) },
433
+ requireAuth: true
434
+ });
435
+ if (!raw || raw.ok !== true) {
436
+ throw new NeetruError("invalid_response", "usage.report response missing ok");
437
+ }
438
+ return {
439
+ ok: true,
440
+ counterId: raw.counterId,
441
+ value: raw.value,
442
+ limit: raw.limit,
443
+ remaining: raw.remaining,
444
+ status: raw.status
445
+ };
446
+ },
447
+ /**
448
+ * v0.3 — Verifica entitlement de um resource via GET /sdk/v1/entitlements.
449
+ */
450
+ async check(resource, options) {
451
+ if (!resource || typeof resource !== "string") {
452
+ throw new NeetruError("validation_failed", "resource is required");
453
+ }
454
+ const productId = options?.productId ?? config.productId;
455
+ const tenantId = options?.tenantId ?? config.tenantId;
456
+ if (!productId || !tenantId) {
457
+ throw new NeetruError(
458
+ "validation_failed",
459
+ "productId and tenantId required"
460
+ );
461
+ }
462
+ const raw = await httpRequest(config, {
463
+ method: "GET",
464
+ path: "/sdk/v1/entitlements",
465
+ query: { productId, tenantId, feature: resource },
466
+ requireAuth: true
467
+ });
468
+ if (!raw || typeof raw.allowed !== "boolean") {
469
+ throw new NeetruError("invalid_response", "usage.check response missing allowed");
470
+ }
471
+ return {
472
+ allowed: raw.allowed,
473
+ reason: raw.reason,
474
+ remaining: raw.remaining,
475
+ limit: raw.limit,
476
+ planId: raw.planId,
477
+ planFeatures: raw.planFeatures
478
+ };
479
+ }
480
+ };
481
+ }
482
+
483
+ // src/support.ts
484
+ var VALID_SEVERITIES = ["low", "normal", "high", "urgent"];
485
+ var VALID_STATUSES = ["open", "pending", "resolved", "closed"];
486
+ function toTicket(raw) {
487
+ if (!raw || typeof raw !== "object") {
488
+ throw new NeetruError("invalid_response", "Ticket response is not an object");
489
+ }
490
+ const r = raw;
491
+ if (typeof r.id !== "string") {
492
+ throw new NeetruError("invalid_response", "Ticket missing id");
493
+ }
494
+ return {
495
+ id: r.id,
496
+ subject: typeof r.subject === "string" ? r.subject : "",
497
+ message: typeof r.message === "string" ? r.message : "",
498
+ severity: VALID_SEVERITIES.includes(r.severity) ? r.severity : "normal",
499
+ status: VALID_STATUSES.includes(r.status) ? r.status : "open",
500
+ createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
501
+ updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
502
+ productSlug: typeof r.productSlug === "string" ? r.productSlug : void 0
503
+ };
504
+ }
505
+ function createSupportNamespace(config) {
506
+ return {
507
+ /**
508
+ * Cria um ticket de suporte. Requer Bearer auth. Se `productSlug` não é
509
+ * passado, o backend infere do escopo do token.
510
+ */
511
+ async createTicket(input) {
512
+ if (!input || typeof input !== "object") {
513
+ throw new NeetruError("validation_failed", "input is required");
514
+ }
515
+ if (!input.subject || typeof input.subject !== "string") {
516
+ throw new NeetruError("validation_failed", "subject is required");
517
+ }
518
+ if (input.subject.length > 200) {
519
+ throw new NeetruError("validation_failed", "subject max 200 chars");
520
+ }
521
+ if (!input.message || typeof input.message !== "string") {
522
+ throw new NeetruError("validation_failed", "message is required");
523
+ }
524
+ if (input.message.length > 1e4) {
525
+ throw new NeetruError("validation_failed", "message max 10000 chars");
526
+ }
527
+ if (input.severity && !VALID_SEVERITIES.includes(input.severity)) {
528
+ throw new NeetruError("validation_failed", `severity must be one of ${VALID_SEVERITIES.join(", ")}`);
529
+ }
530
+ const slug = input.productSlug ?? "_default";
531
+ const body = {
532
+ subject: input.subject,
533
+ message: input.message,
534
+ severity: input.severity ?? "normal"
535
+ };
536
+ const raw = await httpRequest(config, {
537
+ method: "POST",
538
+ path: `/api/v1/products/${encodeURIComponent(slug)}/tickets`,
539
+ body,
540
+ requireAuth: true
541
+ });
542
+ const candidate = raw && typeof raw === "object" && "ticket" in raw ? raw.ticket : raw;
543
+ return toTicket(candidate);
544
+ },
545
+ /**
546
+ * Lista tickets do customer no produto atual (escopo do token).
547
+ */
548
+ async listMyTickets() {
549
+ const raw = await httpRequest(config, {
550
+ method: "GET",
551
+ path: "/api/v1/products/_default/tickets",
552
+ requireAuth: true
553
+ });
554
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
555
+ return list.map(toTicket);
556
+ }
557
+ };
558
+ }
559
+
560
+ // src/db.ts
561
+ var COLL_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
562
+ function assertValidCollection(name) {
563
+ if (!COLL_RE.test(name)) {
564
+ throw new NeetruError(
565
+ "validation_failed",
566
+ `Invalid collection name: "${name}". Must match ${COLL_RE}.`
567
+ );
568
+ }
569
+ }
570
+ function serializeWhere(filter) {
571
+ const { field, op, value } = filter;
572
+ if (op === "in") {
573
+ if (!Array.isArray(value)) {
574
+ throw new NeetruError(
575
+ "validation_failed",
576
+ `where op="in" requer value array (recebido: ${typeof value})`
577
+ );
578
+ }
579
+ return `${field}:in:${value.map((v) => String(v)).join(",")}`;
580
+ }
581
+ return `${field}:${op}:${String(value)}`;
582
+ }
583
+ function createDbNamespace(config) {
584
+ return {
585
+ collection(name) {
586
+ assertValidCollection(name);
587
+ const headers = {};
588
+ if (config.tenantId) headers["x-neetru-tenant"] = config.tenantId;
589
+ return {
590
+ async list(opts) {
591
+ if (opts?.limit !== void 0) opts.limit;
592
+ let path = `/sdk/v1/datastore/${name}`;
593
+ const params = new URLSearchParams();
594
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
595
+ if (opts?.where && opts.where.length > 0) {
596
+ for (const f of opts.where) {
597
+ params.append("where", serializeWhere(f));
598
+ }
599
+ }
600
+ if (config.tenantId) params.set("tenantId", config.tenantId);
601
+ const qs = params.toString();
602
+ if (qs) path += `?${qs}`;
603
+ const raw = await httpRequest(config, {
604
+ method: "GET",
605
+ path,
606
+ requireAuth: true,
607
+ headers
608
+ });
609
+ if (!raw || !Array.isArray(raw.items)) {
610
+ throw new NeetruError(
611
+ "invalid_response",
612
+ "datastore.list missing items[]"
613
+ );
614
+ }
615
+ return raw.items;
616
+ },
617
+ async get(id) {
618
+ if (!id || typeof id !== "string") {
619
+ throw new NeetruError("validation_failed", "id required");
620
+ }
621
+ try {
622
+ const raw = await httpRequest(config, {
623
+ method: "GET",
624
+ path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
625
+ requireAuth: true,
626
+ headers
627
+ });
628
+ return raw?.item ?? null;
629
+ } catch (err) {
630
+ if (err instanceof NeetruError && err.code === "not_found") return null;
631
+ throw err;
632
+ }
633
+ },
634
+ async add(data) {
635
+ if (!data || typeof data !== "object") {
636
+ throw new NeetruError("validation_failed", "data object required");
637
+ }
638
+ const raw = await httpRequest(config, {
639
+ method: "POST",
640
+ path: `/sdk/v1/datastore/${name}`,
641
+ body: { data },
642
+ requireAuth: true,
643
+ headers
644
+ });
645
+ if (!raw || typeof raw.id !== "string") {
646
+ throw new NeetruError("invalid_response", "datastore.add missing id");
647
+ }
648
+ return { ok: true, id: raw.id };
649
+ },
650
+ async set(id, data) {
651
+ if (!id || typeof id !== "string") {
652
+ throw new NeetruError("validation_failed", "id required");
653
+ }
654
+ await httpRequest(config, {
655
+ method: "PUT",
656
+ path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
657
+ body: { data },
658
+ requireAuth: true,
659
+ headers
660
+ });
661
+ return { ok: true };
662
+ },
663
+ async update(id, data) {
664
+ if (!id || typeof id !== "string") {
665
+ throw new NeetruError("validation_failed", "id required");
666
+ }
667
+ await httpRequest(config, {
668
+ method: "PATCH",
669
+ path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
670
+ body: { data },
671
+ requireAuth: true,
672
+ headers
673
+ });
674
+ return { ok: true };
675
+ },
676
+ async remove(id) {
677
+ if (!id || typeof id !== "string") {
678
+ throw new NeetruError("validation_failed", "id required");
679
+ }
680
+ await httpRequest(config, {
681
+ method: "DELETE",
682
+ path: `/sdk/v1/datastore/${name}/${encodeURIComponent(id)}`,
683
+ requireAuth: true,
684
+ headers
685
+ });
686
+ return { ok: true };
687
+ }
688
+ };
689
+ }
690
+ };
691
+ }
692
+
693
+ // src/mocks.ts
694
+ var DEV_FIXTURE_USER = Object.freeze({
695
+ uid: "dev-fixture-uid-0001",
696
+ email: "dev@neetru.local",
697
+ emailVerified: true,
698
+ displayName: "Dev Fixture",
699
+ isCustomer: true,
700
+ isStaff: false
701
+ });
702
+ var MockAuth = class {
703
+ _user;
704
+ _listeners;
705
+ constructor(initialUser = null) {
706
+ this._user = initialUser;
707
+ this._listeners = /* @__PURE__ */ new Set();
708
+ }
709
+ async signIn(_options) {
710
+ if (!this._user) this._user = { ...DEV_FIXTURE_USER };
711
+ this._notify();
712
+ return this._user;
713
+ }
714
+ async signOut() {
715
+ this._user = null;
716
+ this._notify();
717
+ }
718
+ getUser() {
719
+ return this._user;
720
+ }
721
+ onAuthStateChanged(listener) {
722
+ this._listeners.add(listener);
723
+ try {
724
+ listener(this._user);
725
+ } catch {
726
+ }
727
+ return () => {
728
+ this._listeners.delete(listener);
729
+ };
730
+ }
731
+ /** Helper de tests — força um user state arbitrário. */
732
+ __setUser(user) {
733
+ this._user = user;
734
+ this._notify();
735
+ }
736
+ _notify() {
737
+ for (const l of this._listeners) {
738
+ try {
739
+ l(this._user);
740
+ } catch {
741
+ }
742
+ }
743
+ }
744
+ };
745
+ var MockUsage = class {
746
+ _records = [];
747
+ _quotas;
748
+ /** v0.3 — counters in-memory pra `report()` / `check()`. */
749
+ _counters = /* @__PURE__ */ new Map();
750
+ constructor(initialQuotas = {}) {
751
+ this._quotas = new Map(Object.entries(initialQuotas));
752
+ }
753
+ async track(event, properties) {
754
+ this._records.push({
755
+ event,
756
+ properties,
757
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
758
+ });
759
+ return { ok: true };
760
+ }
761
+ async getQuota(metric) {
762
+ const existing = this._quotas.get(metric);
763
+ if (existing) return existing;
764
+ return {
765
+ metric,
766
+ used: this._records.filter((r) => r.event === metric).length,
767
+ limit: -1,
768
+ // unlimited em mock por default
769
+ plan: "mock"
770
+ };
771
+ }
772
+ /** v0.3 — Mock report incrementa o counter local. */
773
+ async report(resource, qty = 1, _options) {
774
+ if (!resource) throw new Error("resource required");
775
+ const safeQty = Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1;
776
+ const existing = this._counters.get(resource) ?? 0;
777
+ const next = existing + safeQty;
778
+ this._counters.set(resource, next);
779
+ const limitFixture = this._quotas.get(resource);
780
+ const limit = limitFixture?.limit ?? -1;
781
+ return {
782
+ ok: true,
783
+ counterId: `mock_${resource}`,
784
+ value: next,
785
+ limit,
786
+ remaining: limit < 0 ? -1 : Math.max(0, limit - next),
787
+ status: limit > 0 && next >= limit ? "read_only_overlimit" : "ok"
788
+ };
789
+ }
790
+ /** v0.3 — Mock check usa quotas + counters in-memory. */
791
+ async check(resource, _options) {
792
+ const quota = this._quotas.get(resource);
793
+ const used = this._counters.get(resource) ?? 0;
794
+ if (!quota) {
795
+ return { allowed: true, reason: "granted", limit: -1, remaining: -1 };
796
+ }
797
+ const remaining = quota.limit < 0 ? -1 : Math.max(0, quota.limit - used);
798
+ if (remaining === 0) {
799
+ return {
800
+ allowed: false,
801
+ reason: "limit_exceeded",
802
+ limit: quota.limit,
803
+ remaining: 0
804
+ };
805
+ }
806
+ return {
807
+ allowed: true,
808
+ reason: "granted",
809
+ limit: quota.limit,
810
+ remaining
811
+ };
812
+ }
813
+ /** Test helper. */
814
+ __getRecords() {
815
+ return [...this._records];
816
+ }
817
+ /** Test helper — substitui quota fixture. */
818
+ __setQuota(metric, quota) {
819
+ this._quotas.set(metric, quota);
820
+ }
821
+ /** Test helper — limpa estado. */
822
+ __reset() {
823
+ this._records = [];
824
+ this._quotas.clear();
825
+ this._counters.clear();
826
+ }
827
+ };
828
+ var mockTicketSeq = 0;
829
+ var MockSupport = class {
830
+ _tickets = [];
831
+ async createTicket(input) {
832
+ if (!input?.subject) throw new Error("subject required");
833
+ if (!input?.message) throw new Error("message required");
834
+ const ticket = {
835
+ id: `mock-ticket-${++mockTicketSeq}`,
836
+ subject: input.subject,
837
+ message: input.message,
838
+ severity: input.severity ?? "normal",
839
+ status: "open",
840
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
841
+ productSlug: input.productSlug
842
+ };
843
+ this._tickets.push(ticket);
844
+ return ticket;
845
+ }
846
+ async listMyTickets() {
847
+ return [...this._tickets];
848
+ }
849
+ /** Test helper. */
850
+ __reset() {
851
+ this._tickets = [];
852
+ }
853
+ };
854
+ var MockEntitlements = class {
855
+ _denied = /* @__PURE__ */ new Set();
856
+ async check(productSlug, feature) {
857
+ return !this._denied.has(`${productSlug}:${feature}`);
858
+ }
859
+ async checkDetailed(productSlug, feature) {
860
+ const allowed = await this.check(productSlug, feature);
861
+ return {
862
+ allowed,
863
+ productSlug,
864
+ feature,
865
+ reason: allowed ? "granted" : "mock_denied"
866
+ };
867
+ }
868
+ /** Test helper. */
869
+ __deny(productSlug, feature) {
870
+ this._denied.add(`${productSlug}:${feature}`);
871
+ }
872
+ /** Test helper. */
873
+ __reset() {
874
+ this._denied.clear();
875
+ }
876
+ };
877
+ var MockDb = class {
878
+ _store = /* @__PURE__ */ new Map();
879
+ _fixtures;
880
+ constructor(initialFixtures = {}) {
881
+ this._fixtures = new Map(Object.entries(initialFixtures));
882
+ }
883
+ collection(name) {
884
+ const _store = this._store;
885
+ const _fixtures = this._fixtures;
886
+ if (!_store.has(name)) {
887
+ const init = _fixtures.get(name);
888
+ _store.set(
889
+ name,
890
+ init ? new Map(Object.entries(init)) : /* @__PURE__ */ new Map()
891
+ );
892
+ }
893
+ const coll = _store.get(name);
894
+ const matchesFilter = (doc, f) => {
895
+ const v = doc[f.field];
896
+ switch (f.op) {
897
+ case "==":
898
+ return v === f.value;
899
+ case "!=":
900
+ return v !== f.value;
901
+ case "<":
902
+ return typeof v === "number" && typeof f.value === "number" && v < f.value;
903
+ case "<=":
904
+ return typeof v === "number" && typeof f.value === "number" && v <= f.value;
905
+ case ">":
906
+ return typeof v === "number" && typeof f.value === "number" && v > f.value;
907
+ case ">=":
908
+ return typeof v === "number" && typeof f.value === "number" && v >= f.value;
909
+ case "in":
910
+ return Array.isArray(f.value) && f.value.includes(v);
911
+ default:
912
+ return true;
913
+ }
914
+ };
915
+ let autoSeq = 0;
916
+ return {
917
+ async list(opts) {
918
+ let items = Array.from(coll.values());
919
+ if (opts?.where && opts.where.length > 0) {
920
+ items = items.filter(
921
+ (doc) => opts.where.every((f) => matchesFilter(doc, f))
922
+ );
923
+ }
924
+ if (opts?.limit !== void 0) items = items.slice(0, opts.limit);
925
+ return items;
926
+ },
927
+ async get(id) {
928
+ return coll.get(id) ?? null;
929
+ },
930
+ async add(data) {
931
+ const id = `mock-${++autoSeq}-${Math.random().toString(36).slice(2, 8)}`;
932
+ coll.set(id, { ...data, id });
933
+ return { ok: true, id };
934
+ },
935
+ async set(id, data) {
936
+ coll.set(id, { ...data, id });
937
+ return { ok: true };
938
+ },
939
+ async update(id, data) {
940
+ const cur = coll.get(id);
941
+ if (!cur) {
942
+ coll.set(id, { ...data, id });
943
+ } else {
944
+ coll.set(id, { ...cur, ...data });
945
+ }
946
+ return { ok: true };
947
+ },
948
+ async remove(id) {
949
+ coll.delete(id);
950
+ return { ok: true };
951
+ }
952
+ };
953
+ }
954
+ /** Test helper — substitui fixture inteira de uma collection. */
955
+ __setFixture(name, items) {
956
+ this._store.set(name, new Map(Object.entries(items)));
957
+ }
958
+ /** Test helper — reset total. */
959
+ __reset() {
960
+ this._store.clear();
961
+ }
962
+ };
963
+
964
+ // src/auth.ts
965
+ function readEnvApiKey() {
966
+ try {
967
+ const proc = globalThis.process;
968
+ const val = proc?.env?.NEETRU_API_KEY;
969
+ return typeof val === "string" && val.length > 0 ? val : void 0;
970
+ } catch {
971
+ return void 0;
972
+ }
973
+ }
974
+ function resolveEnv(configEnv) {
975
+ if (configEnv) return configEnv;
976
+ try {
977
+ const proc = globalThis.process;
978
+ const val = proc?.env?.NEETRU_ENV;
979
+ if (val === "dev" || val === "workspace" || val === "prod") return val;
980
+ } catch {
981
+ }
982
+ try {
983
+ const g = globalThis.NEETRU_ENV;
984
+ if (g === "dev" || g === "workspace" || g === "prod") return g;
985
+ } catch {
986
+ }
987
+ return "prod";
988
+ }
989
+ function createOidcAuthNamespace(config) {
990
+ const STORAGE_KEY = "neetru_id_token";
991
+ const listeners = /* @__PURE__ */ new Set();
992
+ let cachedUser = null;
993
+ function getStorage() {
994
+ try {
995
+ return typeof globalThis.localStorage !== "undefined" ? globalThis.localStorage : null;
996
+ } catch {
997
+ return null;
998
+ }
999
+ }
1000
+ function decodeJwtPayload(token) {
1001
+ const parts = token.split(".");
1002
+ if (parts.length !== 3) return null;
1003
+ try {
1004
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1005
+ const pad = "=".repeat((4 - b64.length % 4) % 4);
1006
+ const decoded = typeof atob === "function" ? atob(b64 + pad) : Buffer.from(b64 + pad, "base64").toString("utf-8");
1007
+ return JSON.parse(decoded);
1008
+ } catch {
1009
+ return null;
1010
+ }
1011
+ }
1012
+ function tokenToUser(token) {
1013
+ const payload = decodeJwtPayload(token);
1014
+ if (!payload || typeof payload.sub !== "string") return null;
1015
+ return {
1016
+ uid: payload.sub,
1017
+ email: typeof payload.email === "string" ? payload.email : "",
1018
+ emailVerified: typeof payload.email_verified === "boolean" ? payload.email_verified : void 0,
1019
+ displayName: typeof payload.name === "string" ? payload.name : void 0,
1020
+ photoURL: typeof payload.picture === "string" ? payload.picture : void 0,
1021
+ isStaff: payload.is_staff === true,
1022
+ isCustomer: payload.is_customer === true,
1023
+ tenantId: typeof payload.tenant_id === "string" ? payload.tenant_id : void 0,
1024
+ ...payload
1025
+ };
1026
+ }
1027
+ function loadCachedUser() {
1028
+ if (cachedUser) return cachedUser;
1029
+ const storage = getStorage();
1030
+ if (!storage) return null;
1031
+ const token = storage.getItem(STORAGE_KEY);
1032
+ if (!token) return null;
1033
+ cachedUser = tokenToUser(token);
1034
+ return cachedUser;
1035
+ }
1036
+ function notify() {
1037
+ for (const l of listeners) {
1038
+ try {
1039
+ l(cachedUser);
1040
+ } catch {
1041
+ }
1042
+ }
1043
+ }
1044
+ return {
1045
+ async signIn(options) {
1046
+ if (typeof globalThis.location !== "undefined" && typeof globalThis.location.assign === "function") {
1047
+ const redirectUri = options?.redirectUri ?? globalThis.location.origin;
1048
+ const scope = options?.scope ?? "openid profile email";
1049
+ const state = options?.postLoginRedirect ?? globalThis.location.href;
1050
+ const url = new URL("/oauth/authorize", config.baseUrl.replace(/\/api$/, ""));
1051
+ url.searchParams.set("response_type", "code");
1052
+ url.searchParams.set("redirect_uri", redirectUri);
1053
+ url.searchParams.set("scope", scope);
1054
+ url.searchParams.set("state", state);
1055
+ if (config.apiKey) {
1056
+ const clientId = config.apiKey.split("_")[1] ?? config.apiKey;
1057
+ url.searchParams.set("client_id", clientId);
1058
+ }
1059
+ globalThis.location.assign(url.toString());
1060
+ return;
1061
+ }
1062
+ throw new NeetruError(
1063
+ "invalid_config",
1064
+ "auth.signIn requires a browser context or mocks. Use NEETRU_ENV=dev or pass mocks.auth."
1065
+ );
1066
+ },
1067
+ async signOut() {
1068
+ const storage = getStorage();
1069
+ if (storage) storage.removeItem(STORAGE_KEY);
1070
+ cachedUser = null;
1071
+ try {
1072
+ await config.fetch(`${config.baseUrl}/oauth/revoke`, {
1073
+ method: "POST",
1074
+ headers: { "content-type": "application/json" }
1075
+ });
1076
+ } catch {
1077
+ }
1078
+ notify();
1079
+ },
1080
+ getUser() {
1081
+ return loadCachedUser();
1082
+ },
1083
+ onAuthStateChanged(listener) {
1084
+ listeners.add(listener);
1085
+ try {
1086
+ listener(loadCachedUser());
1087
+ } catch {
1088
+ }
1089
+ return () => {
1090
+ listeners.delete(listener);
1091
+ };
1092
+ }
1093
+ };
1094
+ }
1095
+ function createNeetruClient(config = {}) {
1096
+ const fetchImpl = config.fetch ?? globalThis.fetch;
1097
+ if (typeof fetchImpl !== "function") {
1098
+ throw new NeetruError(
1099
+ "invalid_config",
1100
+ "fetch is not available in this runtime. Pass `fetch` explicitly to createNeetruClient."
1101
+ );
1102
+ }
1103
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1104
+ const apiKey = config.apiKey ?? readEnvApiKey();
1105
+ const env = resolveEnv(config.env);
1106
+ const resolved = Object.freeze({
1107
+ apiKey,
1108
+ baseUrl,
1109
+ fetch: fetchImpl.bind(globalThis),
1110
+ env,
1111
+ productId: config.productId,
1112
+ tenantId: config.tenantId
1113
+ });
1114
+ const isDev = env === "dev";
1115
+ const auth = config.mocks?.auth ?? (isDev ? new MockAuth({ ...DEV_FIXTURE_USER }) : createOidcAuthNamespace(resolved));
1116
+ const usage = config.mocks?.usage ?? (isDev ? new MockUsage() : createUsageNamespace(resolved));
1117
+ const support = config.mocks?.support ?? (isDev ? new MockSupport() : createSupportNamespace(resolved));
1118
+ const entitlements = config.mocks?.entitlements ?? (isDev ? new MockEntitlements() : createEntitlementsNamespace(resolved));
1119
+ const db = config.mocks?.db ?? (isDev ? new MockDb() : createDbNamespace(resolved));
1120
+ const client = Object.freeze({
1121
+ config: resolved,
1122
+ auth,
1123
+ catalog: createCatalogNamespace(resolved),
1124
+ entitlements,
1125
+ telemetry: createTelemetryNamespace(resolved),
1126
+ usage,
1127
+ support,
1128
+ db
1129
+ });
1130
+ return client;
1131
+ }
1132
+
1133
+ // src/index.ts
1134
+ var VERSION = "1.0.0";
1135
+ function initNeetru(config) {
1136
+ const { apiUrl, baseUrl, ...rest } = config;
1137
+ return createNeetruClient({ ...rest, baseUrl: baseUrl ?? apiUrl });
1138
+ }
1139
+
1140
+ export { DEFAULT_BASE_URL, DEV_FIXTURE_USER, MockAuth, MockDb, MockEntitlements, MockSupport, MockUsage, NeetruError, VERSION, createNeetruClient, initNeetru };
1141
+ //# sourceMappingURL=index.mjs.map
1142
+ //# sourceMappingURL=index.mjs.map