@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/auth.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 ?? [] : [];
@@ -4768,42 +4910,71 @@ function validateInput(input) {
4768
4910
  throw new NeetruError("validation_failed", "secret deve ter \u226516 chars (recomendado 32+)");
4769
4911
  }
4770
4912
  }
4913
+ function resolveProductId2(config, ...candidates) {
4914
+ for (const c of candidates) {
4915
+ if (typeof c === "string" && c.length > 0) {
4916
+ assertValidSdkId("productId", c);
4917
+ return c;
4918
+ }
4919
+ }
4920
+ if (config.productId) {
4921
+ assertValidSdkId("productId", config.productId);
4922
+ return config.productId;
4923
+ }
4924
+ throw new NeetruError(
4925
+ "validation_failed",
4926
+ "productId required (pass to options/input or set on createNeetruClient)"
4927
+ );
4928
+ }
4771
4929
  function createWebhooksNamespace(config) {
4772
4930
  return {
4773
4931
  async register(input) {
4774
4932
  validateInput(input);
4933
+ const productId = resolveProductId2(config, input.productId);
4775
4934
  const raw = await httpRequest(config, {
4776
4935
  method: "POST",
4777
4936
  path: "/api/sdk/v1/webhooks",
4778
- body: input,
4937
+ body: {
4938
+ productId,
4939
+ url: input.url,
4940
+ events: input.events,
4941
+ ...input.secret ? { secret: input.secret } : {}
4942
+ },
4779
4943
  requireAuth: true
4780
4944
  });
4781
4945
  return toEndpoint(raw);
4782
4946
  },
4783
- async list() {
4947
+ async list(options) {
4948
+ const productId = resolveProductId2(config, options?.productId);
4784
4949
  const raw = await httpRequest(config, {
4785
4950
  method: "GET",
4786
4951
  path: "/api/sdk/v1/webhooks",
4952
+ query: { productId },
4787
4953
  requireAuth: true
4788
4954
  });
4789
4955
  const list = Array.isArray(raw?.endpoints) ? raw.endpoints : [];
4790
4956
  return list.map(toEndpoint);
4791
4957
  },
4792
- async unregister(id) {
4958
+ async unregister(id, options) {
4793
4959
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
4960
+ const productId = resolveProductId2(config, options?.productId);
4794
4961
  await httpRequest(config, {
4795
4962
  method: "DELETE",
4796
4963
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}`,
4964
+ query: { productId },
4797
4965
  requireAuth: true
4798
4966
  });
4799
4967
  return { ok: true };
4800
4968
  },
4801
- async test(id) {
4969
+ async test(id, options) {
4802
4970
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
4971
+ const productId = resolveProductId2(config, options?.productId);
4803
4972
  const raw = await httpRequest(config, {
4804
4973
  method: "POST",
4805
4974
  path: `/api/sdk/v1/webhooks/${encodeURIComponent(id)}/test`,
4806
- requireAuth: true
4975
+ query: { productId },
4976
+ requireAuth: true,
4977
+ idempotencyKey: true
4807
4978
  });
4808
4979
  if (!raw || typeof raw !== "object") {
4809
4980
  return { ok: false, error: "invalid response" };
@@ -4835,17 +5006,17 @@ var MockWebhooks = class {
4835
5006
  this.endpoints.set(id, endpoint);
4836
5007
  return endpoint;
4837
5008
  }
4838
- async list() {
5009
+ async list(_options) {
4839
5010
  return [...this.endpoints.values()];
4840
5011
  }
4841
- async unregister(id) {
5012
+ async unregister(id, _options) {
4842
5013
  if (!this.endpoints.has(id)) {
4843
5014
  throw new NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4844
5015
  }
4845
5016
  this.endpoints.delete(id);
4846
5017
  return { ok: true };
4847
5018
  }
4848
- async test(id) {
5019
+ async test(id, _options) {
4849
5020
  if (!this.endpoints.has(id)) {
4850
5021
  throw new NeetruError("not_found", `Webhook ${id} n\xE3o encontrado`);
4851
5022
  }
@@ -4902,49 +5073,85 @@ function validateInput2(input) {
4902
5073
  throw new NeetruError("validation_failed", "body m\xE1x 2000 chars");
4903
5074
  }
4904
5075
  }
5076
+ function resolveProductId3(config, ...candidates) {
5077
+ for (const c of candidates) {
5078
+ if (typeof c === "string" && c.length > 0) {
5079
+ assertValidSdkId("productId", c);
5080
+ return c;
5081
+ }
5082
+ }
5083
+ if (config.productId) {
5084
+ assertValidSdkId("productId", config.productId);
5085
+ return config.productId;
5086
+ }
5087
+ throw new NeetruError(
5088
+ "validation_failed",
5089
+ "productId required (pass to options/input or set on createNeetruClient)"
5090
+ );
5091
+ }
4905
5092
  function createNotificationsNamespace(config) {
4906
5093
  return {
4907
5094
  async send(input) {
4908
5095
  validateInput2(input);
5096
+ const productId = resolveProductId3(config, input.productId);
5097
+ const body = {
5098
+ productId,
5099
+ userId: input.userId,
5100
+ kind: input.kind,
5101
+ title: input.title,
5102
+ severity: input.severity ?? "info"
5103
+ };
5104
+ if (input.body !== void 0) body.body = input.body;
5105
+ if (input.link !== void 0) body.link = input.link;
5106
+ if (input.metadata !== void 0) body.metadata = input.metadata;
5107
+ if (input.fingerprint !== void 0) body.fingerprint = input.fingerprint;
4909
5108
  const raw = await httpRequest(config, {
4910
5109
  method: "POST",
4911
5110
  path: "/api/sdk/v1/notifications",
4912
- body: input,
4913
- requireAuth: true
5111
+ body,
5112
+ requireAuth: true,
5113
+ idempotencyKey: true
4914
5114
  });
4915
5115
  return toNotification(raw);
4916
5116
  },
4917
5117
  async list(userId, options) {
4918
5118
  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");
5119
+ const productId = resolveProductId3(config, options?.productId);
5120
+ const query = {
5121
+ productId
5122
+ };
5123
+ if (options?.includeDismissed) query.includeDismissed = true;
5124
+ if (options?.onlyUnread) query.onlyUnread = true;
4922
5125
  if (options?.limit) {
4923
- params.set("limit", Math.min(Math.max(1, options.limit), 200).toString());
5126
+ query.limit = Math.min(Math.max(1, options.limit), 200);
4924
5127
  }
4925
- const qs = params.toString();
4926
5128
  const raw = await httpRequest(config, {
4927
5129
  method: "GET",
4928
- path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}${qs ? `?${qs}` : ""}`,
5130
+ path: `/api/sdk/v1/notifications/user/${encodeURIComponent(userId)}`,
5131
+ query,
4929
5132
  requireAuth: true
4930
5133
  });
4931
5134
  const list = Array.isArray(raw?.notifications) ? raw.notifications : [];
4932
5135
  return list.map(toNotification);
4933
5136
  },
4934
- async markRead(id) {
5137
+ async markRead(id, options) {
4935
5138
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
5139
+ const productId = resolveProductId3(config, options?.productId);
4936
5140
  await httpRequest(config, {
4937
5141
  method: "POST",
4938
5142
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/read`,
5143
+ query: { productId },
4939
5144
  requireAuth: true
4940
5145
  });
4941
5146
  return { ok: true };
4942
5147
  },
4943
- async dismiss(id) {
5148
+ async dismiss(id, options) {
4944
5149
  if (!id) throw new NeetruError("validation_failed", "id obrigat\xF3rio");
5150
+ const productId = resolveProductId3(config, options?.productId);
4945
5151
  await httpRequest(config, {
4946
5152
  method: "POST",
4947
5153
  path: `/api/sdk/v1/notifications/${encodeURIComponent(id)}/dismiss`,
5154
+ query: { productId },
4948
5155
  requireAuth: true
4949
5156
  });
4950
5157
  return { ok: true };
@@ -4985,7 +5192,7 @@ var MockNotifications = class {
4985
5192
  const limit = Math.min(Math.max(1, options?.limit ?? 50), 200);
4986
5193
  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
5194
  }
4988
- async markRead(id) {
5195
+ async markRead(id, _options) {
4989
5196
  const n = this.notifications.get(id);
4990
5197
  if (!n) throw new NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
4991
5198
  if (!n.readAt) {
@@ -4994,7 +5201,7 @@ var MockNotifications = class {
4994
5201
  }
4995
5202
  return { ok: true };
4996
5203
  }
4997
- async dismiss(id) {
5204
+ async dismiss(id, _options) {
4998
5205
  const n = this.notifications.get(id);
4999
5206
  if (!n) throw new NeetruError("not_found", `Notification ${id} n\xE3o encontrada`);
5000
5207
  if (!n.dismissedAt) {
@@ -5006,6 +5213,7 @@ var MockNotifications = class {
5006
5213
  };
5007
5214
 
5008
5215
  // src/mocks.ts
5216
+ init_errors();
5009
5217
  var DEV_FIXTURE_USER = Object.freeze({
5010
5218
  uid: "dev-fixture-uid-0001",
5011
5219
  email: "dev@neetru.local",
@@ -5113,39 +5321,25 @@ var MockAuth = class {
5113
5321
  }
5114
5322
  };
5115
5323
  var MockUsage = class {
5116
- _records = [];
5324
+ _reports = [];
5117
5325
  _quotas;
5118
- /** v0.3 — counters in-memory pra `report()` / `check()`. */
5119
5326
  _counters = /* @__PURE__ */ new Map();
5120
5327
  constructor(initialQuotas = {}) {
5121
5328
  this._quotas = new Map(Object.entries(initialQuotas));
5122
5329
  }
5123
- async track(event, properties) {
5124
- this._records.push({
5125
- event,
5126
- properties,
5127
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5128
- });
5129
- return { ok: true };
5130
- }
5131
- async getQuota(metric) {
5132
- const existing = this._quotas.get(metric);
5133
- if (existing) return existing;
5134
- return {
5135
- metric,
5136
- used: this._records.filter((r) => r.event === metric).length,
5137
- limit: -1,
5138
- // unlimited em mock por default
5139
- plan: "mock"
5140
- };
5141
- }
5142
- /** v0.3 — Mock report incrementa o counter local. */
5143
- async report(resource, qty = 1, _options) {
5330
+ async report(resource, qty = 1, options) {
5144
5331
  if (!resource) throw new Error("resource required");
5145
5332
  const safeQty = Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1;
5146
5333
  const existing = this._counters.get(resource) ?? 0;
5147
5334
  const next = existing + safeQty;
5148
5335
  this._counters.set(resource, next);
5336
+ this._reports.push({
5337
+ resource,
5338
+ qty: safeQty,
5339
+ productId: options?.productId,
5340
+ tenantId: options?.tenantId,
5341
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5342
+ });
5149
5343
  const limitFixture = this._quotas.get(resource);
5150
5344
  const limit = limitFixture?.limit ?? -1;
5151
5345
  return {
@@ -5157,12 +5351,11 @@ var MockUsage = class {
5157
5351
  status: limit > 0 && next >= limit ? "read_only_overlimit" : "ok"
5158
5352
  };
5159
5353
  }
5160
- /** v0.3 — Mock check usa quotas + counters in-memory. */
5161
5354
  async check(resource, _options) {
5162
5355
  const quota = this._quotas.get(resource);
5163
5356
  const used = this._counters.get(resource) ?? 0;
5164
5357
  if (!quota) {
5165
- return { allowed: true, reason: "granted", limit: -1, remaining: -1 };
5358
+ return { allowed: true, reason: "granted", limit: -1, remaining: -1, behavior: null };
5166
5359
  }
5167
5360
  const remaining = quota.limit < 0 ? -1 : Math.max(0, quota.limit - used);
5168
5361
  if (remaining === 0) {
@@ -5170,19 +5363,25 @@ var MockUsage = class {
5170
5363
  allowed: false,
5171
5364
  reason: "limit_exceeded",
5172
5365
  limit: quota.limit,
5173
- remaining: 0
5366
+ remaining: 0,
5367
+ behavior: "readonly"
5174
5368
  };
5175
5369
  }
5176
5370
  return {
5177
5371
  allowed: true,
5178
5372
  reason: "granted",
5179
5373
  limit: quota.limit,
5180
- remaining
5374
+ remaining,
5375
+ behavior: null
5181
5376
  };
5182
5377
  }
5183
- /** Test helper. */
5378
+ /** Test helper — retorna histórico de `report()` calls. */
5379
+ __getReports() {
5380
+ return [...this._reports];
5381
+ }
5382
+ /** @deprecated Removido em 3.0 (usage.track foi removido). Use `__getReports`. */
5184
5383
  __getRecords() {
5185
- return [...this._records];
5384
+ return this.__getReports();
5186
5385
  }
5187
5386
  /** Test helper — substitui quota fixture. */
5188
5387
  __setQuota(metric, quota) {
@@ -5190,7 +5389,7 @@ var MockUsage = class {
5190
5389
  }
5191
5390
  /** Test helper — limpa estado. */
5192
5391
  __reset() {
5193
- this._records = [];
5392
+ this._reports = [];
5194
5393
  this._quotas.clear();
5195
5394
  this._counters.clear();
5196
5395
  }
@@ -5199,8 +5398,14 @@ var MockSupport = class {
5199
5398
  _tickets = [];
5200
5399
  _ticketSeq = 0;
5201
5400
  async createTicket(input) {
5202
- if (!input?.subject) throw new Error("subject required");
5203
- if (!input?.message) throw new Error("message required");
5401
+ if (!input?.subject) throw new NeetruError("validation_failed", "subject required");
5402
+ if (!input?.message) throw new NeetruError("validation_failed", "message required");
5403
+ if (!input?.productSlug) {
5404
+ throw new NeetruError(
5405
+ "validation_failed",
5406
+ "productSlug required (3.0 breaking \u2014 pass to input or set on createNeetruClient)"
5407
+ );
5408
+ }
5204
5409
  const ticket = {
5205
5410
  id: `mock-ticket-${++this._ticketSeq}`,
5206
5411
  subject: input.subject,
@@ -5213,7 +5418,10 @@ var MockSupport = class {
5213
5418
  this._tickets.push(ticket);
5214
5419
  return ticket;
5215
5420
  }
5216
- async listMyTickets() {
5421
+ async listMyTickets(options) {
5422
+ if (options?.productSlug) {
5423
+ return this._tickets.filter((t) => t.productSlug === options.productSlug);
5424
+ }
5217
5425
  return [...this._tickets];
5218
5426
  }
5219
5427
  /** Test helper. */
@@ -5371,8 +5579,8 @@ function createOidcAuthNamespace(config) {
5371
5579
  url.searchParams.set("scope", scope);
5372
5580
  url.searchParams.set("state", state);
5373
5581
  if (config.apiKey) {
5374
- const clientId = config.apiKey.split("_")[1] ?? config.apiKey;
5375
- url.searchParams.set("client_id", clientId);
5582
+ const { keyId } = parseApiKey(config.apiKey);
5583
+ url.searchParams.set("client_id", keyId);
5376
5584
  }
5377
5585
  globalThis.location.assign(url.toString());
5378
5586
  return;
@@ -5386,14 +5594,30 @@ function createOidcAuthNamespace(config) {
5386
5594
  const storage = getStorage();
5387
5595
  if (storage) storage.removeItem(STORAGE_KEY);
5388
5596
  cachedUser = null;
5389
- try {
5390
- const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5391
- const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5392
- await config.fetch(`${idpOrigin}/api/v1/oauth/revoke`, {
5393
- method: "POST",
5394
- headers: { "content-type": "application/json" }
5395
- });
5396
- } catch {
5597
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5598
+ const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5599
+ const revokeUrl = `${idpOrigin}/api/v1/oauth/revoke`;
5600
+ let revokeError = null;
5601
+ for (let attempt = 0; attempt < 2; attempt++) {
5602
+ try {
5603
+ await config.fetch(revokeUrl, {
5604
+ method: "POST",
5605
+ headers: { "content-type": "application/json" }
5606
+ });
5607
+ revokeError = null;
5608
+ break;
5609
+ } catch (err) {
5610
+ revokeError = err;
5611
+ if (attempt === 0) {
5612
+ await new Promise((resolve) => setTimeout(resolve, 500));
5613
+ }
5614
+ }
5615
+ }
5616
+ if (revokeError && typeof console !== "undefined") {
5617
+ console.warn(
5618
+ "[neetru-sdk] signOut: server-side revoke falhou (2 tentativas). Refresh token pode estar stale.",
5619
+ revokeError
5620
+ );
5397
5621
  }
5398
5622
  notify();
5399
5623
  },
@@ -5419,7 +5643,8 @@ function createOidcAuthNamespace(config) {
5419
5643
  const issuer = idpOrigin;
5420
5644
  const jwksUrl = `${idpOrigin}/api/v1/oauth/.well-known/jwks.json`;
5421
5645
  const cacheTtlMs = options?.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
5422
- const audience = config.apiKey ? config.apiKey.split("_")[1] ?? void 0 : void 0;
5646
+ const parsedKey = tryParseApiKey(config.apiKey);
5647
+ const audience = parsedKey?.keyId;
5423
5648
  const jwks = _getJwks(jwksUrl, cacheTtlMs);
5424
5649
  try {
5425
5650
  const { payload } = await jwtVerify(token, jwks, {
@@ -5459,7 +5684,8 @@ function createNeetruClient(config = {}) {
5459
5684
  "fetch is not available in this runtime. Pass `fetch` explicitly to createNeetruClient."
5460
5685
  );
5461
5686
  }
5462
- const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
5687
+ const rawBaseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
5688
+ const baseUrl = validateBaseUrl(rawBaseUrl, config.allowedHosts ?? []);
5463
5689
  const apiKey = config.apiKey ?? readEnvApiKey();
5464
5690
  const env = resolveEnv(config.env);
5465
5691
  const resolved = Object.freeze({