@neetru/sdk 2.3.6 → 3.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 (83) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +59 -22
  3. package/dist/auth.cjs +360 -140
  4. package/dist/auth.cjs.map +1 -1
  5. package/dist/auth.d.cts +1 -1
  6. package/dist/auth.d.ts +1 -1
  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 -1
  12. package/dist/catalog.d.ts +1 -1
  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 -1
  18. package/dist/checkout.d.ts +1 -1
  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 -1
  24. package/dist/db.d.ts +1 -1
  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 -1
  30. package/dist/entitlements.d.ts +1 -1
  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 -19
  40. package/dist/index.d.ts +31 -19
  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 -19
  46. package/dist/mocks.d.ts +23 -19
  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 -1
  52. package/dist/notifications.d.ts +1 -1
  53. package/dist/notifications.mjs +80 -14
  54. package/dist/notifications.mjs.map +1 -1
  55. package/dist/react.d.cts +1 -1
  56. package/dist/react.d.ts +1 -1
  57. package/dist/support.cjs +72 -12
  58. package/dist/support.cjs.map +1 -1
  59. package/dist/support.d.cts +1 -1
  60. package/dist/support.d.ts +1 -1
  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 -1
  66. package/dist/telemetry.d.ts +1 -1
  67. package/dist/telemetry.mjs +23 -4
  68. package/dist/telemetry.mjs.map +1 -1
  69. package/dist/{types-sGGN1vkg.d.cts → types-DLOvkeAP.d.cts} +110 -49
  70. package/dist/{types-CSC-RIdS.d.ts → types-dw19bdID.d.ts} +110 -49
  71. package/dist/usage.cjs +57 -63
  72. package/dist/usage.cjs.map +1 -1
  73. package/dist/usage.d.cts +1 -1
  74. package/dist/usage.d.ts +1 -1
  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 -1
  80. package/dist/webhooks.d.ts +1 -1
  81. package/dist/webhooks.mjs +110 -33
  82. package/dist/webhooks.mjs.map +1 -1
  83. package/package.json +2 -2
package/dist/index.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 exports.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 exports.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 exports.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 exports.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 exports.NeetruError("invalid_config", "baseUrl is required");
645
+ }
646
+ let parsed;
647
+ try {
648
+ parsed = new URL(baseUrl);
649
+ } catch {
650
+ throw new exports.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 exports.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 exports.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 exports.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 exports.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 exports.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 exports.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 exports.NeetruError("validation_failed", "event name is required");
966
- }
967
- if (event.length > 128) {
968
- throw new exports.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 exports.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 exports.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 exports.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 exports.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 exports.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 exports.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 exports.NeetruError("validation_failed", "message max 10000 chars");
1121
1245
  }
