@neetru/sdk 2.3.5 → 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 +184 -0
  2. package/README.md +59 -22
  3. package/dist/auth.cjs +368 -142
  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 +368 -142
  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 +36 -5
  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 +36 -5
  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 +432 -152
  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 +432 -152
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/mocks.cjs +49 -37
  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 +49 -37
  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",
@@ -450,7 +475,10 @@ function parseLease(raw) {
450
475
  };
451
476
  }
452
477
  async function createPgPool(lease) {
453
- const { Pool } = await import('pg');
478
+ const { Pool } = await import(
479
+ /* webpackIgnore: true */
480
+ 'pg'
481
+ );
454
482
  const ssl = lease.sslca ? {
455
483
  ca: lease.sslca,
456
484
  cert: lease.clientCert ?? void 0,
@@ -470,7 +498,10 @@ async function createPgPool(lease) {
470
498
  });
471
499
  }
472
500
  async function createDrizzleHandle(pool, schema) {
473
- const { drizzle } = await import('drizzle-orm/node-postgres');
501
+ const { drizzle } = await import(
502
+ /* webpackIgnore: true */
503
+ 'drizzle-orm/node-postgres'
504
+ );
474
505
  return drizzle(pool, { schema });
475
506
  }
476
507
  async function createSqlClientWithDeps(deps, options) {
@@ -570,6 +601,109 @@ var init_sql_client = __esm({
570
601
  // src/auth.ts
571
602
  init_errors();
572
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
+
573
707
  // src/types.ts
574
708
  var DEFAULT_BASE_URL = "https://api.neetru.com";
575
709
 
@@ -771,7 +905,7 @@ function createTelemetryNamespace(config) {
771
905
  if (ev.timestamp) body.timestamp = ev.timestamp;
772
906
  await httpRequest(config, {
773
907
  method: "POST",
774
- path: "/api/v1/sdk/telemetry/event",
908
+ path: "/api/sdk/v1/telemetry/event",
775
909
  body,
776
910
  requireAuth: true,
777
911
  retries: 1
@@ -820,7 +954,7 @@ function createTelemetryNamespace(config) {
820
954
  if (input.timestamp) body.timestamp = input.timestamp;
821
955
  const raw = await httpRequest(config, {
822
956
  method: "POST",
823
- path: "/api/v1/sdk/telemetry/event",
957
+ path: "/api/sdk/v1/telemetry/event",
824
958
  body,
825
959
  requireAuth: true
826
960
  });
@@ -932,65 +1066,33 @@ function createTelemetryNamespace(config) {
932
1066
  // src/usage.ts
933
1067
  init_errors();
934
1068
  init_http();
935
- function toQuota(metric, raw) {
936
- if (!raw || typeof raw !== "object") {
937
- throw new exports.NeetruError("invalid_response", "Quota response is not an object");
938
- }
939
- const r = raw;
940
- if (typeof r.used !== "number" || typeof r.limit !== "number") {
941
- 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
+ );
942
1079
  }
943
- return {
944
- metric: typeof r.metric === "string" ? r.metric : metric,
945
- used: r.used,
946
- limit: r.limit,
947
- resetsAt: typeof r.resetsAt === "string" ? r.resetsAt : void 0,
948
- plan: typeof r.plan === "string" ? r.plan : void 0
949
- };
950
1080
  }
1081
+
1082
+ // src/usage.ts
951
1083
  function createUsageNamespace(config) {
952
1084
  return {
953
1085
  /**
954
- * Persiste um evento de usage. Em dev (mocks ativos via factory) só loga.
955
- * Em workspace/prod chama POST /sdk/v1/usage/record.
956
- */
957
- async track(event, properties) {
958
- if (!event || typeof event !== "string") {
959
- throw new exports.NeetruError("validation_failed", "event name is required");
960
- }
961
- if (event.length > 128) {
962
- throw new exports.NeetruError("validation_failed", "event name max 128 chars");
963
- }
964
- const body = { event };
965
- if (properties && typeof properties === "object") body.properties = properties;
966
- const raw = await httpRequest(config, {
967
- method: "POST",
968
- path: "/sdk/v1/usage/record",
969
- body,
970
- requireAuth: true
971
- });
972
- if (!raw || raw.ok !== true) {
973
- throw new exports.NeetruError("invalid_response", "Usage record response missing ok");
974
- }
975
- return { ok: true };
976
- },
977
- /**
978
- * Lê quota atual de uma métrica. Cacheável (caller decide), SDK não cacheia.
979
- */
980
- async getQuota(metric) {
981
- if (!metric || typeof metric !== "string") {
982
- throw new exports.NeetruError("validation_failed", "metric is required");
983
- }
984
- const raw = await httpRequest(config, {
985
- method: "GET",
986
- path: "/sdk/v1/usage/quota",
987
- query: { metric },
988
- requireAuth: true
989
- });
990
- return toQuota(metric, raw);
991
- },
992
- /**
993
- * 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
+ * ```
994
1096
  */
995
1097
  async report(resource, qty = 1, options) {
996
1098
  if (!resource || typeof resource !== "string") {
@@ -1013,11 +1115,14 @@ function createUsageNamespace(config) {
1013
1115
  "tenantId required (pass to options or set on createNeetruClient)"
1014
1116
  );
1015
1117
  }
1118
+ assertValidSdkId("productId", productId);
1119
+ assertValidSdkId("tenantId", tenantId);
1016
1120
  const raw = await httpRequest(config, {
1017
1121
  method: "POST",
1018
- path: "/sdk/v1/usage/record",
1122
+ path: "/api/sdk/v1/usage/record",
1019
1123
  body: { productId, tenantId, resource, qty: Math.floor(qty) },
1020
- requireAuth: true
1124
+ requireAuth: true,
1125
+ idempotencyKey: true
1021
1126
  });
1022
1127
  if (!raw || raw.ok !== true) {
1023
1128
  throw new exports.NeetruError("invalid_response", "usage.report response missing ok");
@@ -1032,7 +1137,10 @@ function createUsageNamespace(config) {
1032
1137
  };
1033
1138
  },
1034
1139
  /**
1035
- * 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`.
1036
1144
  */
1037
1145
  async check(resource, options) {
1038
1146
  if (!resource || typeof resource !== "string") {
@@ -1043,25 +1151,29 @@ function createUsageNamespace(config) {
1043
1151
  if (!productId || !tenantId) {
1044
1152
  throw new exports.NeetruError(
1045
1153
  "validation_failed",
1046
- "productId and tenantId required"
1154
+ "productId and tenantId required (pass to options or set on createNeetruClient)"
1047
1155
  );
1048
1156
  }
1157
+ assertValidSdkId("productId", productId);
1158
+ assertValidSdkId("tenantId", tenantId);
1049
1159
  const raw = await httpRequest(config, {
1050
1160
  method: "GET",
1051
- path: "/sdk/v1/entitlements",
1161
+ path: "/api/sdk/v1/entitlements",
1052
1162
  query: { productId, tenantId, feature: resource },
1053
1163
  requireAuth: true
1054
1164
  });
1055
1165
  if (!raw || typeof raw.allowed !== "boolean") {
1056
1166
  throw new exports.NeetruError("invalid_response", "usage.check response missing allowed");
1057
1167
  }
1168
+ const behavior = raw.behavior ?? (raw.reason === "limit_exceeded" ? "readonly" : null);
1058
1169
  return {
1059
1170
  allowed: raw.allowed,
1060
1171
  reason: raw.reason,
1061
1172
  remaining: raw.remaining,
1062
1173
  limit: raw.limit,
1063
1174
  planId: raw.planId,
1064
- planFeatures: raw.planFeatures
1175
+ planFeatures: raw.planFeatures,
1176
+ behavior
1065
1177
  };
1066
1178
  }
1067
1179
  };
@@ -1088,14 +1200,32 @@ function toTicket(raw) {
1088
1200
  status: VALID_STATUSES.includes(r.status) ? r.status : "open",
1089
1201
  createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
1090
1202
  updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
1091
- 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
1092
1204
  };
1093
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
+ }
1094
1222
  function createSupportNamespace(config) {
1095
1223
  return {
1096
1224
  /**
1097
- * Cria um ticket de suporte. Requer Bearer auth. Se `productSlug` não é
1098
- * 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`.
1099
1229
  */
1100
1230
  async createTicket(input) {
1101
1231
  if (!input || typeof input !== "object") {
@@ -1114,30 +1244,42 @@ function createSupportNamespace(config) {
1114
1244
  throw new exports.NeetruError("validation_failed", "message max 10000 chars");
1115
1245
  }
1116
1246
  if (input.severity && !VALID_SEVERITIES.includes(input.severity)) {
1117
- 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
+ );
1118
1251
  }
1119
- const slug = input.productSlug ?? "_default";
1252
+ const productId = resolveProductId(config, input.productSlug);
1120
1253
  const body = {
1254
+ productId,
1121
1255
  subject: input.subject,
1122
1256
  message: input.message,
1123
1257
  severity: input.severity ?? "normal"
1124
1258
  };
1259
+ if (input.metadata && typeof input.metadata === "object") {
1260
+ body.metadata = input.metadata;
1261
+ }
1125
1262
  const raw = await httpRequest(config, {
1126
1263
  method: "POST",
1127
- path: `/api/v1/products/${encodeURIComponent(slug)}/tickets`,
1264
+ path: "/api/sdk/v1/support/tickets",
1128
1265
  body,
1129
- requireAuth: true
1266
+ requireAuth: true,
1267
+ idempotencyKey: true
1130
1268
  });
1131
1269
  const candidate = raw && typeof raw === "object" && "ticket" in raw ? raw.ticket : raw;
1132
1270
  return toTicket(candidate);
1133
1271
  },
1134
1272
  /**
1135
- * 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=...`.
1136
1276
  */
1137
- async listMyTickets() {
1277
+ async listMyTickets(options) {
1278
+ const productId = resolveProductId(config, options?.productSlug);
1138
1279
  const raw = await httpRequest(config, {
1139
1280
  method: "GET",
1140
- path: "/api/v1/products/_default/tickets",
1281
+ path: "/api/sdk/v1/support/tickets",
1282
+ query: { productId },
1141
1283
  requireAuth: true
1142
1284
  });
1143
1285
  const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
@@ -4715,6 +4857,52 @@ function createCheckoutNamespace(config) {
4715
4857
  // src/webhooks.ts
4716
4858
  init_errors();
4717
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
+ }
4718
4906
  var VALID_EVENTS = [
4719
4907
  "subscription.activated",
4720
4908
  "subscription.cancelled",
@@ -4770,42 +4958,71 @@ function validateInput(input) {
4770
4958
  throw new exports.NeetruError("validation_failed", "secret deve ter \u226516 chars (recomendado 32+)");
4771
4959
  }
4772
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
+ }
4773
4977
  function createWebhooksNamespace(config) {
4774
4978
  return {
4775
4979
  async register(input) {
4776
4980
  validateInput(input);
4981
+ const productId = resolveProductId2(config, input.productId);
4777
4982
  const raw = await httpRequest(config, {
4778
4983
  method: "POST",
4779
4984
  path: "/api/sdk/v1/webhooks",
4780
- body: input,
4985
+ body: {
4986
+ productId,
4987
+ url: input.url,
4988
+ events: input.events,
4989
+ ...input.secret ? { secret: input.secret } : {}
4990
+ },
4781
4991
  requireAuth: true
4782
4992
  });
4783
4993
  return toEndpoint(raw);
4784
4994
  },
4785
- async list() {
4995
+ async list(options) {
4996
+ const productId = resolveProductId2(config, options?.productId);
4786
4997
  const raw = await httpRequest(config, {
4787
4998
  method: "GET",
4788
4999
  path: "/api/sdk/v1/webhooks",
5000
+ query: { productId },
4789
5001
  requireAuth: true
4790
5002
  });
4791
5003
  const list = Array.isArray(raw?.endpoints) ? raw.endpoints : [];
4792
5004
  return list.map(toEndpoint);
4793
5005
  },
4794
- async unregister(id) {
5006
+ async unregister(id, options) {
4795
5007
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5008
+ const productId = resolveProductId2(config, options?.productId);
4796
5009
  await httpRequest(config, {
4797
5010
  method: "DELETE",
4798
5011
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}`,
5012
+ query: { productId },
4799
5013
  requireAuth: true
4800
5014
  });
4801
5015
  return { ok: true };
4802
5016
  },
4803
- async test(id) {
5017
+ async test(id, options) {
4804
5018
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5019
+ const productId = resolveProductId2(config, options?.productId);
4805
5020
  const raw = await httpRequest(config, {
4806
5021
  method: "POST",
4807
5022
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}/test`,
4808
- requireAuth: true
5023
+ query: { productId },
5024
+ requireAuth: true,
5025
+ idempotencyKey: true
4809
5026
  });
4810
5027
  if (!raw || typeof raw !== "object") {
4811
5028
  return { ok: false, error: "invalid response" };
@@ -4837,17 +5054,17 @@ var MockWebhooks = class {
4837
5054
  this.endpoints.set(id, endpoint);
4838
5055
  return endpoint;
4839
5056
  }
4840
- async list() {
5057
+ async list(_options) {
4841
5058
  return [...this.endpoints.values()];
4842
5059
  }
4843
- async unregister(id) {
5060
+ async unregister(id, _options) {
4844
5061
  if (!this.endpoints.has(id)) {
4845
5062
  throw new exports.NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4846
5063
  }
4847
5064
  this.endpoints.delete(id);
4848
5065
  return { ok: true };
4849
5066
  }
4850
- async test(id) {
5067
+ async test(id, _options) {
4851
5068
  if (!this.endpoints.has(id)) {
4852
5069
  throw new exports.NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4853
5070
  }
@@ -4904,49 +5121,85 @@ function validateInput2(input) {
4904
5121
  throw new exports.NeetruError("validation_failed", "body m\xE1x 2000 chars");
4905
5122
  }
4906
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
+ }
4907
5140
  function createNotificationsNamespace(config) {
4908
5141
  return {
4909
5142
  async send(input) {
4910
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;
4911
5156
  const raw = await httpRequest(config, {
4912
5157
  method: "POST",
4913
5158
  path: "/api/sdk/v1/notifications",
4914
- body: input,
4915
- requireAuth: true
5159
+ body,
5160
+ requireAuth: true,
5161
+ idempotencyKey: true
4916
5162
  });
4917
5163
  return toNotification(raw);
4918
5164
  },
4919
5165
  async list(userId, options) {
4920
5166
  if (!userId) throw new exports.NeetruError("validation_failed", "userId obrigat\xF3rio");
4921
- const params = new URLSearchParams();
4922
- if (options?.includeDismissed) params.set("includeDismissed", "true");
4923
- 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;
4924
5173
  if (options?.limit) {
4925
- params.set("limit", Math.min(Math.max(1, options.limit), 200).toString());
5174
+ query.limit = Math.min(Math.max(1, options.limit), 200);
4926
5175
  }
4927
- const qs = params.toString();
4928
5176
  const raw = await httpRequest(config, {
4929
5177
  method: "GET",
4930
- path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}${qs ? `?${qs}` : ""}`,
5178
+ path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}`,
5179
+ query,
4931
5180
  requireAuth: true
4932
5181
  });
4933
5182
  const list = Array.isArray(raw?.notifications) ? raw.notifications : [];
4934
5183
  return list.map(toNotification);
4935
5184
  },
4936
- async markRead(id) {
5185
+ async markRead(id, options) {
4937
5186
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5187
+ const productId = resolveProductId3(config, options?.productId);
4938
5188
  await httpRequest(config, {
4939
5189
  method: "POST",
4940
5190
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/read`,
5191
+ query: { productId },
4941
5192
  requireAuth: true
4942
5193
  });
4943
5194
  return { ok: true };
4944
5195
  },
4945
- async dismiss(id) {
5196
+ async dismiss(id, options) {
4946
5197
  if (!id) throw new exports.NeetruError("validation_failed", "id obrigat\xF3rio");
5198
+ const productId = resolveProductId3(config, options?.productId);
4947
5199
  await httpRequest(config, {
4948
5200
  method: "POST",
4949
5201
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/dismiss`,
5202
+ query: { productId },
4950
5203
  requireAuth: true
4951
5204
  });
4952
5205
  return { ok: true };
@@ -4987,7 +5240,7 @@ var MockNotifications = class {
4987
5240
  const limit = Math.min(Math.max(1, options?.limit ?? 50), 200);
4988
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);
4989
5242
  }
4990
- async markRead(id) {
5243
+ async markRead(id, _options) {
4991
5244
  const n = this.notifications.get(id);
4992
5245
  if (!n) throw new exports.NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
4993
5246
  if (!n.readAt) {
@@ -4996,7 +5249,7 @@ var MockNotifications = class {
4996
5249
  }
4997
5250
  return { ok: true };
4998
5251
  }
4999
- async dismiss(id) {
5252
+ async dismiss(id, _options) {
5000
5253
  const n = this.notifications.get(id);
5001
5254
  if (!n) throw new exports.NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
5002
5255
  if (!n.dismissedAt) {
@@ -5009,6 +5262,7 @@ var MockNotifications = class {
5009
5262
 
5010
5263
  // src/mocks.ts
5011
5264
  init_db_errors();
5265
+ init_errors();
5012
5266
  var DEV_FIXTURE_USER = Object.freeze({
5013
5267
  uid: "dev-fixture-uid-0001",
5014
5268
  email: "dev@neetru.local",
@@ -5116,39 +5370,25 @@ var MockAuth = class {
5116
5370
  }
5117
5371
  };
5118
5372
  var MockUsage = class {
5119
- _records = [];
5373
+ _reports = [];
5120
5374
  _quotas;
5121
- /** v0.3 — counters in-memory pra `report()` / `check()`. */
5122
5375
  _counters = /* @__PURE__ */ new Map();
5123
5376
  constructor(initialQuotas = {}) {
5124
5377
  this._quotas = new Map(Object.entries(initialQuotas));
5125
5378
  }
5126
- async track(event, properties) {
5127
- this._records.push({
5128
- event,
5129
- properties,
5130
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5131
- });
5132
- return { ok: true };
5133
- }
5134
- async getQuota(metric) {
5135
- const existing = this._quotas.get(metric);
5136
- if (existing) return existing;
5137
- return {
5138
- metric,
5139
- used: this._records.filter((r) => r.event === metric).length,
5140
- limit: -1,
5141
- // unlimited em mock por default
5142
- plan: "mock"
5143
- };
5144
- }
5145
- /** v0.3 — Mock report incrementa o counter local. */
5146
- async report(resource, qty = 1, _options) {
5379
+ async report(resource, qty = 1, options) {
5147
5380
  if (!resource) throw new Error("resource required");
5148
5381
  const safeQty = Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1;
5149
5382
  const existing = this._counters.get(resource) ?? 0;
5150
5383
  const next = existing + safeQty;
5151
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
+ });
5152
5392
  const limitFixture = this._quotas.get(resource);
5153
5393
  const limit = limitFixture?.limit ?? -1;
5154
5394
  return {
@@ -5160,12 +5400,11 @@ var MockUsage = class {
5160
5400
  status: limit > 0 && next >= limit ? "read_only_overlimit" : "ok"
5161
5401
  };
5162
5402
  }
5163
- /** v0.3 — Mock check usa quotas + counters in-memory. */
5164
5403
  async check(resource, _options) {
5165
5404
  const quota = this._quotas.get(resource);
5166
5405
  const used = this._counters.get(resource) ?? 0;
5167
5406
  if (!quota) {
5168
- return { allowed: true, reason: "granted", limit: -1, remaining: -1 };
5407
+ return { allowed: true, reason: "granted", limit: -1, remaining: -1, behavior: null };
5169
5408
  }
5170
5409
  const remaining = quota.limit < 0 ? -1 : Math.max(0, quota.limit - used);
5171
5410
  if (remaining === 0) {
@@ -5173,19 +5412,25 @@ var MockUsage = class {
5173
5412
  allowed: false,
5174
5413
  reason: "limit_exceeded",
5175
5414
  limit: quota.limit,
5176
- remaining: 0
5415
+ remaining: 0,
5416
+ behavior: "readonly"
5177
5417
  };
5178
5418
  }
5179
5419
  return {
5180
5420
  allowed: true,
5181
5421
  reason: "granted",
5182
5422
  limit: quota.limit,
5183
- remaining
5423
+ remaining,
5424
+ behavior: null
5184
5425
  };
5185
5426
  }
5186
- /** 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`. */
5187
5432
  __getRecords() {
5188
- return [...this._records];
5433
+ return this.__getReports();
5189
5434
  }
5190
5435
  /** Test helper — substitui quota fixture. */
5191
5436
  __setQuota(metric, quota) {
@@ -5193,7 +5438,7 @@ var MockUsage = class {
5193
5438
  }
5194
5439
  /** Test helper — limpa estado. */
5195
5440
  __reset() {
5196
- this._records = [];
5441
+ this._reports = [];
5197
5442
  this._quotas.clear();
5198
5443
  this._counters.clear();
5199
5444
  }
@@ -5202,8 +5447,14 @@ var MockSupport = class {
5202
5447
  _tickets = [];
5203
5448
  _ticketSeq = 0;
5204
5449
  async createTicket(input) {
5205
- if (!input?.subject) throw new Error("subject required");
5206
- 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
+ }
5207
5458
  const ticket = {
5208
5459
  id: `mock-ticket-${++this._ticketSeq}`,
5209
5460
  subject: input.subject,
@@ -5216,7 +5467,10 @@ var MockSupport = class {
5216
5467
  this._tickets.push(ticket);
5217
5468
  return ticket;
5218
5469
  }
5219
- async listMyTickets() {
5470
+ async listMyTickets(options) {
5471
+ if (options?.productSlug) {
5472
+ return this._tickets.filter((t) => t.productSlug === options.productSlug);
5473
+ }
5220
5474
  return [...this._tickets];
5221
5475
  }
5222
5476
  /** Test helper. */
@@ -5459,8 +5713,14 @@ var MockDb = class {
5459
5713
  async sql(schema, options) {
5460
5714
  const ddls = options?.initSql ? Array.isArray(options.initSql) ? options.initSql : [options.initSql] : [];
5461
5715
  try {
5462
- const pglitePkg = await import('@electric-sql/pglite');
5463
- const drizzlePglitePkg = await import('drizzle-orm/pglite');
5716
+ const pglitePkg = await import(
5717
+ /* webpackIgnore: true */
5718
+ '@electric-sql/pglite'
5719
+ );
5720
+ const drizzlePglitePkg = await import(
5721
+ /* webpackIgnore: true */
5722
+ 'drizzle-orm/pglite'
5723
+ );
5464
5724
  const pglite = await pglitePkg.PGlite.create();
5465
5725
  for (const ddl of ddls) {
5466
5726
  await pglite.query(ddl);
@@ -5481,8 +5741,14 @@ var MockDb = class {
5481
5741
  let pgMemMod;
5482
5742
  let drizzlePg;
5483
5743
  try {
5484
- pgMemMod = await import('pg-mem');
5485
- drizzlePg = await import('drizzle-orm/node-postgres');
5744
+ pgMemMod = await import(
5745
+ /* webpackIgnore: true */
5746
+ 'pg-mem'
5747
+ );
5748
+ drizzlePg = await import(
5749
+ /* webpackIgnore: true */
5750
+ 'drizzle-orm/node-postgres'
5751
+ );
5486
5752
  } catch {
5487
5753
  throw new exports.NeetruDbError(
5488
5754
  "db_unavailable",
@@ -5673,8 +5939,8 @@ function createOidcAuthNamespace(config) {
5673
5939
  url.searchParams.set("scope", scope);
5674
5940
  url.searchParams.set("state", state);
5675
5941
  if (config.apiKey) {
5676
- const clientId = config.apiKey.split("_")[1] ?? config.apiKey;
5677
- url.searchParams.set("client_id", clientId);
5942
+ const { keyId } = parseApiKey(config.apiKey);
5943
+ url.searchParams.set("client_id", keyId);
5678
5944
  }
5679
5945
  globalThis.location.assign(url.toString());
5680
5946
  return;
@@ -5688,14 +5954,30 @@ function createOidcAuthNamespace(config) {
5688
5954
  const storage = getStorage();
5689
5955
  if (storage) storage.removeItem(STORAGE_KEY);
5690
5956
  cachedUser = null;
5691
- try {
5692
- const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5693
- const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5694
- await config.fetch(`${idpOrigin}/api/v1/oauth/revoke`, {
5695
- method: "POST",
5696
- headers: { "content-type": "application/json" }
5697
- });
5698
- } 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
+ );
5699
5981
  }
5700
5982
  notify();
5701
5983
  },
@@ -5721,7 +6003,8 @@ function createOidcAuthNamespace(config) {
5721
6003
  const issuer = idpOrigin;
5722
6004
  const jwksUrl = `${idpOrigin}/api/v1/oauth/.well-known/jwks.json`;
5723
6005
  const cacheTtlMs = options?.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
5724
- const audience = config.apiKey ? config.apiKey.split("_")[1] ?? void 0 : void 0;
6006
+ const parsedKey = tryParseApiKey(config.apiKey);
6007
+ const audience = parsedKey?.keyId;
5725
6008
  const jwks = _getJwks(jwksUrl, cacheTtlMs);
5726
6009
  try {
5727
6010
  const { payload } = await jose.jwtVerify(token, jwks, {
@@ -5761,7 +6044,8 @@ function createNeetruClient(config = {}) {
5761
6044
  "fetch is not available in this runtime. Pass `fetch` explicitly to createNeetruClient."
5762
6045
  );
5763
6046
  }
5764
- const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
6047
+ const rawBaseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
6048
+ const baseUrl = validateBaseUrl(rawBaseUrl, config.allowedHosts ?? []);
5765
6049
  const apiKey = config.apiKey ?? readEnvApiKey();
5766
6050
  const env = resolveEnv(config.env);
5767
6051
  const resolved = Object.freeze({
@@ -5799,11 +6083,7 @@ function createNeetruClient(config = {}) {
5799
6083
  // src/index.ts
5800
6084
  init_errors();
5801
6085
  init_db_errors();
5802
- var VERSION = "2.0.0";
5803
- function initNeetru(config) {
5804
- const { apiUrl: _apiUrl, baseUrl, ...rest } = config;
5805
- return createNeetruClient({ ...rest, baseUrl });
5806
- }
6086
+ var VERSION = "3.0.0";
5807
6087
 
5808
6088
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
5809
6089
  exports.DEV_FIXTURE_USER = DEV_FIXTURE_USER;
@@ -5821,6 +6101,6 @@ exports.createNeetruClient = createNeetruClient;
5821
6101
  exports.createNeetruDb = createNeetruDb;
5822
6102
  exports.createNotificationsNamespace = createNotificationsNamespace;
5823
6103
  exports.createWebhooksNamespace = createWebhooksNamespace;
5824
- exports.initNeetru = initNeetru;
6104
+ exports.verifyWebhookSignature = verifyWebhookSignature;
5825
6105
  //# sourceMappingURL=index.cjs.map
5826
6106
  //# sourceMappingURL=index.cjs.map