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