@neetru/sdk 2.3.6 → 3.0.1

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 (83) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/README.md +84 -22
  3. package/dist/auth.cjs +360 -140
  4. package/dist/auth.cjs.map +1 -1
  5. package/dist/auth.d.cts +1 -2
  6. package/dist/auth.d.ts +1 -2
  7. package/dist/auth.mjs +360 -140
  8. package/dist/auth.mjs.map +1 -1
  9. package/dist/catalog.cjs +21 -2
  10. package/dist/catalog.cjs.map +1 -1
  11. package/dist/catalog.d.cts +1 -2
  12. package/dist/catalog.d.ts +1 -2
  13. package/dist/catalog.mjs +21 -2
  14. package/dist/catalog.mjs.map +1 -1
  15. package/dist/checkout.cjs +21 -2
  16. package/dist/checkout.cjs.map +1 -1
  17. package/dist/checkout.d.cts +1 -2
  18. package/dist/checkout.d.ts +1 -2
  19. package/dist/checkout.mjs +21 -2
  20. package/dist/checkout.mjs.map +1 -1
  21. package/dist/db.cjs +28 -3
  22. package/dist/db.cjs.map +1 -1
  23. package/dist/db.d.cts +1 -2
  24. package/dist/db.d.ts +1 -2
  25. package/dist/db.mjs +28 -3
  26. package/dist/db.mjs.map +1 -1
  27. package/dist/entitlements.cjs +21 -2
  28. package/dist/entitlements.cjs.map +1 -1
  29. package/dist/entitlements.d.cts +1 -2
  30. package/dist/entitlements.d.ts +1 -2
  31. package/dist/entitlements.mjs +21 -2
  32. package/dist/entitlements.mjs.map +1 -1
  33. package/dist/errors.cjs.map +1 -1
  34. package/dist/errors.d.cts +3 -1
  35. package/dist/errors.d.ts +3 -1
  36. package/dist/errors.mjs.map +1 -1
  37. package/dist/index.cjs +408 -146
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +31 -20
  40. package/dist/index.d.ts +31 -20
  41. package/dist/index.mjs +408 -146
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/mocks.cjs +33 -33
  44. package/dist/mocks.cjs.map +1 -1
  45. package/dist/mocks.d.cts +23 -20
  46. package/dist/mocks.d.ts +23 -20
  47. package/dist/mocks.mjs +33 -33
  48. package/dist/mocks.mjs.map +1 -1
  49. package/dist/notifications.cjs +80 -14
  50. package/dist/notifications.cjs.map +1 -1
  51. package/dist/notifications.d.cts +1 -2
  52. package/dist/notifications.d.ts +1 -2
  53. package/dist/notifications.mjs +80 -14
  54. package/dist/notifications.mjs.map +1 -1
  55. package/dist/react.d.cts +1 -2
  56. package/dist/react.d.ts +1 -2
  57. package/dist/support.cjs +72 -12
  58. package/dist/support.cjs.map +1 -1
  59. package/dist/support.d.cts +1 -2
  60. package/dist/support.d.ts +1 -2
  61. package/dist/support.mjs +72 -12
  62. package/dist/support.mjs.map +1 -1
  63. package/dist/telemetry.cjs +23 -4
  64. package/dist/telemetry.cjs.map +1 -1
  65. package/dist/telemetry.d.cts +1 -2
  66. package/dist/telemetry.d.ts +1 -2
  67. package/dist/telemetry.mjs +23 -4
  68. package/dist/telemetry.mjs.map +1 -1
  69. package/dist/{types-CSC-RIdS.d.ts → types-DALIhcbq.d.ts} +130 -74
  70. package/dist/{types-sGGN1vkg.d.cts → types-Lfd3LiAF.d.cts} +130 -74
  71. package/dist/usage.cjs +57 -63
  72. package/dist/usage.cjs.map +1 -1
  73. package/dist/usage.d.cts +1 -2
  74. package/dist/usage.d.ts +1 -2
  75. package/dist/usage.mjs +57 -63
  76. package/dist/usage.mjs.map +1 -1
  77. package/dist/webhooks.cjs +110 -33
  78. package/dist/webhooks.cjs.map +1 -1
  79. package/dist/webhooks.d.cts +1 -2
  80. package/dist/webhooks.d.ts +1 -2
  81. package/dist/webhooks.mjs +110 -33
  82. package/dist/webhooks.mjs.map +1 -1
  83. package/package.json +2 -3
package/dist/auth.mjs CHANGED
@@ -31,6 +31,23 @@ var init_errors = __esm({
31
31
  }
32
32
  });
33
33
 
34
+ // src/idempotency.ts
35
+ function generateIdempotencyKey() {
36
+ const cryptoObj = typeof globalThis !== "undefined" && "crypto" in globalThis ? globalThis.crypto : void 0;
37
+ if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
38
+ return cryptoObj.randomUUID();
39
+ }
40
+ throw new NeetruError(
41
+ "runtime_unsupported",
42
+ "crypto.randomUUID not available in this runtime. SDK 3.0 requires Node \u226520 or modern browser/Edge."
43
+ );
44
+ }
45
+ var init_idempotency = __esm({
46
+ "src/idempotency.ts"() {
47
+ init_errors();
48
+ }
49
+ });
50
+
34
51
  // src/http.ts
35
52
  var http_exports = {};
