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