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