1122
1246
  if (input.severity && !VALID_SEVERITIES.includes(input.severity)) {
1123
- throw new exports.NeetruError("validation_failed", `severity must be one of ${VALID_SEVERITIES.join(", ")}`);
1247
+ throw new exports.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 ?? [] : [];
@@ -4721,6 +4857,52 @@ function createCheckoutNamespace(config) {
4721
4857
  // src/webhooks.ts
4722
4858
  init_errors();
4723
4859
  init_http();
4860
+ async function verifyWebhookSignature(payload, signature, secret) {
4861
+ if (!signature || !secret) return false;
4862
+ const sigHex = signature.startsWith("sha256=") ? signature.slice(7) : signature;
4863
+ if (!/^[0-9a-f]+$/i.test(sigHex)) return false;
4864
+ const subtle = typeof globalThis !== "undefined" && "crypto" in globalThis ? globalThis.crypto?.subtle : void 0;
4865
+ if (!subtle) {
4866
+ throw new exports.NeetruError(
4867
+ "runtime_unsupported",
4868
+ "verifyWebhookSignature requires globalThis.crypto.subtle (Node \u226520, modern browser, or Edge runtime)."
4869
+ );
4870
+ }
4871
+ const encoder = new TextEncoder();
4872
+ const keyBytes = encoder.encode(secret);
4873
+ const payloadBytes = typeof payload === "string" ? encoder.encode(payload) : payload;
4874
+ const key = await subtle.importKey(
4875
+ "raw",
4876
+ keyBytes,
4877
+ { name: "HMAC", hash: "SHA-256" },
4878
+ false,
4879
+ ["sign"]
4880
+ );
4881
+ const macBuffer = await subtle.sign(
4882
+ "HMAC",
4883
+ key,
4884
+ payloadBytes
4885
+ );
4886
+ const macBytes = new Uint8Array(macBuffer);
4887
+ const sigBytes = hexToBytes(sigHex.toLowerCase());
4888
+ if (!sigBytes || sigBytes.length !== macBytes.length) return false;
4889
+ let diff = 0;
4890
+ for (let i = 0; i < macBytes.length; i++) {
4891
+ diff |= macBytes[i] ^ sigBytes[i];
4892
+ }
4893
+ return diff === 0;
4894
+ }
4895
+ function hexToBytes(hex) {
4896
+ if (hex.length % 2 !== 0) return null;
4897
+ const bytes = new Uint8Array(hex.length / 2);
4898
+ for (let i = 0; i < bytes.length; i++) {
4899
+ const high = parseInt(hex[i * 2], 16);
4900
+ const low = parseInt(hex[i * 2 + 1], 16);
4901
+ if (Number.isNaN(high) || Number.isNaN(low)) return null;
4902
+ bytes[i] = high << 4 | low;
4903
+ }
4904
+ return bytes;
4905
+ }
4724
4906
  var VALID_EVENTS = [
4725
4907
  "subscription.activated",
4726
4908
  "subscription.cancelled",
@@ -4776,42 +4958,71 @@ function validateInput(input) {
4776
4958
  throw new exports.NeetruError("validation_failed", "secret deve ter \u226516 chars (recomendado 32+)");
4777
4959
  }
4778
4960
  }
4961
+ function resolveProductId2(config, ...candidates) {
4962
+ for (const c of candidates) {
4963
+ if (typeof c === "string" && c.length > 0) {
4964
+ assertValidSdkId("productId", c);
4965
+ return c;
4966
+ }
4967
+ }
4968
+ if (config.productId) {
4969
+ assertValidSdkId("productId", config.productId);
4970
+ return config.productId;
4971
+ }
4972
+ throw new exports.NeetruError(
4973
+ "validation_failed",
4974
+ "productId required (pass to options/input or set on createNeetruClient)"
4975
+ );
4976
+ }
4779
4977
  function createWebhooksNamespace(config) {
4780
4978
  return {
4781
4979
  async register(input) {
4782
4980
  validateInput(input);
4981
+ const productId = resolveProductId2(config, input.productId);
4783
4982
  const raw = await httpRequest(config, {
4784
4983
  method: "POST",
4785
4984
  path: "/api/sdk/v1/webhooks",
4786
- body: input,
4985
+ body: {
4986
+ productId,
4987
+ url: input.url,
4988
+ events: input.events,
4989
+ ...input.secret ? { secret: input.secret } : {}
4990
+ },
4787
4991
  requireAuth: true
4788
4992
  });
4789
4993
  return toEndpoint(raw);
4790
4994
  },
4791
- async list() {
4995
+ async list(options) {
4996
+ const productId = resolveProductId2(config, options?.productId);
4792
4997
  const raw = await httpRequest(config, {
4793
4998
  method: "GET",
4794
4999
  path: "/api/sdk/v1/webhooks",
5000
+ query: { productId },
4795
5001
  requireAuth: true
4796
5002
  });
4797
5003
  const list = Array.isArray(raw?.endpoints) ? raw.endpoints : [];
4798
5004
  return list.map(toEndpoint);
4799
5005
  },
4800
- async unregister(id) {
5006
+ async unregister(id, options) {
4801
5007
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5008
+ const productId = resolveProductId2(config, options?.productId);
4802
5009
  await httpRequest(config, {
4803
5010
  method: "DELETE",
4804
5011
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}`,
5012
+ query: { productId },
4805
5013
  requireAuth: true
4806
5014
  });
4807
5015
  return { ok: true };
4808
5016
  },
4809
- async test(id) {
5017
+ async test(id, options) {
4810
5018
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5019
+ const productId = resolveProductId2(config, options?.productId);
4811
5020
  const raw = await httpRequest(config, {
4812
5021
  method: "POST",
4813
5022
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}/test`,
4814
- requireAuth: true
5023
+ query: { productId },
5024
+ requireAuth: true,
5025
+ idempotencyKey: true
4815
5026
  });
4816
5027
  if (!raw || typeof raw !== "object") {
4817
5028
  return { ok: false, error: "invalid response" };
@@ -4843,17 +5054,17 @@ var MockWebhooks = class {
4843
5054
  this.endpoints.set(id, endpoint);
4844
5055
  return endpoint;
4845
5056
  }
4846
- async list() {
5057
+ async list(_options) {
4847
5058
  return [...this.endpoints.values()];
4848
5059
  }
4849
- async unregister(id) {
5060
+ async unregister(id, _options) {
4850
5061
  if (!this.endpoints.has(id)) {
4851
5062
  throw new exports.NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4852
5063
  }
4853
5064
  this.endpoints.delete(id);
4854
5065
  return { ok: true };
4855
5066
  }
4856
- async test(id) {
5067
+ async test(id, _options) {
4857
5068
  if (!this.endpoints.has(id)) {
4858
5069
  throw new exports.NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4859
5070
  }
@@ -4910,49 +5121,85 @@ function validateInput2(input) {
4910
5121
  throw new exports.NeetruError("validation_failed", "body m\xE1x 2000 chars");
4911
5122
  }
4912
5123
  }
5124
+ function resolveProductId3(config, ...candidates) {
5125
+ for (const c of candidates) {
5126
+ if (typeof c === "string" && c.length > 0) {
5127
+ assertValidSdkId("productId", c);
5128
+ return c;
5129
+ }
5130
+ }
5131
+ if (config.productId) {
5132
+ assertValidSdkId("productId", config.productId);
5133
+ return config.productId;
5134
+ }
5135
+ throw new exports.NeetruError(
5136
+ "validation_failed",
5137
+ "productId required (pass to options/input or set on createNeetruClient)"
5138
+ );
5139
+ }
4913
5140
  function createNotificationsNamespace(config) {
4914
5141
  return {
4915
5142
  async send(input) {
4916
5143
  validateInput2(input);
5144
+ const productId = resolveProductId3(config, input.productId);
5145
+ const body = {
5146
+ productId,
5147
+ userId: input.userId,
5148
+ kind: input.kind,
5149
+ title: input.title,
5150
+ severity: input.severity ?? "info"
5151
+ };
5152
+ if (input.body !== void 0) body.body = input.body;
5153
+ if (input.link !== void 0) body.link = input.link;
5154
+ if (input.metadata !== void 0) body.metadata = input.metadata;
5155
+ if (input.fingerprint !== void 0) body.fingerprint = input.fingerprint;
4917
5156
  const raw = await httpRequest(config, {
4918
5157
  method: "POST",
4919
5158
  path: "/api/sdk/v1/notifications",
4920
- body: input,
4921
- requireAuth: true
5159
+ body,
5160
+ requireAuth: true,
5161
+ idempotencyKey: true
4922
5162
  });
4923
5163
  return toNotification(raw);
4924
5164
  },
4925
5165
  async list(userId, options) {
4926
5166
  if (!userId) throw new exports.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");
5167
+ const productId = resolveProductId3(config, options?.productId);
5168
+ const query = {
5169
+ productId
5170
+ };
5171
+ if (options?.includeDismissed) query.includeDismissed = true;
5172
+ if (options?.onlyUnread) query.onlyUnread = true;
4930
5173
  if (options?.limit) {
4931
- params.set("limit", Math.min(Math.max(1, options.limit), 200).toString());
5174
+ query.limit = Math.min(Math.max(1, options.limit), 200);
4932
5175
  }
4933
- const qs = params.toString();
4934
5176
  const raw = await httpRequest(config, {
4935
5177
  method: "GET",
4936
- path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}${qs ? `?${qs}` : ""}`,
5178
+ path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}`,
5179
+ query,
4937
5180
  requireAuth: true
4938
5181
  });
4939
5182
  const list = Array.isArray(raw?.notifications) ? raw.notifications : [];
4940
5183
  return list.map(toNotification);
4941
5184
  },
4942
- async markRead(id) {
5185
+ async markRead(id, options) {
4943
5186
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5187
+ const productId = resolveProductId3(config, options?.productId);
4944
5188
  await httpRequest(config, {
4945
5189
  method: "POST",
4946
5190
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/read`,
5191
+ query: { productId },
4947
5192
  requireAuth: true
4948
5193
  });
4949
5194
  return { ok: true };
4950
5195
  },
4951
- async dismiss(id) {
5196
+ async dismiss(id, options) {
4952
5197
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5198
+ const productId = resolveProductId3(config, options?.productId);
4953
5199
  await httpRequest(config, {
4954
5200
  method: "POST",
4955
5201
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/dismiss`,
5202
+ query: { productId },
4956
5203
  requireAuth: true
4957
5204
  });
4958
5205
  return { ok: true };
@@ -4993,7 +5240,7 @@ var MockNotifications = class {
4993
5240
  const limit = Math.min(Math.max(1, options?.limit ?? 50), 200);
4994
5241
  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
5242
  }
4996
- async markRead(id) {
5243
+ async markRead(id, _options) {
4997
5244
  const n = this.notifications.get(id);
4998
5245
  if (!n) throw new exports.NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
4999
5246
  if (!n.readAt) {
@@ -5002,7 +5249,7 @@ var MockNotifications = class {
5002
5249
  }
5003
5250
  return { ok: true };
5004
5251
  }
5005
- async dismiss(id) {
5252
+ async dismiss(id, _options) {
5006
5253
  const n = this.notifications.get(id);
5007
5254
  if (!n) throw new exports.NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
5008
5255
  if (!n.dismissedAt) {
@@ -5015,6 +5262,7 @@ var MockNotifications = class {
5015
5262
 
5016
5263
  // src/mocks.ts
5017
5264
  init_db_errors();
5265
+ init_errors();
5018
5266
  var DEV_FIXTURE_USER = Object.freeze({
5019
5267
  uid: "dev-fixture-uid-0001",
5020
5268
  email: "dev@neetru.local",
@@ -5122,39 +5370,25 @@ var MockAuth = class {
5122
5370
  }
5123
5371
  };
5124
5372
  var MockUsage = class {
5125
- _records = [];
5373
+ _reports = [];
5126
5374
  _quotas;
5127
- /** v0.3 — counters in-memory pra `report()` / `check()`. */
5128
5375
  _counters = /* @__PURE__ */ new Map();
5129
5376
  constructor(initialQuotas = {}) {
5130
5377
  this._quotas = new Map(Object.entries(initialQuotas));
5131
5378
  }
5132
- async track(event, properties) {
5133
- this._records.push({
5134
- event,
5135
- properties,
5136
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5137
- });
5138
- return { ok: true };
5139
- }
5140
- async getQuota(metric) {
5141
- const existing = this._quotas.get(metric);
5142
- if (existing) return existing;
5143
- return {
5144
- metric,
5145
- used: this._records.filter((r) => r.event === metric).length,
5146
- limit: -1,
5147
- // unlimited em mock por default
5148
- plan: "mock"
5149
- };
5150
- }
5151
- /** v0.3 — Mock report incrementa o counter local. */
5152
- async report(resource, qty = 1, _options) {
5379
+ async report(resource, qty = 1, options) {
5153
5380
  if (!resource) throw new Error("resource required");
5154
5381
  const safeQty = Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1;
5155
5382
  const existing = this._counters.get(resource) ?? 0;
5156
5383
  const next = existing + safeQty;
5157
5384
  this._counters.set(resource, next);
5385
+ this._reports.push({
5386
+ resource,
5387
+ qty: safeQty,
5388
+ productId: options?.productId,
5389
+ tenantId: options?.tenantId,
5390
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5391
+ });
5158
5392
  const limitFixture = this._quotas.get(resource);
5159
5393
  const limit = limitFixture?.limit ?? -1;
5160
5394
  return {
@@ -5166,12 +5400,11 @@ var MockUsage = class {
5166
5400
  status: limit > 0 && next >= limit ? "read_only_overlimit" : "ok"
5167
5401
  };
5168
5402
  }
5169
- /** v0.3 — Mock check usa quotas + counters in-memory. */
5170
5403
  async check(resource, _options) {
5171
5404
  const quota = this._quotas.get(resource);
5172
5405
  const used = this._counters.get(resource) ?? 0;
5173
5406
  if (!quota) {
5174
- return { allowed: true, reason: "granted", limit: -1, remaining: -1 };
5407
+ return { allowed: true, reason: "granted", limit: -1, remaining: -1, behavior: null };
5175
5408
  }
5176
5409
  const remaining = quota.limit < 0 ? -1 : Math.max(0, quota.limit - used);
5177
5410
  if (remaining === 0) {
@@ -5179,19 +5412,25 @@ var MockUsage = class {
5179
5412
  allowed: false,
5180
5413
  reason: "limit_exceeded",
5181
5414
  limit: quota.limit,
5182
- remaining: 0
5415
+ remaining: 0,
5416
+ behavior: "readonly"
5183
5417
  };
5184
5418
  }
5185
5419
  return {
5186
5420
  allowed: true,
5187
5421
  reason: "granted",
5188
5422
  limit: quota.limit,
5189
- remaining
5423
+ remaining,
5424
+ behavior: null
5190
5425
  };
5191
5426
  }
5192
- /** Test helper. */
5427
+ /** Test helper — retorna histórico de `report()` calls. */
5428
+ __getReports() {
5429
+ return [...this._reports];
5430
+ }
5431
+ /** @deprecated Removido em 3.0 (usage.track foi removido). Use `__getReports`. */
5193
5432
  __getRecords() {
5194
- return [...this._records];
5433
+ return this.__getReports();
5195
5434
  }
5196
5435
  /** Test helper — substitui quota fixture. */
5197
5436
  __setQuota(metric, quota) {
@@ -5199,7 +5438,7 @@ var MockUsage = class {
5199
5438
  }
5200
5439
  /** Test helper — limpa estado. */
5201
5440
  __reset() {
5202
- this._records = [];
5441
+ this._reports = [];
5203
5442
  this._quotas.clear();
5204
5443
  this._counters.clear();
5205
5444
  }
@@ -5208,8 +5447,14 @@ var MockSupport = class {
5208
5447
  _tickets = [];
5209
5448
  _ticketSeq = 0;
5210
5449
  async createTicket(input) {
5211
- if (!input?.subject) throw new Error("subject required");
5212
- if (!input?.message) throw new Error("message required");
5450
+ if (!input?.subject) throw new exports.NeetruError("validation_failed", "subject required");
5451
+ if (!input?.message) throw new exports.NeetruError("validation_failed", "message required");
5452
+ if (!input?.productSlug) {
5453
+ throw new exports.NeetruError(
5454
+ "validation_failed",
5455
+ "productSlug required (3.0 breaking \u2014 pass to input or set on createNeetruClient)"
5456
+ );
5457
+ }
5213
5458
  const ticket = {
5214
5459
  id: `mock-ticket-${++this._ticketSeq}`,
5215
5460
  subject: input.subject,
@@ -5222,7 +5467,10 @@ var MockSupport = class {
5222
5467
  this._tickets.push(ticket);
5223
5468
  return ticket;
5224
5469
  }
5225
- async listMyTickets() {
5470
+ async listMyTickets(options) {
5471
+ if (options?.productSlug) {
5472
+ return this._tickets.filter((t) => t.productSlug === options.productSlug);
5473
+ }
5226
5474
  return [...this._tickets];
5227
5475
  }
5228
5476
  /** Test helper. */
@@ -5691,8 +5939,8 @@ function createOidcAuthNamespace(config) {
5691
5939
  url.searchParams.set("scope", scope);
5692
5940
  url.searchParams.set("state", state);
5693
5941
  if (config.apiKey) {
5694
- const clientId = config.apiKey.split("_")[1] ?? config.apiKey;
5695
- url.searchParams.set("client_id", clientId);
5942
+ const { keyId } = parseApiKey(config.apiKey);
5943
+ url.searchParams.set("client_id", keyId);
5696
5944
  }
5697
5945
  globalThis.location.assign(url.toString());
5698
5946
  return;
@@ -5706,14 +5954,30 @@ function createOidcAuthNamespace(config) {
5706
5954
  const storage = getStorage();
5707
5955
  if (storage) storage.removeItem(STORAGE_KEY);
5708
5956
  cachedUser = null;
5709
- try {
5710
- const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5711
- const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5712
- await config.fetch(`${idpOrigin}/api/v1/oauth/revoke`, {
5713
- method: "POST",
5714
- headers: { "content-type": "application/json" }
5715
- });
5716
- } catch {
5957
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5958
+ const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5959
+ const revokeUrl = `${idpOrigin}/api/v1/oauth/revoke`;
5960
+ let revokeError = null;
5961
+ for (let attempt = 0; attempt < 2; attempt++) {
5962
+ try {
5963
+ await config.fetch(revokeUrl, {
5964
+ method: "POST",
5965
+ headers: { "content-type": "application/json" }
5966
+ });
5967
+ revokeError = null;
5968
+ break;
5969
+ } catch (err) {
5970
+ revokeError = err;
5971
+ if (attempt === 0) {
5972
+ await new Promise((resolve) => setTimeout(resolve, 500));
5973
+ }
5974
+ }
5975
+ }
5976
+ if (revokeError && typeof console !== "undefined") {
5977
+ console.warn(
5978
+ "[neetru-sdk] signOut: server-side revoke falhou (2 tentativas). Refresh token pode estar stale.",
5979
+ revokeError
5980
+ );
5717
5981
  }
5718
5982
  notify();
5719
5983
  },
@@ -5739,7 +6003,8 @@ function createOidcAuthNamespace(config) {
5739
6003
  const issuer = idpOrigin;
5740
6004
  const jwksUrl = `${idpOrigin}/api/v1/oauth/.well-known/jwks.json`;
5741
6005
  const cacheTtlMs = options?.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
5742
- const audience = config.apiKey ? config.apiKey.split("_")[1] ?? void 0 : void 0;
6006
+ const parsedKey = tryParseApiKey(config.apiKey);
6007
+ const audience = parsedKey?.keyId;
5743
6008
  const jwks = _getJwks(jwksUrl, cacheTtlMs);
5744
6009
  try {
5745
6010
  const { payload } = await jose.jwtVerify(token, jwks, {
@@ -5779,7 +6044,8 @@ function createNeetruClient(config = {}) {
5779
6044
  "fetch is not available in this runtime. Pass `fetch` explicitly to createNeetruClient."
5780
6045
  );
5781
6046
  }
5782
- const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
6047
+ const rawBaseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
6048
+ const baseUrl = validateBaseUrl(rawBaseUrl, config.allowedHosts ?? []);
5783
6049
  const apiKey = config.apiKey ?? readEnvApiKey();
5784
6050
  const env = resolveEnv(config.env);
5785
6051
  const resolved = Object.freeze({
@@ -5817,11 +6083,7 @@ function createNeetruClient(config = {}) {
5817
6083
  // src/index.ts
5818
6084
  init_errors();
5819
6085
  init_db_errors();
5820
- var VERSION = "2.0.0";
5821
- function initNeetru(config) {
5822
- const { apiUrl: _apiUrl, baseUrl, ...rest } = config;
5823
- return createNeetruClient({ ...rest, baseUrl });
5824
- }
6086
+ var VERSION = "3.0.0";
5825
6087
 
5826
6088
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
5827
6089
  exports.DEV_FIXTURE_USER = DEV_FIXTURE_USER;
@@ -5839,6 +6101,6 @@ exports.createNeetruClient = createNeetruClient;
5839
6101
  exports.createNeetruDb = createNeetruDb;
5840
6102
  exports.createNotificationsNamespace = createNotificationsNamespace;
5841
6103
  exports.createWebhooksNamespace = createWebhooksNamespace;
5842
- exports.initNeetru = initNeetru;
6104
+ exports.verifyWebhookSignature = verifyWebhookSignature;
5843
6105
  //# sourceMappingURL=index.cjs.map
5844
6106
  //# sourceMappingURL=index.cjs.map