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