36
53
  __export(http_exports, {
@@ -88,7 +105,8 @@ async function safeJson(res) {
88
105
  async function httpRequest(config, opts) {
89
106
  const method = opts.method ?? "GET";
90
107
  const url = buildUrl(config.baseUrl, opts.path, opts.query);
91
- const maxRetries = opts.retries ?? DEFAULT_RETRIES;
108
+ const defaultRetries = READ_METHODS.has(method) ? DEFAULT_RETRIES_READ : DEFAULT_RETRIES_WRITE;
109
+ const maxRetries = opts.retries ?? defaultRetries;
92
110
  const headers = {
93
111
  accept: "application/json",
94
112
  ...opts.headers
@@ -102,6 +120,10 @@ async function httpRequest(config, opts) {
102
120
  }
103
121
  headers.authorization = `Bearer ${config.apiKey}`;
104
122
  }
123
+ if (opts.idempotencyKey) {
124
+ const key = typeof opts.idempotencyKey === "string" ? opts.idempotencyKey : generateIdempotencyKey();
125
+ headers["idempotency-key"] = key;
126
+ }
105
127
  const bodyString = opts.body !== void 0 && method !== "GET" && method !== "DELETE" ? JSON.stringify(opts.body) : void 0;
106
128
  if (bodyString !== void 0) {
107
129
  headers["content-type"] = "application/json";
@@ -153,11 +175,14 @@ async function httpRequest(config, opts) {
153
175
  }
154
176
  throw lastError ?? new NeetruError("unknown", "unexpected httpRequest exit");
155
177
  }
156
- var DEFAULT_RETRIES, RETRYABLE_CODES;
178
+ var DEFAULT_RETRIES_READ, DEFAULT_RETRIES_WRITE, READ_METHODS, RETRYABLE_CODES;
157
179
  var init_http = __esm({
158
180
  "src/http.ts"() {
159
181
  init_errors();
160
- DEFAULT_RETRIES = 2;
182
+ init_idempotency();
183
+ DEFAULT_RETRIES_READ = 2;
184
+ DEFAULT_RETRIES_WRITE = 0;
185
+ READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
161
186
  RETRYABLE_CODES = /* @__PURE__ */ new Set([
162
187
  "rate_limited",
163
188
  "server_error",
@@ -574,6 +599,109 @@ var init_sql_client = __esm({
574
599
  // src/auth.ts
575
600
  init_errors();
576
601
 
602
+ // src/api-key.ts
603
+ init_errors();
604
+ var API_KEY_RE = /^nrt_([A-Za-z0-9]+)_([A-Za-z0-9]+)$/;
605
+ function parseApiKey(apiKey) {
606
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
607
+ throw new NeetruError(
608
+ "invalid_config",
609
+ "apiKey is required (formato nrt_<keyId>_<secret>)"
610
+ );
611
+ }
612
+ const match = API_KEY_RE.exec(apiKey);
613
+ if (!match) {
614
+ throw new NeetruError(
615
+ "invalid_config",
616
+ "apiKey malformado - esperado formato nrt_<keyId>_<secret>"
617
+ );
618
+ }
619
+ return { keyId: match[1], secret: match[2] };
620
+ }
621
+ function tryParseApiKey(apiKey) {
622
+ if (typeof apiKey !== "string" || apiKey.length === 0) return null;
623
+ const match = API_KEY_RE.exec(apiKey);
624
+ if (!match) return null;
625
+ return { keyId: match[1], secret: match[2] };
626
+ }
627
+
628
+ // src/base-url-allowlist.ts
629
+ init_errors();
630
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
631
+ var NEETRU_SUFFIX = ".neetru.com";
632
+ var NEETRU_APEX = "neetru.com";
633
+ var SAFE_DEV_HOST_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\.(test|localhost)$/;
634
+ function isSafeDevHost(hostname) {
635
+ return SAFE_DEV_HOST_RE.test(hostname);
636
+ }
637
+ function normalizeHostname(hostname) {
638
+ return hostname.toLowerCase().replace(/\.+$/, "");
639
+ }
640
+ function validateBaseUrl(baseUrl, allowedHosts = []) {
641
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) {
642
+ throw new NeetruError("invalid_config", "baseUrl is required");
643
+ }
644
+ let parsed;
645
+ try {
646
+ parsed = new URL(baseUrl);
647
+ } catch {
648
+ throw new NeetruError("invalid_config", `baseUrl is not a valid URL: ${baseUrl}`);
649
+ }
650
+ const protocol = parsed.protocol;
651
+ const hostname = normalizeHostname(parsed.hostname);
652
+ if (isInsecureBypassActive()) {
653
+ if (typeof console !== "undefined") {
654
+ console.warn(
655
+ `[neetru-sdk] NEETRU_ALLOW_INSECURE_BASEURL=1 ativo. baseUrl=${baseUrl} aceito sem valida\xE7\xE3o.`
656
+ );
657
+ }
658
+ return baseUrl.replace(/\/+$/, "");
659
+ }
660
+ if (protocol !== "https:" && protocol !== "http:") {
661
+ throw new NeetruError(
662
+ "invalid_config",
663
+ `baseUrl protocol must be http: or https:, got ${protocol}`
664
+ );
665
+ }
666
+ if (protocol === "http:") {
667
+ const isLoopback = LOOPBACK_HOSTS.has(hostname) || isSafeDevHost(hostname);
668
+ if (!isLoopback) {
669
+ throw new NeetruError(
670
+ "invalid_config",
671
+ `baseUrl with http: only allowed for localhost/127.0.0.1/*.test/*.localhost. Got: ${hostname}. Use https: or set NEETRU_ALLOW_INSECURE_BASEURL=1 to bypass.`
672
+ );
673
+ }
674
+ return baseUrl.replace(/\/+$/, "");
675
+ }
676
+ const isNeetruDomain = hostname === NEETRU_APEX || hostname.endsWith(NEETRU_SUFFIX);
677
+ const isAllowedHost = allowedHosts.some((allowed) => {
678
+ const norm = allowed.toLowerCase();
679
+ return hostname === norm || hostname.endsWith(`.${norm}`);
680
+ });
681
+ const isLoopbackHttps = LOOPBACK_HOSTS.has(hostname) || isSafeDevHost(hostname);
682
+ if (!isNeetruDomain && !isAllowedHost && !isLoopbackHttps) {
683
+ throw new NeetruError(
684
+ "invalid_config",
685
+ `baseUrl ${hostname} is not in the allowlist. Allowed: *.neetru.com, localhost, or configure { allowedHosts: ['...'] }. Bypass: NEETRU_ALLOW_INSECURE_BASEURL=1.`
686
+ );
687
+ }
688
+ return baseUrl.replace(/\/+$/, "");
689
+ }
690
+ function isInsecureBypassActive() {
691
+ try {
692
+ const proc = globalThis.process;
693
+ const envVal = proc?.env?.NEETRU_ALLOW_INSECURE_BASEURL;
694
+ if (envVal === "1" || envVal === "true") return true;
695
+ } catch {
696
+ }
697
+ try {
698
+ const g = globalThis.NEETRU_ALLOW_INSECURE_BASEURL;
699
+ if (g === "1" || g === "true" || g === true) return true;
700
+ } catch {
701
+ }
702
+ return false;
703
+ }
704
+
577
705
  // src/types.ts
578
706
  var DEFAULT_BASE_URL = "https://api.neetru.com";
579
707
 
@@ -775,7 +903,7 @@ function createTelemetryNamespace(config) {
775
903
  if (ev.timestamp) body.timestamp = ev.timestamp;
776
904
  await httpRequest(config, {
777
905
  method: "POST",
778
- path: "/api/v1/sdk/telemetry/event",
906
+ path: "/api/sdk/v1/telemetry/event",
779
907
  body,
780
908
  requireAuth: true,
781
909
  retries: 1
@@ -824,7 +952,7 @@ function createTelemetryNamespace(config) {
824
952
  if (input.timestamp) body.timestamp = input.timestamp;
825
953
  const raw = await httpRequest(config, {
826
954
  method: "POST",
827
- path: "/api/v1/sdk/telemetry/event",
955
+ path: "/api/sdk/v1/telemetry/event",
828
956
  body,
829
957
  requireAuth: true
830
958
  });
@@ -936,65 +1064,33 @@ function createTelemetryNamespace(config) {
936
1064
  // src/usage.ts
937
1065
  init_errors();
938
1066
  init_http();
939
- function toQuota(metric, raw) {
940
- if (!raw || typeof raw !== "object") {
941
- throw new NeetruError("invalid_response", "Quota response is not an object");
942
- }
943
- const r = raw;
944
- if (typeof r.used !== "number" || typeof r.limit !== "number") {
945
- throw new NeetruError("invalid_response", "Quota response missing used/limit numbers");
1067
+
1068
+ // src/validators.ts
1069
+ init_errors();
1070
+ var SDK_ID_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
1071
+ function assertValidSdkId(field, value) {
1072
+ if (typeof value !== "string" || !SDK_ID_RE.test(value)) {
1073
+ throw new NeetruError(
1074
+ "validation_failed",
1075
+ `${field} must match ${SDK_ID_RE.source} (lowercase alphanumeric, _, -; first char must be alphanumeric; max 63 chars)`
1076
+ );
946
1077
  }
947
- return {
948
- metric: typeof r.metric === "string" ? r.metric : metric,
949
- used: r.used,
950
- limit: r.limit,
951
- resetsAt: typeof r.resetsAt === "string" ? r.resetsAt : void 0,
952
- plan: typeof r.plan === "string" ? r.plan : void 0
953
- };
954
1078
  }
1079
+
1080
+ // src/usage.ts
955
1081
  function createUsageNamespace(config) {
956
1082
  return {
957
1083
  /**
958
- * Persiste um evento de usage. Em dev (mocks ativos via factory) só loga.
959
- * Em workspace/prod chama POST /sdk/v1/usage/record.
960
- */
961
- async track(event, properties) {
962
- if (!event || typeof event !== "string") {
963
- throw new NeetruError("validation_failed", "event name is required");
964
- }
965
- if (event.length > 128) {
966
- throw new NeetruError("validation_failed", "event name max 128 chars");
967
- }
968
- const body = { event };
969
- if (properties && typeof properties === "object") body.properties = properties;
970
- const raw = await httpRequest(config, {
971
- method: "POST",
972
- path: "/sdk/v1/usage/record",
973
- body,
974
- requireAuth: true
975
- });
976
- if (!raw || raw.ok !== true) {
977
- throw new NeetruError("invalid_response", "Usage record response missing ok");
978
- }
979
- return { ok: true };
980
- },
981
- /**
982
- * Lê quota atual de uma métrica. Cacheável (caller decide), SDK não cacheia.
983
- */
984
- async getQuota(metric) {
985
- if (!metric || typeof metric !== "string") {
986
- throw new NeetruError("validation_failed", "metric is required");
987
- }
988
- const raw = await httpRequest(config, {
989
- method: "GET",
990
- path: "/sdk/v1/usage/quota",
991
- query: { metric },
992
- requireAuth: true
993
- });
994
- return toQuota(metric, raw);
995
- },
996
- /**
997
- * v0.3 — Reporta consumo metered. Hit no endpoint canônico Sprint 7.
1084
+ * Reporta consumo metered de um resource. Hit no endpoint canônico
1085
+ * `POST /api/sdk/v1/usage/record`. Idempotency-Key gerado por chamada;
1086
+ * Core dedup serverside TTL 24h.
1087
+ *
1088
+ * @example
1089
+ * ```ts
1090
+ * await client.usage.report('api_call', 1);
1091
+ * // ou explicit:
1092
+ * await client.usage.report('api_call', 5, { productId: 'gestovendas', tenantId: 't-acme' });
1093
+ * ```
998
1094
  */
999
1095
  async report(resource, qty = 1, options) {
1000
1096
  if (!resource || typeof resource !== "string") {
@@ -1017,11 +1113,14 @@ function createUsageNamespace(config) {
1017
1113
  "tenantId required (pass to options or set on createNeetruClient)"
1018
1114
  );
1019
1115
  }
1116
+ assertValidSdkId("productId", productId);
1117
+ assertValidSdkId("tenantId", tenantId);
1020
1118
  const raw = await httpRequest(config, {
1021
1119
  method: "POST",
1022
- path: "/sdk/v1/usage/record",
1120
+ path: "/api/sdk/v1/usage/record",
1023
1121
  body: { productId, tenantId, resource, qty: Math.floor(qty) },
1024
- requireAuth: true
1122
+ requireAuth: true,
1123
+ idempotencyKey: true
1025
1124
  });
1026
1125
  if (!raw || raw.ok !== true) {
1027
1126
  throw new NeetruError("invalid_response", "usage.report response missing ok");
@@ -1036,7 +1135,10 @@ function createUsageNamespace(config) {
1036
1135
  };
1037
1136
  },
1038
1137
  /**
1039
- * v0.3 — Verifica entitlement de um resource via GET /sdk/v1/entitlements.
1138
+ * Verifica entitlement de um resource via `GET /api/sdk/v1/entitlements`.
1139
+ * Resposta inclui `allowed`, `reason` ('granted' | 'not_subscribed' |
1140
+ * 'subscription_inactive' | 'feature_not_in_plan' | 'limit_exceeded'),
1141
+ * `remaining`, `limit`, `planId`, `planFeatures`.
1040
1142
  */
1041
1143
  async check(resource, options) {
1042
1144
  if (!resource || typeof resource !== "string") {
@@ -1047,25 +1149,29 @@ function createUsageNamespace(config) {
1047
1149
  if (!productId || !tenantId) {
1048
1150
  throw new NeetruError(
1049
1151
  "validation_failed",
1050
- "productId and tenantId required"
1152
+ "productId and tenantId required (pass to options or set on createNeetruClient)"
1051
1153
  );
1052
1154
  }
1155
+ assertValidSdkId("productId", productId);
1156
+ assertValidSdkId("tenantId", tenantId);
1053
1157
  const raw = await httpRequest(config, {
1054
1158
  method: "GET",
1055
- path: "/sdk/v1/entitlements",
1159
+ path: "/api/sdk/v1/entitlements",
1056
1160
  query: { productId, tenantId, feature: resource },
1057
1161
  requireAuth: true
1058
1162
  });
1059
1163
  if (!raw || typeof raw.allowed !== "boolean") {
1060
1164
  throw new NeetruError("invalid_response", "usage.check response missing allowed");
1061
1165
  }
1166
+ const behavior = raw.behavior ?? (raw.reason === "limit_exceeded" ? "readonly" : null);
1062
1167
  return {
1063
1168
  allowed: raw.allowed,
1064
1169
  reason: raw.reason,
1065
1170
  remaining: raw.remaining,
1066
1171
  limit: raw.limit,
1067
1172
  planId: raw.planId,
1068
- planFeatures: raw.planFeatures
1173
+ planFeatures: raw.planFeatures,
1174
+ behavior
1069
1175
  };
1070
1176
  }
1071
1177
  };
@@ -1092,14 +1198,32 @@ function toTicket(raw) {
1092
1198
  status: VALID_STATUSES.includes(r.status) ? r.status : "open",
1093
1199
  createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
1094
1200
  updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
1095
- productSlug: typeof r.productSlug === "string" ? r.productSlug : void 0
1201
+ productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0
1096
1202
  };
1097
1203
  }
1204
+ function resolveProductId(config, ...candidates) {
1205
+ for (const c of candidates) {
1206
+ if (typeof c === "string" && c.length > 0) {
1207
+ assertValidSdkId("productSlug", c);
1208
+ return c;
1209
+ }
1210
+ }
1211
+ if (config.productId) {
1212
+ assertValidSdkId("productId", config.productId);
1213
+ return config.productId;
1214
+ }
1215
+ throw new NeetruError(
1216
+ "validation_failed",
1217
+ "productId required (pass to input.productSlug or set on createNeetruClient)"
1218
+ );
1219
+ }
1098
1220
  function createSupportNamespace(config) {
1099
1221
  return {
1100
1222
  /**
1101
- * Cria um ticket de suporte. Requer Bearer auth. Se `productSlug` não é
1102
- * passado, o backend infere do escopo do token.
1223
+ * Cria um ticket de suporte. Requer Bearer auth. `productSlug` no input
1224
+ * (ou `config.productId` default) vai como `productId` no body do Core.
1225
+ *
1226
+ * Endpoint canonical: `POST /api/sdk/v1/support/tickets`.
1103
1227
  */
1104
1228
  async createTicket(input) {
1105
1229
  if (!input || typeof input !== "object") {
@@ -1118,30 +1242,42 @@ function createSupportNamespace(config) {
1118
1242
  throw new NeetruError("validation_failed", "message max 10000 chars");
1119
1243
  }
1120
1244
  if (input.severity && !VALID_SEVERITIES.includes(input.severity)) {
1121
- throw new NeetruError("validation_failed", `severity must be one of ${VALID_SEVERITIES.join(", ")}`);
1245
+ throw new NeetruError(
1246
+ "validation_failed",
1247
+ `severity must be one of ${VALID_SEVERITIES.join(", ")}`
1248
+ );
1122
1249
  }
1123
- const slug = input.productSlug ?? "_default";
1250
+ const productId = resolveProductId(config, input.productSlug);
1124
1251
  const body = {
1252
+ productId,
1125
1253
  subject: input.subject,
1126
1254
  message: input.message,
1127
1255
  severity: input.severity ?? "normal"
1128
1256
  };
1257
+ if (input.metadata && typeof input.metadata === "object") {
1258
+ body.metadata = input.metadata;
1259
+ }
1129
1260
  const raw = await httpRequest(config, {
1130
1261
  method: "POST",
1131
- path: `/api/v1/products/${encodeURIComponent(slug)}/tickets`,
1262
+ path: "/api/sdk/v1/support/tickets",
1132
1263
  body,
1133
- requireAuth: true
1264
+ requireAuth: true,
1265
+ idempotencyKey: true
1134
1266
  });
1135
1267
  const candidate = raw && typeof raw === "object" && "ticket" in raw ? raw.ticket : raw;
1136
1268
  return toTicket(candidate);
1137
1269
  },
1138
1270
  /**
1139
- * Lista tickets do customer no produto atual (escopo do token).
1271
+ * Lista tickets do customer no produto atual. `productSlug` no contexto.
1272
+ *
1273
+ * Endpoint canonical: `GET /api/sdk/v1/support/tickets?productId=...`.
1140
1274
  */
1141
- async listMyTickets() {
1275
+ async listMyTickets(options) {
1276
+ const productId = resolveProductId(config, options?.productSlug);
1142
1277
  const raw = await httpRequest(config, {
1143
1278
  method: "GET",
1144
- path: "/api/v1/products/_default/tickets",
1279
+ path: "/api/sdk/v1/support/tickets",
1280
+ query: { productId },
1145
1281
  requireAuth: true
1146
1282
  });
1147
1283
  const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
@@ -4774,42 +4910,71 @@ function validateInput(input) {
4774
4910
  throw new NeetruError("validation_failed", "secret deve ter \u226516 chars (recomendado 32+)");
4775
4911
  }
4776
4912
  }
4913
+ function resolveProductId2(config, ...candidates) {
4914
+ for (const c of candidates) {
4915
+ if (typeof c === "string" && c.length > 0) {
4916
+ assertValidSdkId("productId", c);
4917
+ return c;
4918
+ }
4919
+ }
4920
+ if (config.productId) {
4921
+ assertValidSdkId("productId", config.productId);
4922
+ return config.productId;
4923
+ }
4924
+ throw new NeetruError(
4925
+ "validation_failed",
4926
+ "productId required (pass to options/input or set on createNeetruClient)"
4927
+ );
4928
+ }
4777
4929
  function createWebhooksNamespace(config) {
4778
4930
  return {
4779
4931
  async register(input) {
4780
4932
  validateInput(input);
4933
+ const productId = resolveProductId2(config, input.productId);
4781
4934
  const raw = await httpRequest(config, {
4782
4935
  method: "POST",
4783
4936
  path: "/api/sdk/v1/webhooks",
4784
- body: input,
4937
+ body: {
4938
+ productId,
4939
+ url: input.url,
4940
+ events: input.events,
4941
+ ...input.secret ? { secret: input.secret } : {}
4942
+ },
4785
4943
  requireAuth: true
4786
4944
  });
4787
4945
  return toEndpoint(raw);
4788
4946
  },
4789
- async list() {
4947
+ async list(options) {
4948
+ const productId = resolveProductId2(config, options?.productId);
4790
4949
  const raw = await httpRequest(config, {
4791
4950
  method: "GET",
4792
4951
  path: "/api/sdk/v1/webhooks",
4952
+ query: { productId },
4793
4953
  requireAuth: true
4794
4954
  });
4795
4955
  const list = Array.isArray(raw?.endpoints) ? raw.endpoints : [];
4796
4956
  return list.map(toEndpoint);
4797
4957
  },
4798
- async unregister(id) {
4958
+ async unregister(id, options) {
4799
4959
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
4960
+ const productId = resolveProductId2(config, options?.productId);
4800
4961
  await httpRequest(config, {
4801
4962
  method: "DELETE",
4802
4963
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}`,
4964
+ query: { productId },
4803
4965
  requireAuth: true
4804
4966
  });
4805
4967
  return { ok: true };
4806
4968
  },
4807
- async test(id) {
4969
+ async test(id, options) {
4808
4970
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
4971
+ const productId = resolveProductId2(config, options?.productId);
4809
4972
  const raw = await httpRequest(config, {
4810
4973
  method: "POST",
4811
4974
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}/test`,
4812
- requireAuth: true
4975
+ query: { productId },
4976
+ requireAuth: true,
4977
+ idempotencyKey: true
4813
4978
  });
4814
4979
  if (!raw || typeof raw !== "object") {
4815
4980
  return { ok: false, error: "invalid response" };
@@ -4841,17 +5006,17 @@ var MockWebhooks = class {
4841
5006
  this.endpoints.set(id, endpoint);
4842
5007
  return endpoint;
4843
5008
  }
4844
- async list() {
5009
+ async list(_options) {
4845
5010
  return [...this.endpoints.values()];
4846
5011
  }
4847
- async unregister(id) {
5012
+ async unregister(id, _options) {
4848
5013
  if (!this.endpoints.has(id)) {
4849
5014
  throw new NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4850
5015
  }
4851
5016
  this.endpoints.delete(id);
4852
5017
  return { ok: true };
4853
5018
  }
4854
- async test(id) {
5019
+ async test(id, _options) {
4855
5020
  if (!this.endpoints.has(id)) {
4856
5021
  throw new NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4857
5022
  }
@@ -4908,49 +5073,85 @@ function validateInput2(input) {
4908
5073
  throw new NeetruError("validation_failed", "body m\xE1x 2000 chars");
4909
5074
  }
4910
5075
  }
5076
+ function resolveProductId3(config, ...candidates) {
5077
+ for (const c of candidates) {
5078
+ if (typeof c === "string" && c.length > 0) {
5079
+ assertValidSdkId("productId", c);
5080
+ return c;
5081
+ }
5082
+ }
5083
+ if (config.productId) {
5084
+ assertValidSdkId("productId", config.productId);
5085
+ return config.productId;
5086
+ }
5087
+ throw new NeetruError(
5088
+ "validation_failed",
5089
+ "productId required (pass to options/input or set on createNeetruClient)"
5090
+ );
5091
+ }
4911
5092
  function createNotificationsNamespace(config) {
4912
5093
  return {
4913
5094
  async send(input) {
4914
5095
  validateInput2(input);
5096
+ const productId = resolveProductId3(config, input.productId);
5097
+ const body = {
5098
+ productId,
5099
+ userId: input.userId,
5100
+ kind: input.kind,
5101
+ title: input.title,
5102
+ severity: input.severity ?? "info"
5103
+ };
5104
+ if (input.body !== void 0) body.body = input.body;
5105
+ if (input.link !== void 0) body.link = input.link;
5106
+ if (input.metadata !== void 0) body.metadata = input.metadata;
5107
+ if (input.fingerprint !== void 0) body.fingerprint = input.fingerprint;
4915
5108
  const raw = await httpRequest(config, {
4916
5109
  method: "POST",
4917
5110
  path: "/api/sdk/v1/notifications",
4918
- body: input,
4919
- requireAuth: true
5111
+ body,
5112
+ requireAuth: true,
5113
+ idempotencyKey: true
4920
5114
  });
4921
5115
  return toNotification(raw);
4922
5116
  },
4923
5117
  async list(userId, options) {
4924
5118
  if (!userId) throw new NeetruError("validation_failed", "userId obrigat\xF3rio");
4925
- const params = new URLSearchParams();
4926
- if (options?.includeDismissed) params.set("includeDismissed", "true");
4927
- if (options?.onlyUnread) params.set("onlyUnread", "true");
5119
+ const productId = resolveProductId3(config, options?.productId);
5120
+ const query = {
5121
+ productId
5122
+ };
5123
+ if (options?.includeDismissed) query.includeDismissed = true;
5124
+ if (options?.onlyUnread) query.onlyUnread = true;
4928
5125
  if (options?.limit) {
4929
- params.set("limit", Math.min(Math.max(1, options.limit), 200).toString());
5126
+ query.limit = Math.min(Math.max(1, options.limit), 200);
4930
5127
  }
4931
- const qs = params.toString();
4932
5128
  const raw = await httpRequest(config, {
4933
5129
  method: "GET",
4934
- path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}${qs ? `?${qs}` : ""}`,
5130
+ path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}`,
5131
+ query,
4935
5132
  requireAuth: true
4936
5133
  });
4937
5134
  const list = Array.isArray(raw?.notifications) ? raw.notifications : [];
4938
5135
  return list.map(toNotification);
4939
5136
  },
4940
- async markRead(id) {
5137
+ async markRead(id, options) {
4941
5138
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
5139
+ const productId = resolveProductId3(config, options?.productId);
4942
5140
  await httpRequest(config, {
4943
5141
  method: "POST",
4944
5142
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/read`,
5143
+ query: { productId },
4945
5144
  requireAuth: true
4946
5145
  });
4947
5146
  return { ok: true };
4948
5147
  },
4949
- async dismiss(id) {
5148
+ async dismiss(id, options) {
4950
5149
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
5150
+ const productId = resolveProductId3(config, options?.productId);
4951
5151
  await httpRequest(config, {
4952
5152
  method: "POST",
4953
5153
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/dismiss`,
5154
+ query: { productId },
4954
5155
  requireAuth: true
4955
5156
  });
4956
5157
  return { ok: true };
@@ -4991,7 +5192,7 @@ var MockNotifications = class {
4991
5192
  const limit = Math.min(Math.max(1, options?.limit ?? 50), 200);
4992
5193
  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);
4993
5194
  }
4994
- async markRead(id) {
5195
+ async markRead(id, _options) {
4995
5196
  const n = this.notifications.get(id);
4996
5197
  if (!n) throw new NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
4997
5198
  if (!n.readAt) {
@@ -5000,7 +5201,7 @@ var MockNotifications = class {
5000
5201
  }
5001
5202
  return { ok: true };
5002
5203
  }
5003
- async dismiss(id) {
5204
+ async dismiss(id, _options) {
5004
5205
  const n = this.notifications.get(id);
5005
5206
  if (!n) throw new NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
5006
5207
  if (!n.dismissedAt) {
@@ -5012,6 +5213,7 @@ var MockNotifications = class {
5012
5213
  };
5013
5214
 
5014
5215
  // src/mocks.ts
5216
+ init_errors();
5015
5217
  var DEV_FIXTURE_USER = Object.freeze({
5016
5218
  uid: "dev-fixture-uid-0001",
5017
5219
  email: "dev@neetru.local",
@@ -5119,39 +5321,25 @@ var MockAuth = class {
5119
5321
  }
5120
5322
  };
5121
5323
  var MockUsage = class {
5122
- _records = [];
5324
+ _reports = [];
5123
5325
  _quotas;
5124
- /** v0.3 — counters in-memory pra `report()` / `check()`. */
5125
5326
  _counters = /* @__PURE__ */ new Map();
5126
5327
  constructor(initialQuotas = {}) {
5127
5328
  this._quotas = new Map(Object.entries(initialQuotas));
5128
5329
  }
5129
- async track(event, properties) {
5130
- this._records.push({
5131
- event,
5132
- properties,
5133
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5134
- });
5135
- return { ok: true };
5136
- }
5137
- async getQuota(metric) {
5138
- const existing = this._quotas.get(metric);
5139
- if (existing) return existing;
5140
- return {
5141
- metric,
5142
- used: this._records.filter((r) => r.event === metric).length,
5143
- limit: -1,
5144
- // unlimited em mock por default
5145
- plan: "mock"
5146
- };
5147
- }
5148
- /** v0.3 — Mock report incrementa o counter local. */
5149
- async report(resource, qty = 1, _options) {
5330
+ async report(resource, qty = 1, options) {
5150
5331
  if (!resource) throw new Error("resource required");
5151
5332
  const safeQty = Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1;
5152
5333
  const existing = this._counters.get(resource) ?? 0;
5153
5334
  const next = existing + safeQty;
5154
5335
  this._counters.set(resource, next);
5336
+ this._reports.push({
5337
+ resource,
5338
+ qty: safeQty,
5339
+ productId: options?.productId,
5340
+ tenantId: options?.tenantId,
5341
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5342
+ });
5155
5343
  const limitFixture = this._quotas.get(resource);
5156
5344
  const limit = limitFixture?.limit ?? -1;
5157
5345
  return {
@@ -5163,12 +5351,11 @@ var MockUsage = class {
5163
5351
  status: limit > 0 && next >= limit ? "read_only_overlimit" : "ok"
5164
5352
  };
5165
5353
  }
5166
- /** v0.3 — Mock check usa quotas + counters in-memory. */
5167
5354
  async check(resource, _options) {
5168
5355
  const quota = this._quotas.get(resource);
5169
5356
  const used = this._counters.get(resource) ?? 0;
5170
5357
  if (!quota) {
5171
- return { allowed: true, reason: "granted", limit: -1, remaining: -1 };
5358
+ return { allowed: true, reason: "granted", limit: -1, remaining: -1, behavior: null };
5172
5359
  }
5173
5360
  const remaining = quota.limit < 0 ? -1 : Math.max(0, quota.limit - used);
5174
5361
  if (remaining === 0) {
@@ -5176,19 +5363,25 @@ var MockUsage = class {
5176
5363
  allowed: false,
5177
5364
  reason: "limit_exceeded",
5178
5365
  limit: quota.limit,
5179
- remaining: 0
5366
+ remaining: 0,
5367
+ behavior: "readonly"
5180
5368
  };
5181
5369
  }
5182
5370
  return {
5183
5371
  allowed: true,
5184
5372
  reason: "granted",
5185
5373
  limit: quota.limit,
5186
- remaining
5374
+ remaining,
5375
+ behavior: null
5187
5376
  };
5188
5377
  }
5189
- /** Test helper. */
5378
+ /** Test helper — retorna histórico de `report()` calls. */
5379
+ __getReports() {
5380
+ return [...this._reports];
5381
+ }
5382
+ /** @deprecated Removido em 3.0 (usage.track foi removido). Use `__getReports`. */
5190
5383
  __getRecords() {
5191
- return [...this._records];
5384
+ return this.__getReports();
5192
5385
  }
5193
5386
  /** Test helper — substitui quota fixture. */
5194
5387
  __setQuota(metric, quota) {
@@ -5196,7 +5389,7 @@ var MockUsage = class {
5196
5389
  }
5197
5390
  /** Test helper — limpa estado. */
5198
5391
  __reset() {
5199
- this._records = [];
5392
+ this._reports = [];
5200
5393
  this._quotas.clear();
5201
5394
  this._counters.clear();
5202
5395
  }
@@ -5205,8 +5398,14 @@ var MockSupport = class {
5205
5398
  _tickets = [];
5206
5399
  _ticketSeq = 0;
5207
5400
  async createTicket(input) {
5208
- if (!input?.subject) throw new Error("subject required");
5209
- if (!input?.message) throw new Error("message required");
5401
+ if (!input?.subject) throw new NeetruError("validation_failed", "subject required");
5402
+ if (!input?.message) throw new NeetruError("validation_failed", "message required");
5403
+ if (!input?.productSlug) {
5404
+ throw new NeetruError(
5405
+ "validation_failed",
5406
+ "productSlug required (3.0 breaking \u2014 pass to input or set on createNeetruClient)"
5407
+ );
5408
+ }
5210
5409
  const ticket = {
5211
5410
  id: `mock-ticket-${++this._ticketSeq}`,
5212
5411
  subject: input.subject,
@@ -5219,7 +5418,10 @@ var MockSupport = class {
5219
5418
  this._tickets.push(ticket);
5220
5419
  return ticket;
5221
5420
  }
5222
- async listMyTickets() {
5421
+ async listMyTickets(options) {
5422
+ if (options?.productSlug) {
5423
+ return this._tickets.filter((t) => t.productSlug === options.productSlug);
5424
+ }
5223
5425
  return [...this._tickets];
5224
5426
  }
5225
5427
  /** Test helper. */
@@ -5377,8 +5579,8 @@ function createOidcAuthNamespace(config) {
5377
5579
  url.searchParams.set("scope", scope);
5378
5580
  url.searchParams.set("state", state);
5379
5581
  if (config.apiKey) {
5380
- const clientId = config.apiKey.split("_")[1] ?? config.apiKey;
5381
- url.searchParams.set("client_id", clientId);
5582
+ const { keyId } = parseApiKey(config.apiKey);
5583
+ url.searchParams.set("client_id", keyId);
5382
5584
  }
5383
5585
  globalThis.location.assign(url.toString());
5384
5586
  return;
@@ -5392,14 +5594,30 @@ function createOidcAuthNamespace(config) {
5392
5594
  const storage = getStorage();
5393
5595
  if (storage) storage.removeItem(STORAGE_KEY);
5394
5596
  cachedUser = null;
5395
- try {
5396
- const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5397
- const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5398
- await config.fetch(`${idpOrigin}/api/v1/oauth/revoke`, {
5399
- method: "POST",
5400
- headers: { "content-type": "application/json" }
5401
- });
5402
- } catch {
5597
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5598
+ const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5599
+ const revokeUrl = `${idpOrigin}/api/v1/oauth/revoke`;
5600
+ let revokeError = null;
5601
+ for (let attempt = 0; attempt < 2; attempt++) {
5602
+ try {
5603
+ await config.fetch(revokeUrl, {
5604
+ method: "POST",
5605
+ headers: { "content-type": "application/json" }
5606
+ });
5607
+ revokeError = null;
5608
+ break;
5609
+ } catch (err) {
5610
+ revokeError = err;
5611
+ if (attempt === 0) {
5612
+ await new Promise((resolve) => setTimeout(resolve, 500));
5613
+ }
5614
+ }
5615
+ }
5616
+ if (revokeError && typeof console !== "undefined") {
5617
+ console.warn(
5618
+ "[neetru-sdk] signOut: server-side revoke falhou (2 tentativas). Refresh token pode estar stale.",
5619
+ revokeError
5620
+ );
5403
5621
  }
5404
5622
  notify();
5405
5623
  },
@@ -5425,7 +5643,8 @@ function createOidcAuthNamespace(config) {
5425
5643
  const issuer = idpOrigin;
5426
5644
  const jwksUrl = `${idpOrigin}/api/v1/oauth/.well-known/jwks.json`;
5427
5645
  const cacheTtlMs = options?.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
5428
- const audience = config.apiKey ? config.apiKey.split("_")[1] ?? void 0 : void 0;
5646
+ const parsedKey = tryParseApiKey(config.apiKey);
5647
+ const audience = parsedKey?.keyId;
5429
5648
  const jwks = _getJwks(jwksUrl, cacheTtlMs);
5430
5649
  try {
5431
5650
  const { payload } = await jwtVerify(token, jwks, {
@@ -5465,7 +5684,8 @@ function createNeetruClient(config = {}) {
5465
5684
  "fetch is not available in this runtime. Pass `fetch` explicitly to createNeetruClient."
5466
5685
  );
5467
5686
  }
5468
- const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
5687
+ const rawBaseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
5688
+ const baseUrl = validateBaseUrl(rawBaseUrl, config.allowedHosts ?? []);
5469
5689
  const apiKey = config.apiKey ?? readEnvApiKey();
5470
5690
  const env = resolveEnv(config.env);
5471
5691
  const resolved = Object.freeze({