@nexushub/client 0.9.0 → 1.1.1

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.
package/dist/index.cjs CHANGED
@@ -31,27 +31,32 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/config.ts
34
- var DEFAULT_API_URL, DEFAULT_ANALYTICS_URL, LOCAL_NEST_URL, LOCAL_RUST_URL, getEnvConfig, mergeConfigs, getFullConfig, validateConfig;
34
+ var DEFAULT_API_URL, DEFAULT_ANALYTICS_URL, SDK_VERSION, LOCAL_NEST_URL, LOCAL_RUST_URL, getEnvConfig, mergeConfigs, getFullConfig, validateConfig, hasRequiredConfig;
35
35
  var init_config = __esm({
36
36
  "src/config.ts"() {
37
37
  "use strict";
38
38
  DEFAULT_API_URL = "https://api.gnapex.com";
39
39
  DEFAULT_ANALYTICS_URL = "https://sentry.gnapex.com";
40
+ SDK_VERSION = "1.1.0";
40
41
  LOCAL_NEST_URL = "https://api.gnapex.com";
41
42
  LOCAL_RUST_URL = "https://sentry.gnapex.com";
42
43
  getEnvConfig = () => {
43
- const NEXT_PUBLIC_ID = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_ID : void 0;
44
- const NEXT_PUBLIC_KEY = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_KEY : void 0;
45
- const NEXT_PUBLIC_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_API_URL : void 0;
46
- const NODE_ID = typeof process !== "undefined" ? process.env.NEXUS_PROJECT_ID : void 0;
47
- const NODE_KEY = typeof process !== "undefined" ? process.env.NEXUS_API_KEY : void 0;
48
- const NODE_URL = typeof process !== "undefined" ? process.env.NEXUS_API_URL : void 0;
44
+ const NEXT_PUBLIC_ID = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_ID ?? process.env.NEXT_PUBLIC_NEXUS_ID : void 0;
45
+ const NEXT_PUBLIC_KEY = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_KEY ?? process.env.NEXT_PUBLIC_NEXUS_KEY : void 0;
46
+ const NEXT_PUBLIC_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_API_URL ?? process.env.NEXT_PUBLIC_NEXUS_API_URL : void 0;
47
+ const NEXT_PUBLIC_ANALYTICS_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_ANALYTICS_URL ?? process.env.NEXT_PUBLIC_NEXUS_ANALYTICS_URL : void 0;
48
+ const NODE_ID = typeof process !== "undefined" ? process.env.GNAPEX_PROJECT_ID ?? process.env.NEXUS_PROJECT_ID : void 0;
49
+ const NODE_KEY = typeof process !== "undefined" ? process.env.GNAPEX_API_KEY ?? process.env.NEXUS_API_KEY : void 0;
50
+ const NODE_URL = typeof process !== "undefined" ? process.env.GNAPEX_API_URL ?? process.env.NEXUS_API_URL : void 0;
51
+ const NODE_ANALYTICS_URL = typeof process !== "undefined" ? process.env.GNAPEX_ANALYTICS_URL ?? process.env.NEXUS_ANALYTICS_URL : void 0;
49
52
  const NODE_ENV = typeof process !== "undefined" ? process.env.NODE_ENV : "production";
50
53
  const isDev = NODE_ENV === "development" || typeof window !== "undefined" && window.location.hostname === "localhost";
51
- const projectId = NEXT_PUBLIC_ID ?? NODE_ID;
52
- const apiKey = NEXT_PUBLIC_KEY ?? NODE_KEY;
53
- const apiUrl = NEXT_PUBLIC_URL ?? NODE_URL ?? (isDev ? LOCAL_NEST_URL : DEFAULT_API_URL);
54
- const analyticsUrl = apiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL;
54
+ const runtime = typeof window !== "undefined" ? window.__GNAPEX__ ?? window.__NEXUS__ : void 0;
55
+ const meta = typeof document !== "undefined" ? (name) => document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ?? void 0 : (_name) => void 0;
56
+ const projectId = runtime?.projectId ?? meta("gnapex-project") ?? meta("nexus-project") ?? NEXT_PUBLIC_ID ?? NODE_ID;
57
+ const apiKey = runtime?.apiKey ?? meta("gnapex-key") ?? meta("nexus-key") ?? NEXT_PUBLIC_KEY ?? NODE_KEY;
58
+ const apiUrl = runtime?.apiUrl ?? meta("gnapex-api") ?? meta("nexus-api") ?? NEXT_PUBLIC_URL ?? NODE_URL ?? (isDev ? LOCAL_NEST_URL : DEFAULT_API_URL);
59
+ const analyticsUrl = runtime?.analyticsUrl ?? meta("gnapex-analytics") ?? meta("nexus-analytics") ?? NEXT_PUBLIC_ANALYTICS_URL ?? NODE_ANALYTICS_URL ?? (apiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL);
55
60
  return {
56
61
  projectId,
57
62
  apiKey,
@@ -79,8 +84,12 @@ var init_config = __esm({
79
84
  const errors = [];
80
85
  if (!config.projectId) errors.push("Missing projectId");
81
86
  if (!config.apiUrl) errors.push("Missing apiUrl");
87
+ else if (!/^https?:\/\//.test(config.apiUrl)) {
88
+ errors.push("apiUrl must be a valid URL starting with http:// or https://");
89
+ }
82
90
  return errors;
83
91
  };
92
+ hasRequiredConfig = (config) => validateConfig(config).length === 0;
84
93
  }
85
94
  });
86
95
 
@@ -188,6 +197,61 @@ var init_binary_compiler = __esm({
188
197
  }
189
198
  });
190
199
 
200
+ // src/content/utils.ts
201
+ function validateSlug(slug) {
202
+ if (!slug || typeof slug !== "string") {
203
+ throw new Error("Slug must be a non-empty string");
204
+ }
205
+ if (!/^[a-z0-9-_]+$/.test(slug)) {
206
+ throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
207
+ }
208
+ }
209
+ function normalizeQuery(query) {
210
+ const normalized = { ...query };
211
+ normalized.page = Math.max(1, normalized.page || 1);
212
+ normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
213
+ normalized.order = normalized.order || "desc";
214
+ if (normalized.page < 1) {
215
+ throw new Error("Page must be greater than 0");
216
+ }
217
+ if (normalized.limit < 1 || normalized.limit > 100) {
218
+ throw new Error("Limit must be between 1 and 100");
219
+ }
220
+ return normalized;
221
+ }
222
+ function buildQueryString(query) {
223
+ const params = new URLSearchParams();
224
+ if (query.page) params.append("page", query.page.toString());
225
+ if (query.limit) params.append("limit", query.limit.toString());
226
+ if (query.sort) params.append("sort", query.sort);
227
+ if (query.order) params.append("order", query.order);
228
+ if (query.search) params.append("search", query.search);
229
+ if (query.include?.length) {
230
+ params.append("include", query.include.join(","));
231
+ }
232
+ if (query.fields?.length) {
233
+ params.append("fields", query.fields.join(","));
234
+ }
235
+ if (query.filter) {
236
+ params.append("filter", JSON.stringify(query.filter));
237
+ }
238
+ return params.toString();
239
+ }
240
+ function measurePerformance(name, fn) {
241
+ const start = performance.now();
242
+ const result = fn();
243
+ const end = performance.now();
244
+ if (process.env.NODE_ENV === "development") {
245
+ console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
246
+ }
247
+ return { result, duration: end - start };
248
+ }
249
+ var init_utils = __esm({
250
+ "src/content/utils.ts"() {
251
+ "use strict";
252
+ }
253
+ });
254
+
191
255
  // src/content/local-cache-server.ts
192
256
  var local_cache_server_exports = {};
193
257
  __export(local_cache_server_exports, {
@@ -201,6 +265,7 @@ var init_local_cache_server = __esm({
201
265
  import_path = __toESM(require("path"), 1);
202
266
  init_config();
203
267
  init_binary_compiler();
268
+ init_utils();
204
269
  LocalCache = class {
205
270
  constructor(customPath, apiKey) {
206
271
  this.apiKey = "";
@@ -211,9 +276,6 @@ var init_local_cache_server = __esm({
211
276
  isLoaded() {
212
277
  return import_fs.default.existsSync(import_path.default.resolve(process.cwd(), this.baseDir));
213
278
  }
214
- /**
215
- * Helper to decrypt and decompile `.nx` files on-the-fly.
216
- */
217
279
  decompileFile(filePath) {
218
280
  if (!this.apiKey) {
219
281
  if (process.env.NODE_ENV === "development") {
@@ -242,9 +304,14 @@ var init_local_cache_server = __esm({
242
304
  }
243
305
  }
244
306
  /**
245
- * Retrieve a specific page directly from its .nx file.
307
+ * Retrieve a specific page safely from its .nx file.
246
308
  */
247
309
  async getPage(slug) {
310
+ try {
311
+ validateSlug(slug);
312
+ } catch {
313
+ return null;
314
+ }
248
315
  const filePath = import_path.default.resolve(
249
316
  process.cwd(),
250
317
  this.baseDir,
@@ -263,9 +330,14 @@ var init_local_cache_server = __esm({
263
330
  return null;
264
331
  }
265
332
  /**
266
- * Retrieve an entire collection from its specific .nx file.
333
+ * Retrieve an entire collection safely from its specific .nx file.
267
334
  */
268
335
  async getCollection(collectionId) {
336
+ try {
337
+ validateSlug(collectionId);
338
+ } catch {
339
+ return null;
340
+ }
269
341
  const filePath = import_path.default.resolve(
270
342
  process.cwd(),
271
343
  this.baseDir,
@@ -398,15 +470,25 @@ __export(index_exports, {
398
470
  ContentEngine: () => ContentEngine,
399
471
  DEFAULT_ANALYTICS_URL: () => DEFAULT_ANALYTICS_URL,
400
472
  DEFAULT_API_URL: () => DEFAULT_API_URL,
473
+ FeatureFlags: () => FeatureFlags,
401
474
  LOCAL_NEST_URL: () => LOCAL_NEST_URL,
402
475
  LOCAL_RUST_URL: () => LOCAL_RUST_URL,
403
476
  LocalCache: () => LocalCacheProxy,
404
477
  MemoryCache: () => MemoryCache,
405
478
  NexusClient: () => NexusClient,
479
+ NexusError: () => NexusError,
480
+ NexusEventBus: () => NexusEventBus,
481
+ NexusHttpClient: () => NexusHttpClient,
482
+ NexusPushClient: () => NexusPushClient,
483
+ RemoteConfig: () => RemoteConfig,
484
+ SDK_VERSION: () => SDK_VERSION,
406
485
  VERSION: () => VERSION,
407
486
  createNexusClient: () => createNexusClient,
487
+ getDiagnostics: () => getDiagnostics,
408
488
  getEnvConfig: () => getEnvConfig,
409
489
  getFullConfig: () => getFullConfig,
490
+ hasRequiredConfig: () => hasRequiredConfig,
491
+ isNexusError: () => isNexusError,
410
492
  mergeConfigs: () => mergeConfigs,
411
493
  nexus: () => nexus,
412
494
  validateConfig: () => validateConfig
@@ -715,90 +797,69 @@ var LocalCacheProxy = class {
715
797
  // src/content/strategies.ts
716
798
  var RateLimiter = class {
717
799
  constructor(config) {
718
- this.requests = [];
719
800
  this.config = config;
801
+ this.requests = [];
720
802
  }
721
803
  async checkLimit() {
722
804
  while (true) {
723
805
  const now = Date.now();
724
- const windowStart = now - this.config.timeWindow;
725
- this.requests = this.requests.filter((time) => time > windowStart);
806
+ const cutoff = now - this.config.timeWindow;
807
+ this.requests = this.requests.filter((t) => t > cutoff);
726
808
  if (this.requests.length < this.config.maxRequests) {
727
809
  this.requests.push(now);
728
810
  return;
729
811
  }
730
- const oldestRequest = this.requests[0];
731
- const waitTime = oldestRequest + this.config.timeWindow - now;
732
- if (waitTime > 0) {
733
- await new Promise((resolve) => setTimeout(resolve, waitTime));
734
- } else {
735
- }
812
+ await new Promise((r) => setTimeout(r, Math.max(1, this.requests[0] + this.config.timeWindow - now)));
736
813
  }
737
814
  }
738
815
  getStats() {
739
816
  const now = Date.now();
740
- const windowStart = now - this.config.timeWindow;
741
- const currentRequests = this.requests.filter(
742
- (time) => time > windowStart
743
- ).length;
744
- return { currentRequests, limit: this.config.maxRequests };
817
+ return { currentRequests: this.requests.filter((t) => t > now - this.config.timeWindow).length, limit: this.config.maxRequests };
745
818
  }
746
819
  };
747
820
  var ExponentialBackoff = class {
748
821
  constructor(config) {
822
+ this.config = config;
749
823
  this.config = { jitter: true, ...config };
750
824
  }
751
825
  async execute(fn, onRetry) {
752
- let lastError = new Error("Unknown error");
753
- for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
826
+ let last;
827
+ for (let a = 0; a <= this.config.maxRetries; a++) {
754
828
  try {
755
829
  return await fn();
756
- } catch (error) {
757
- lastError = error;
758
- if (this.isClientError(error) && !this.isRateLimitError(error)) {
759
- throw error;
760
- }
761
- if (attempt === this.config.maxRetries) break;
762
- const delay = this.calculateDelay(attempt);
763
- if (onRetry) onRetry(attempt + 1, delay, error);
764
- await new Promise((resolve) => setTimeout(resolve, delay));
830
+ } catch (e) {
831
+ last = e;
832
+ if (e?.code === "ABORTED" || e?.code === "VALIDATION_ERROR" || e?.status >= 400 && e?.status < 500 && e?.status !== 429) throw e;
833
+ if (a === this.config.maxRetries) break;
834
+ let delay = Math.min(this.config.maxDelay, this.config.baseDelay * 2 ** a);
835
+ if (this.config.jitter) delay *= 0.5 + Math.random();
836
+ onRetry?.(a + 1, delay, e);
837
+ await new Promise((r) => setTimeout(r, delay));
765
838
  }
766
839
  }
767
- throw lastError;
768
- }
769
- calculateDelay(attempt) {
770
- let delay = this.config.baseDelay * Math.pow(2, attempt);
771
- delay = Math.min(delay, this.config.maxDelay);
772
- if (this.config.jitter) {
773
- delay = delay * (0.5 + Math.random());
774
- }
775
- return delay;
776
- }
777
- isClientError(error) {
778
- return error?.status >= 400 && error?.status < 500;
779
- }
780
- isRateLimitError(error) {
781
- return error?.status === 429;
840
+ throw last;
782
841
  }
783
842
  };
843
+ var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
844
+ CircuitState2[CircuitState2["CLOSED"] = 0] = "CLOSED";
845
+ CircuitState2[CircuitState2["OPEN"] = 1] = "OPEN";
846
+ CircuitState2[CircuitState2["HALF_OPEN"] = 2] = "HALF_OPEN";
847
+ return CircuitState2;
848
+ })(CircuitState || {});
784
849
  var CircuitBreaker = class {
785
- constructor() {
850
+ constructor(threshold = 5, reset = 3e4) {
851
+ this.threshold = threshold;
852
+ this.reset = reset;
786
853
  this.state = 0 /* CLOSED */;
787
854
  this.failures = 0;
788
- this.lastFailureTime = 0;
789
- this.failureThreshold = 5;
790
- this.resetTimeout = 3e4;
855
+ this.lastFailure = 0;
791
856
  }
792
- // 30 seconds
793
857
  isOpen() {
794
- if (this.state === 1 /* OPEN */) {
795
- if (Date.now() - this.lastFailureTime > this.resetTimeout) {
796
- this.state = 2 /* HALF_OPEN */;
797
- return false;
798
- }
799
- return true;
858
+ if (this.state === 1 /* OPEN */ && Date.now() - this.lastFailure >= this.reset) {
859
+ this.state = 2 /* HALF_OPEN */;
860
+ return false;
800
861
  }
801
- return false;
862
+ return this.state === 1 /* OPEN */;
802
863
  }
803
864
  recordSuccess() {
804
865
  this.failures = 0;
@@ -806,111 +867,35 @@ var CircuitBreaker = class {
806
867
  }
807
868
  recordFailure() {
808
869
  this.failures++;
809
- this.lastFailureTime = Date.now();
810
- if (this.failures >= this.failureThreshold) {
811
- this.state = 1 /* OPEN */;
812
- if (process.env.NODE_ENV === "development") {
813
- console.warn(
814
- "[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests."
815
- );
816
- }
817
- }
870
+ this.lastFailure = Date.now();
871
+ if (this.failures >= this.threshold) this.state = 1 /* OPEN */;
872
+ }
873
+ getState() {
874
+ return CircuitState[this.state];
818
875
  }
819
876
  };
820
877
  var RequestBatcher = class {
821
- constructor(batchWindow = 10, maxBatchSize = 20) {
822
- this.batchWindow = batchWindow;
878
+ constructor(windowMs = 10, maxBatchSize = 50) {
879
+ this.windowMs = windowMs;
823
880
  this.maxBatchSize = maxBatchSize;
824
- this.batch = [];
825
- this.processing = false;
826
- }
827
- async schedule(key, request) {
828
- return new Promise((resolve, reject) => {
829
- this.batch.push({ key, resolve, reject });
830
- if (this.batch.length >= this.maxBatchSize) {
831
- this.processBatch(request);
832
- } else if (!this.batchTimeout) {
833
- this.batchTimeout = setTimeout(
834
- () => this.processBatch(request),
835
- this.batchWindow
836
- );
837
- }
881
+ this.pending = /* @__PURE__ */ new Map();
882
+ }
883
+ schedule(key, request) {
884
+ const existing = this.pending.get(key);
885
+ if (existing) return existing;
886
+ const promise = new Promise((resolve, reject) => setTimeout(() => request().then(resolve, reject), this.windowMs));
887
+ this.pending.set(key, promise);
888
+ promise.finally(() => this.pending.delete(key)).catch(() => {
838
889
  });
890
+ return promise;
839
891
  }
840
- async processBatch(request) {
841
- if (this.processing || this.batch.length === 0) return;
842
- this.processing = true;
843
- if (this.batchTimeout) {
844
- clearTimeout(this.batchTimeout);
845
- this.batchTimeout = void 0;
846
- }
847
- const currentBatch = [...this.batch];
848
- this.batch = [];
849
- try {
850
- const result = await request();
851
- currentBatch.forEach((item) => item.resolve(result));
852
- } catch (error) {
853
- currentBatch.forEach((item) => item.reject(error));
854
- } finally {
855
- this.processing = false;
856
- if (this.batch.length > 0) {
857
- setTimeout(() => this.processBatch(request), 0);
858
- }
859
- }
892
+ clear() {
893
+ this.pending.clear();
860
894
  }
861
895
  };
862
896
 
863
- // src/content/utils.ts
864
- function validateSlug(slug) {
865
- if (!slug || typeof slug !== "string") {
866
- throw new Error("Slug must be a non-empty string");
867
- }
868
- if (!/^[a-z0-9-_]+$/.test(slug)) {
869
- throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
870
- }
871
- }
872
- function normalizeQuery(query) {
873
- const normalized = { ...query };
874
- normalized.page = Math.max(1, normalized.page || 1);
875
- normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
876
- normalized.order = normalized.order || "desc";
877
- if (normalized.page < 1) {
878
- throw new Error("Page must be greater than 0");
879
- }
880
- if (normalized.limit < 1 || normalized.limit > 100) {
881
- throw new Error("Limit must be between 1 and 100");
882
- }
883
- return normalized;
884
- }
885
- function buildQueryString(query) {
886
- const params = new URLSearchParams();
887
- if (query.page) params.append("page", query.page.toString());
888
- if (query.limit) params.append("limit", query.limit.toString());
889
- if (query.sort) params.append("sort", query.sort);
890
- if (query.order) params.append("order", query.order);
891
- if (query.search) params.append("search", query.search);
892
- if (query.include?.length) {
893
- params.append("include", query.include.join(","));
894
- }
895
- if (query.fields?.length) {
896
- params.append("fields", query.fields.join(","));
897
- }
898
- if (query.filter) {
899
- params.append("filter", JSON.stringify(query.filter));
900
- }
901
- return params.toString();
902
- }
903
- function measurePerformance(name, fn) {
904
- const start = performance.now();
905
- const result = fn();
906
- const end = performance.now();
907
- if (process.env.NODE_ENV === "development") {
908
- console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
909
- }
910
- return { result, duration: end - start };
911
- }
912
-
913
897
  // src/content/index.ts
898
+ init_utils();
914
899
  var ContentEngine = class {
915
900
  constructor(config) {
916
901
  this.config = config;
@@ -942,6 +927,12 @@ var ContentEngine = class {
942
927
  window.addEventListener("beforeunload", this.cleanup.bind(this));
943
928
  }
944
929
  }
930
+ /** Update runtime configuration without exposing internal mutation. */
931
+ updateConfig(config) {
932
+ this.config = config;
933
+ this.defaultRevalidate = config.revalidateTime ?? false;
934
+ this.cacheStrategy = config.cacheStrategy || "memory";
935
+ }
945
936
  /**
946
937
  * Fetch a Single Page with full strategy pipeline
947
938
  */
@@ -1100,7 +1091,7 @@ var ContentEngine = class {
1100
1091
  }
1101
1092
  }
1102
1093
  /**
1103
- * Fetch Global Settings with nested includes support
1094
+ * Fetch Global Settings
1104
1095
  */
1105
1096
  async getGlobals(options = {}) {
1106
1097
  const {
@@ -1152,6 +1143,9 @@ var ContentEngine = class {
1152
1143
  revalidate
1153
1144
  });
1154
1145
  if (!res.ok) {
1146
+ if (res.status === 408) {
1147
+ throw new Error("Request timeout");
1148
+ }
1155
1149
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1156
1150
  }
1157
1151
  const json = await res.json();
@@ -1200,12 +1194,12 @@ var ContentEngine = class {
1200
1194
  method: "GET",
1201
1195
  headers: this.getHeaders(),
1202
1196
  tags: fetchTags,
1203
- // ?? not || — see the constructor comment on defaultRevalidate for
1204
- // why: an explicit `revalidate: 0` on this call must not be
1205
- // discarded in favor of the engine's default.
1206
1197
  revalidate: options.revalidate ?? this.defaultRevalidate
1207
1198
  });
1208
1199
  if (!res.ok) {
1200
+ if (res.status === 408) {
1201
+ throw new Error("Request timeout");
1202
+ }
1209
1203
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1210
1204
  }
1211
1205
  const json = await res.json();
@@ -1237,12 +1231,15 @@ var ContentEngine = class {
1237
1231
  headers: this.getHeaders()
1238
1232
  });
1239
1233
  if (!res.ok) {
1234
+ if (res.status === 408) {
1235
+ throw new Error("Request timeout");
1236
+ }
1240
1237
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1241
1238
  }
1242
1239
  return await res.json();
1243
1240
  }
1244
1241
  /**
1245
- * Prefetch content for better performance
1242
+ * Prefetch content
1246
1243
  */
1247
1244
  async prefetch(urls) {
1248
1245
  if (typeof window !== "undefined" && "requestIdleCallback" in window) {
@@ -1255,41 +1252,19 @@ var ContentEngine = class {
1255
1252
  }
1256
1253
  /**
1257
1254
  * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
1258
- *
1259
- * ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
1260
- * This connects from the BROWSER TAB it's called in, and on message it
1261
- * only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
1262
- * invalidateCache below) in THAT browser tab's JS heap. In a typical
1263
- * Next.js deployment — and Cloudflare specifically, which is stateless
1264
- * per-request at the edge — that is a different process/isolate than the
1265
- * one that will render the NEXT server request for this content. So:
1266
- * - ✅ Useful for: a client component that reads from `nexus.content`
1267
- * directly in the browser and re-renders in place without a page
1268
- * navigation (e.g. a live-updating dashboard widget).
1269
- * - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
1270
- * already-loaded page updates" via a server-rendered page. That
1271
- * requires the backend's /api/revalidate webhook (see
1272
- * content.service.ts `pingNextRevalidateWebhook`) to have actually
1273
- * cleared the *server's* Data Cache, so the NEXT navigation or
1274
- * server request picks up fresh data. This SSE channel does not
1275
- * replace that — it's a complementary, browser-local optimization.
1276
- * If your symptom was "stale content after editing," fix the webhook
1277
- * wiring first; treat this method as an enhancement layered on top.
1278
1255
  */
1279
1256
  subscribeToUpdates(callback) {
1280
1257
  if (this.isServer || typeof EventSource === "undefined") {
1281
1258
  if (this.config.debug) {
1282
1259
  console.warn(
1283
- "[NexusHub] subscribeToUpdates() called in a non-browser environment (server render or SSR pass) \u2014 this is expected and safely skipped; EventSource only makes sense client-side."
1260
+ "[NexusHub] subscribeToUpdates() called in a non-browser environment."
1284
1261
  );
1285
1262
  }
1286
1263
  return () => {
1287
1264
  };
1288
1265
  }
1289
1266
  if (!this.config.apiKey) {
1290
- console.warn(
1291
- "[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
1292
- );
1267
+ console.warn("[NexusHub] subscribeToUpdates(): no apiKey configured.");
1293
1268
  }
1294
1269
  let eventSource = null;
1295
1270
  let retryCount = 0;
@@ -1453,6 +1428,9 @@ var ContentEngine = class {
1453
1428
  `Page '${slug}' not found. Check your Dashboard or Seed data.`
1454
1429
  );
1455
1430
  }
1431
+ if (res.status === 408) {
1432
+ throw new Error("Request timeout");
1433
+ }
1456
1434
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1457
1435
  }
1458
1436
  const json = await res.json();
@@ -1487,6 +1465,9 @@ var ContentEngine = class {
1487
1465
  revalidate
1488
1466
  });
1489
1467
  if (!res.ok) {
1468
+ if (res.status === 408) {
1469
+ throw new Error("Request timeout");
1470
+ }
1490
1471
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1491
1472
  }
1492
1473
  const json = await res.json();
@@ -1573,7 +1554,6 @@ var ContentEngine = class {
1573
1554
  const response = await fetch(url, {
1574
1555
  ...fetchOptions,
1575
1556
  ...nextConfig,
1576
- // Inject Next.js tags
1577
1557
  signal: controller.signal
1578
1558
  });
1579
1559
  clearTimeout(id);
@@ -1586,7 +1566,8 @@ var ContentEngine = class {
1586
1566
  getHeaders() {
1587
1567
  const headers = {
1588
1568
  "Content-Type": "application/json",
1589
- "X-Nexus-Client": "client-sdk/1.0.0",
1569
+ "X-Nexus-Client": `client-sdk/${this.config.sdkVersion ?? "1.1.0"}`,
1570
+ "X-Nexus-Request-ID": typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`,
1590
1571
  "X-Nexus-Project": this.config.projectId
1591
1572
  };
1592
1573
  if (this.config.apiKey) {
@@ -1597,13 +1578,22 @@ var ContentEngine = class {
1597
1578
  isCacheValid(metadata) {
1598
1579
  return Date.now() < metadata.expiresAt;
1599
1580
  }
1581
+ /**
1582
+ * 🛡️ FIX: Normalizes timeout errors (AbortError, 408, or Timeout messages)
1583
+ * into clean, standardized "Request timeout" errors.
1584
+ */
1600
1585
  normalizeError(error, context) {
1601
1586
  if (error instanceof Error) {
1602
- if (error.name === "AbortError") {
1587
+ const msg = error.message.toLowerCase();
1588
+ if (error.name === "AbortError" || error.message.includes("408") || msg.includes("timeout") || msg.includes("aborted")) {
1603
1589
  return new Error(`${context}: Request timeout`);
1604
1590
  }
1605
1591
  return error;
1606
1592
  }
1593
+ const str = String(error).toLowerCase();
1594
+ if (str.includes("408") || str.includes("timeout") || str.includes("aborted")) {
1595
+ return new Error(`${context}: Request timeout`);
1596
+ }
1607
1597
  return new Error(`${context}: ${String(error)}`);
1608
1598
  }
1609
1599
  cancelRequests() {
@@ -1620,9 +1610,12 @@ var ContentEngine = class {
1620
1610
  }
1621
1611
  };
1622
1612
 
1613
+ // src/analytics/tracker.ts
1614
+ init_config();
1615
+
1623
1616
  // src/analytics/fingerprint.ts
1624
1617
  var cachedEntropy = null;
1625
- var getDeviceEntropy = async () => {
1618
+ var getDeviceEntropy = async (allowFingerprinting = true) => {
1626
1619
  if (cachedEntropy) return cachedEntropy;
1627
1620
  if (typeof window === "undefined") return {};
1628
1621
  const nav = window.navigator;
@@ -1637,7 +1630,7 @@ var getDeviceEntropy = async () => {
1637
1630
  platform: nav.platform,
1638
1631
  language: nav.language,
1639
1632
  touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
1640
- canvas_hash: await generateCanvasHash()
1633
+ canvas_hash: allowFingerprinting ? await generateCanvasHash() : void 0
1641
1634
  };
1642
1635
  return cachedEntropy;
1643
1636
  };
@@ -1650,7 +1643,6 @@ var generateCanvasHash = async () => {
1650
1643
  canvas.height = 50;
1651
1644
  ctx.textBaseline = "top";
1652
1645
  ctx.font = '16px "Arial"';
1653
- ctx.textBaseline = "alphabetic";
1654
1646
  ctx.fillStyle = "#f60";
1655
1647
  ctx.fillRect(125, 1, 62, 20);
1656
1648
  ctx.fillStyle = "#069";
@@ -1669,12 +1661,12 @@ var generateCanvasHash = async () => {
1669
1661
  return "";
1670
1662
  }
1671
1663
  };
1672
- var getVisitorId = async () => {
1664
+ var getVisitorId = async (allowFingerprinting = true) => {
1673
1665
  if (typeof window === "undefined") return "server_visitor";
1674
1666
  const STORAGE_KEY = "nexus_vid";
1675
1667
  let vid = localStorage.getItem(STORAGE_KEY);
1676
1668
  if (!vid) {
1677
- const entropy = await getDeviceEntropy();
1669
+ const entropy = await getDeviceEntropy(allowFingerprinting);
1678
1670
  const random = Math.random().toString(36).substring(2, 15);
1679
1671
  const timestamp = Date.now().toString(36);
1680
1672
  const fingerprint = [
@@ -1682,7 +1674,7 @@ var getVisitorId = async () => {
1682
1674
  entropy.hardware_concurrency,
1683
1675
  entropy.timezone_offset,
1684
1676
  entropy.platform,
1685
- entropy.canvas_hash
1677
+ entropy.canvas_hash || "standard_entropy"
1686
1678
  ].join("|");
1687
1679
  let hash = 0;
1688
1680
  for (let i = 0; i < fingerprint.length; i++) {
@@ -1893,7 +1885,6 @@ var EventStorage = class {
1893
1885
  var eventStorage = new EventStorage();
1894
1886
 
1895
1887
  // src/analytics/tracker.ts
1896
- var SDK_VERSION = "0.1.0";
1897
1888
  var safeGetItem = (key) => {
1898
1889
  try {
1899
1890
  return localStorage.getItem(key);
@@ -1929,6 +1920,8 @@ var Tracker = class {
1929
1920
  this.visitorId = "";
1930
1921
  this.anonymousId = "";
1931
1922
  this.isFlushing = false;
1923
+ this.flushPromise = null;
1924
+ this.batchSize = 25;
1932
1925
  this.config = config;
1933
1926
  this.endpoint = `${this.config.analyticsUrl}/api/collect`;
1934
1927
  this.circuitBreaker = new CircuitBreaker();
@@ -1939,7 +1932,9 @@ var Tracker = class {
1939
1932
  }
1940
1933
  }
1941
1934
  async initSession() {
1942
- this.visitorId = await getVisitorId();
1935
+ this.visitorId = await getVisitorId(
1936
+ this.config.privacy?.fingerprinting ?? false
1937
+ );
1943
1938
  let anonId = safeGetItem("nexus_anon_id");
1944
1939
  if (!anonId) {
1945
1940
  anonId = `anon_${generateUUID().replace(/-/g, "")}`;
@@ -1952,8 +1947,8 @@ var Tracker = class {
1952
1947
  const SESSION_TIMEOUT = 30 * 60 * 1e3;
1953
1948
  const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
1954
1949
  if (isExpired) {
1955
- const uuid = generateUUID().replace(/-/g, "").substring(0, 16);
1956
- sid = `sess_${uuid}_${now}`;
1950
+ const uuid2 = generateUUID().replace(/-/g, "").substring(0, 16);
1951
+ sid = `sess_${uuid2}_${now}`;
1957
1952
  safeSetItem("nexus_sid", sid);
1958
1953
  this.sessionStart = now;
1959
1954
  }
@@ -1963,7 +1958,9 @@ var Tracker = class {
1963
1958
  async send(eventType, data = {}, eventName, ecommerce) {
1964
1959
  if (typeof window === "undefined") return;
1965
1960
  safeSetItem("nexus_last_active", Date.now().toString());
1966
- const entropy = await getDeviceEntropy();
1961
+ const entropy = await getDeviceEntropy(
1962
+ this.config.privacy?.fingerprinting ?? false
1963
+ );
1967
1964
  const perfMetrics = vitalsCollector.getMetricsSnapshot();
1968
1965
  const utmParams = extractUtmParams(window.location.href);
1969
1966
  const payload = {
@@ -1974,6 +1971,7 @@ var Tracker = class {
1974
1971
  messageId: generateUUID(),
1975
1972
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
1976
1973
  version: SDK_VERSION,
1974
+ // Dynamically synced to SDK 1.1.0
1977
1975
  url: window.location.href,
1978
1976
  referrer: document.referrer,
1979
1977
  userAgent: window.navigator.userAgent,
@@ -1991,8 +1989,8 @@ var Tracker = class {
1991
1989
  hardwareConcurrency: entropy.hardware_concurrency,
1992
1990
  deviceMemory: entropy.device_memory,
1993
1991
  pixelRatio: entropy.pixel_ratio,
1994
- canvasFingerprint: entropy.canvas_hash,
1995
- platform: entropy.platform
1992
+ platform: entropy.platform,
1993
+ ...this.config.privacy?.fingerprinting ? { canvasFingerprint: entropy.canvas_hash } : {}
1996
1994
  },
1997
1995
  visitorIdLocal: this.visitorId,
1998
1996
  utm: utmParams
@@ -2012,53 +2010,69 @@ var Tracker = class {
2012
2010
  });
2013
2011
  }
2014
2012
  async flushQueue(useBeacon = false) {
2015
- if (this.isFlushing) return;
2016
- if (this.circuitBreaker.isOpen()) return;
2017
- this.isFlushing = true;
2018
- try {
2019
- const storedEvents = await eventStorage.peek(20);
2020
- if (storedEvents.length === 0) {
2021
- this.isFlushing = false;
2022
- return;
2023
- }
2024
- const payloads = storedEvents.map((e) => e.payload);
2025
- const promises = payloads.map(
2026
- (event) => fetch(this.endpoint, {
2027
- method: "POST",
2028
- headers: {
2029
- "Content-Type": "application/json",
2030
- Authorization: `Bearer ${this.config.apiKey}`
2031
- },
2032
- body: JSON.stringify(event),
2033
- keepalive: useBeacon
2034
- })
2035
- );
2036
- const results = await Promise.allSettled(promises);
2037
- const successIds = [];
2038
- let failureCount = 0;
2039
- results.forEach((res, index) => {
2040
- if (res.status === "fulfilled" && res.value.ok) {
2041
- successIds.push(storedEvents[index].id);
2013
+ if (this.flushPromise) return this.flushPromise;
2014
+ this.flushPromise = (async () => {
2015
+ if (this.circuitBreaker.isOpen()) return;
2016
+ try {
2017
+ const storedEvents = await eventStorage.peek(this.batchSize);
2018
+ if (!storedEvents.length) return;
2019
+ const payloads = storedEvents.map((e) => e.payload);
2020
+ const headers = {
2021
+ "Content-Type": "application/json",
2022
+ Authorization: this.config.apiKey ? `Bearer ${this.config.apiKey}` : "",
2023
+ "X-Nexus-Client": `analytics/${SDK_VERSION}`,
2024
+ "X-Nexus-Project": this.config.projectId
2025
+ };
2026
+ let response;
2027
+ try {
2028
+ response = await fetch(
2029
+ `${this.config.analyticsUrl}/api/collect/batch`,
2030
+ {
2031
+ method: "POST",
2032
+ headers,
2033
+ body: JSON.stringify({ events: payloads }),
2034
+ keepalive: useBeacon
2035
+ }
2036
+ );
2037
+ } catch {
2038
+ response = void 0;
2039
+ }
2040
+ if (!response || response.status === 404 || response.status === 405) {
2041
+ const results = await Promise.allSettled(
2042
+ payloads.map(
2043
+ (event) => fetch(this.endpoint, {
2044
+ method: "POST",
2045
+ headers,
2046
+ body: JSON.stringify(event),
2047
+ keepalive: useBeacon
2048
+ })
2049
+ )
2050
+ );
2051
+ const successIds = results.flatMap(
2052
+ (r, i) => r.status === "fulfilled" && r.value.ok ? [storedEvents[i].id] : []
2053
+ );
2054
+ if (successIds.length) await eventStorage.remove(successIds);
2055
+ if (successIds.length === storedEvents.length) {
2056
+ this.circuitBreaker.recordSuccess();
2057
+ } else {
2058
+ this.circuitBreaker.recordFailure();
2059
+ }
2060
+ } else if (response.ok) {
2061
+ await eventStorage.remove(storedEvents.map((e) => e.id));
2062
+ this.circuitBreaker.recordSuccess();
2042
2063
  } else {
2043
- failureCount++;
2064
+ this.circuitBreaker.recordFailure();
2044
2065
  }
2045
- });
2046
- if (successIds.length > 0) {
2047
- await eventStorage.remove(successIds);
2048
- this.circuitBreaker.recordSuccess();
2049
- }
2050
- if (failureCount > 0) {
2066
+ } catch (err) {
2051
2067
  this.circuitBreaker.recordFailure();
2068
+ if (this.config.debug) {
2069
+ console.warn("[GN-Apex] Analytics flush deferred:", err);
2070
+ }
2071
+ } finally {
2072
+ this.flushPromise = null;
2052
2073
  }
2053
- } catch (err) {
2054
- console.error("[NexusHub] Network Error:", err);
2055
- this.circuitBreaker.recordFailure();
2056
- } finally {
2057
- this.isFlushing = false;
2058
- if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
2059
- setTimeout(() => this.flushQueue(), 100);
2060
- }
2061
- }
2074
+ })();
2075
+ return this.flushPromise;
2062
2076
  }
2063
2077
  getSession() {
2064
2078
  return this.sessionId;
@@ -2337,70 +2351,35 @@ var AnalyticsEngine = class {
2337
2351
  }
2338
2352
  setupVideoTracking() {
2339
2353
  if (typeof document === "undefined") return;
2340
- const attachVideoListeners = (video) => {
2341
- if (video.__nexus_tracked) return;
2342
- video.__nexus_tracked = true;
2343
- const src = video.src || video.currentSrc || "unknown";
2344
- let milestone50Fired = false;
2345
- video.addEventListener("play", () => {
2346
- this.tracker.send(
2347
- "custom_event",
2348
- { event_name: "video_play", src },
2349
- "video_play"
2350
- );
2351
- });
2352
- video.addEventListener("pause", () => {
2353
- this.tracker.send(
2354
- "custom_event",
2355
- {
2356
- event_name: "video_pause",
2357
- src,
2358
- position_seconds: Math.round(video.currentTime)
2359
- },
2360
- "video_pause"
2361
- );
2362
- });
2354
+ const attach = (video) => {
2355
+ const v = video;
2356
+ if (v.__gnexusTracked) return;
2357
+ v.__gnexusTracked = true;
2358
+ v.__gnexusMilestones = /* @__PURE__ */ new Set();
2359
+ const src = () => video.currentSrc || video.src || "unknown";
2360
+ const emit = (name, data = {}) => this.tracker.send("media", { media_type: "video", event_name: name, src: src(), ...data }, name);
2361
+ video.addEventListener("loadedmetadata", () => emit("video_loaded", { duration_seconds: Number.isFinite(video.duration) ? Math.round(video.duration) : void 0 }));
2362
+ video.addEventListener("play", () => emit("video_play", { position_seconds: Math.round(video.currentTime) }));
2363
+ video.addEventListener("pause", () => emit("video_pause", { position_seconds: Math.round(video.currentTime) }));
2364
+ video.addEventListener("seeking", () => emit("video_seek", { position_seconds: Math.round(video.currentTime) }));
2365
+ video.addEventListener("ended", () => emit("video_complete", { duration_seconds: Math.round(video.duration) }));
2366
+ video.addEventListener("error", () => emit("video_error"));
2363
2367
  video.addEventListener("timeupdate", () => {
2364
- if (!video.duration || video.duration === Infinity) return;
2365
- const pct = video.currentTime / video.duration;
2366
- if (pct >= 0.5 && !milestone50Fired) {
2367
- milestone50Fired = true;
2368
- this.tracker.send(
2369
- "custom_event",
2370
- {
2371
- event_name: "video_50_percent",
2372
- src
2373
- },
2374
- "video_50_percent"
2375
- );
2368
+ if (!Number.isFinite(video.duration) || video.duration <= 0) return;
2369
+ for (const milestone of [25, 50, 75, 90, 100]) {
2370
+ if (video.currentTime / video.duration * 100 >= milestone && !v.__gnexusMilestones.has(milestone)) {
2371
+ v.__gnexusMilestones.add(milestone);
2372
+ emit(`video_${milestone}_percent`, { progress: milestone / 100 });
2373
+ }
2376
2374
  }
2377
2375
  });
2378
- video.addEventListener("ended", () => {
2379
- this.tracker.send(
2380
- "custom_event",
2381
- {
2382
- event_name: "video_complete",
2383
- src,
2384
- duration_seconds: Math.round(video.duration)
2385
- },
2386
- "video_complete"
2387
- );
2388
- });
2389
2376
  };
2390
- document.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2391
- const observer = new MutationObserver((mutations) => {
2392
- mutations.forEach((m) => {
2393
- m.addedNodes.forEach((node) => {
2394
- if (node instanceof HTMLVideoElement) {
2395
- attachVideoListeners(node);
2396
- }
2397
- if (node instanceof Element) {
2398
- node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2399
- }
2400
- });
2401
- });
2402
- });
2403
- observer.observe(document.body, { childList: true, subtree: true });
2377
+ document.querySelectorAll("video").forEach((v) => attach(v));
2378
+ const observer = new MutationObserver((ms) => ms.forEach((m) => m.addedNodes.forEach((n) => {
2379
+ if (n instanceof HTMLVideoElement) attach(n);
2380
+ if (n instanceof Element) n.querySelectorAll("video").forEach((v) => attach(v));
2381
+ })));
2382
+ if (document.body) observer.observe(document.body, { childList: true, subtree: true });
2404
2383
  this.cleanupFns.push(() => observer.disconnect());
2405
2384
  }
2406
2385
  setupErrorTracking() {
@@ -2589,65 +2568,397 @@ function getSelector(el, depth = 0) {
2589
2568
  return `${parent}${self}`;
2590
2569
  }
2591
2570
 
2571
+ // src/errors.ts
2572
+ var NexusError = class extends Error {
2573
+ constructor(message, code = "UNKNOWN", status, requestId, details, retryable = false, cause) {
2574
+ super(message);
2575
+ this.code = code;
2576
+ this.status = status;
2577
+ this.requestId = requestId;
2578
+ this.details = details;
2579
+ this.retryable = retryable;
2580
+ this.cause = cause;
2581
+ this.name = "NexusError";
2582
+ Object.setPrototypeOf(this, new.target.prototype);
2583
+ }
2584
+ };
2585
+ var isNexusError = (e) => e instanceof NexusError;
2586
+
2587
+ // src/events.ts
2588
+ var NexusEventBus = class {
2589
+ constructor() {
2590
+ this.listeners = /* @__PURE__ */ new Map();
2591
+ }
2592
+ on(event, listener) {
2593
+ const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
2594
+ set.add(listener);
2595
+ this.listeners.set(event, set);
2596
+ return () => set.delete(listener);
2597
+ }
2598
+ emit(event, payload) {
2599
+ this.listeners.get(event)?.forEach((l) => {
2600
+ try {
2601
+ l(payload);
2602
+ } catch {
2603
+ }
2604
+ });
2605
+ }
2606
+ clear() {
2607
+ this.listeners.clear();
2608
+ }
2609
+ };
2610
+
2611
+ // src/http.ts
2612
+ var uuid = () => typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`;
2613
+ var NexusHttpClient = class {
2614
+ constructor(config, events = new NexusEventBus()) {
2615
+ this.config = config;
2616
+ this.events = events;
2617
+ this.breaker = new CircuitBreaker();
2618
+ this.limiter = new RateLimiter({
2619
+ maxRequests: config.debug ? 200 : 100,
2620
+ timeWindow: 6e4
2621
+ });
2622
+ this.backoff = new ExponentialBackoff({
2623
+ maxRetries: config.retries ?? 3,
2624
+ baseDelay: 150,
2625
+ maxDelay: 8e3,
2626
+ jitter: true
2627
+ });
2628
+ }
2629
+ updateConfig(config) {
2630
+ this.config = config;
2631
+ }
2632
+ async request(input, options = {}) {
2633
+ if (this.breaker.isOpen()) {
2634
+ throw new NexusError(
2635
+ "GN-Apex service circuit is open",
2636
+ "CIRCUIT_OPEN",
2637
+ void 0,
2638
+ void 0,
2639
+ void 0,
2640
+ true
2641
+ );
2642
+ }
2643
+ const requestId = uuid();
2644
+ const url = input;
2645
+ const started = Date.now();
2646
+ const headers = new Headers(options.headers);
2647
+ headers.set("X-Nexus-Request-ID", requestId);
2648
+ headers.set(
2649
+ "X-Nexus-Client",
2650
+ `gnexus-sdk/${this.config.sdkVersion ?? "1.1.0"}`
2651
+ );
2652
+ headers.set("X-Nexus-Project", this.config.projectId);
2653
+ if (this.config.apiKey)
2654
+ headers.set("Authorization", `Bearer ${this.config.apiKey}`);
2655
+ this.events.emit("request:start", {
2656
+ requestId,
2657
+ url,
2658
+ method: options.method ?? "GET"
2659
+ });
2660
+ try {
2661
+ await this.limiter.checkLimit();
2662
+ const response = await this.backoff.execute(async () => {
2663
+ const controller = new AbortController();
2664
+ const timeout = options.timeout ?? this.config.timeout ?? 1e4;
2665
+ const timer = setTimeout(() => controller.abort(), timeout);
2666
+ try {
2667
+ return await fetch(url, {
2668
+ ...options,
2669
+ headers,
2670
+ signal: options.signal ?? controller.signal
2671
+ });
2672
+ } catch (e) {
2673
+ if (e?.name === "AbortError") {
2674
+ throw new NexusError(
2675
+ `Request timed out after ${timeout}ms`,
2676
+ "TIMEOUT",
2677
+ void 0,
2678
+ requestId,
2679
+ void 0,
2680
+ true,
2681
+ e
2682
+ );
2683
+ }
2684
+ throw new NexusError(
2685
+ "Network request failed",
2686
+ "NETWORK_ERROR",
2687
+ void 0,
2688
+ requestId,
2689
+ void 0,
2690
+ true,
2691
+ e
2692
+ );
2693
+ } finally {
2694
+ clearTimeout(timer);
2695
+ }
2696
+ });
2697
+ if (!response.ok) {
2698
+ const retryable = response.status === 408 || response.status === 429 || response.status >= 500;
2699
+ let details;
2700
+ try {
2701
+ details = await response.clone().json();
2702
+ } catch {
2703
+ }
2704
+ const code = response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR";
2705
+ const err = new NexusError(
2706
+ `API request failed (${response.status})`,
2707
+ code,
2708
+ response.status,
2709
+ response.headers.get("x-nexus-request-id") ?? requestId,
2710
+ details,
2711
+ retryable
2712
+ );
2713
+ throw err;
2714
+ }
2715
+ this.breaker.recordSuccess();
2716
+ this.events.emit("request:end", {
2717
+ requestId,
2718
+ url,
2719
+ status: response.status,
2720
+ duration: Date.now() - started
2721
+ });
2722
+ return response;
2723
+ } catch (e) {
2724
+ if (e instanceof NexusError) {
2725
+ if (e.retryable || e.status && (e.status >= 500 || e.status === 429)) {
2726
+ this.breaker.recordFailure();
2727
+ } else {
2728
+ this.breaker.recordSuccess();
2729
+ }
2730
+ } else {
2731
+ this.breaker.recordFailure();
2732
+ }
2733
+ this.events.emit("error", { error: e });
2734
+ throw e;
2735
+ }
2736
+ }
2737
+ getStats() {
2738
+ return {
2739
+ rateLimit: this.limiter.getStats(),
2740
+ circuit: this.breaker.getState()
2741
+ };
2742
+ }
2743
+ };
2744
+
2745
+ // src/flags.ts
2746
+ var FeatureFlags = class {
2747
+ constructor(initial) {
2748
+ this.values = {};
2749
+ this.values = { ...initial };
2750
+ }
2751
+ set(values) {
2752
+ this.values = { ...this.values, ...values };
2753
+ }
2754
+ isEnabled(key, fallback = false) {
2755
+ const v = this.values[key];
2756
+ return typeof v === "boolean" ? v : fallback;
2757
+ }
2758
+ get(key, fallback) {
2759
+ return this.values[key] ?? fallback;
2760
+ }
2761
+ all() {
2762
+ return { ...this.values };
2763
+ }
2764
+ };
2765
+ var RemoteConfig = class {
2766
+ constructor() {
2767
+ this.values = {};
2768
+ }
2769
+ set(values) {
2770
+ this.values = { ...this.values, ...values };
2771
+ }
2772
+ get(key, fallback) {
2773
+ return this.values[key] ?? fallback;
2774
+ }
2775
+ all() {
2776
+ return { ...this.values };
2777
+ }
2778
+ };
2779
+
2780
+ // src/diagnostics.ts
2781
+ var getDiagnostics = (version, environment) => ({ version, environment, online: typeof navigator === "undefined" ? true : navigator.onLine, userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent, memory: typeof performance !== "undefined" && "memory" in performance ? performance.memory?.usedJSHeapSize : void 0 });
2782
+
2592
2783
  // src/client.ts
2593
- var DEFAULT_REVALIDATE_SECONDS = false;
2594
2784
  var NexusClient = class {
2595
- constructor(config) {
2596
- const fullConfig = getFullConfig(config);
2785
+ constructor(config = {}) {
2786
+ const full = getFullConfig(config);
2597
2787
  this.config = {
2598
- debug: config?.debug ?? false,
2599
- cacheStrategy: config?.cacheStrategy ?? "memory",
2600
- // Only fall through to the safe default when revalidateTime is
2601
- // genuinely *unset* (undefined). An explicit `false` from the caller
2602
- // is a deliberate "never revalidate" choice and must be respected,
2603
- // not silently upgraded — `??` (not `||`) is required here so that
2604
- // `0` (revalidate on every request) also passes through untouched
2605
- // instead of being treated as falsy.
2606
- revalidateTime: config?.revalidateTime ?? DEFAULT_REVALIDATE_SECONDS,
2607
- timeout: config?.timeout ?? 1e4,
2608
- retries: config?.retries ?? 3,
2609
- ...fullConfig
2788
+ debug: false,
2789
+ cacheStrategy: "memory",
2790
+ revalidateTime: false,
2791
+ timeout: 1e4,
2792
+ retries: 3,
2793
+ sdkVersion: SDK_VERSION,
2794
+ cacheInvalidation: "platform",
2795
+ environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production",
2796
+ autoTracking: true,
2797
+ publicKeyOnly: true,
2798
+ // 🚀 Default: fingerprinting: true (Active by default for high-precision analytics)
2799
+ privacy: { analytics: true, fingerprinting: true, redact: true },
2800
+ ...full,
2801
+ ...config
2610
2802
  };
2611
2803
  const errors = validateConfig(this.config);
2612
- if (errors.length > 0) {
2613
- console.warn("\u26A0\uFE0F NexusHub: Configuration issues:", errors.join(", "));
2614
- if (!this.config.projectId) {
2615
- console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Tracking will fail.");
2616
- }
2804
+ if (errors.length && this.config.debug) {
2805
+ console.warn("[GN-Apex] Configuration warnings:", errors);
2617
2806
  }
2807
+ this.events = new NexusEventBus();
2808
+ this.http = new NexusHttpClient(this.config, this.events);
2809
+ this.flags = new FeatureFlags();
2810
+ this.remoteConfig = new RemoteConfig();
2618
2811
  this.content = new ContentEngine(this.config);
2619
- if (typeof window !== "undefined") {
2812
+ if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false) {
2620
2813
  this.analytics = new AnalyticsEngine(this.config);
2621
2814
  this.analytics.start();
2622
2815
  }
2623
2816
  }
2624
2817
  /**
2625
- * Helper alias for cleaner content fetching.
2818
+ * Helper alias for cleaner page content fetching.
2626
2819
  */
2627
2820
  getPage(slug, options) {
2628
2821
  return this.content.getPage(slug, options);
2629
2822
  }
2630
2823
  /**
2631
- * Returns a readonly snapshot of the current config.
2824
+ * Returns a readonly snapshot of the active configuration.
2632
2825
  */
2633
2826
  getConfig() {
2634
- return { ...this.config };
2827
+ return Object.freeze({
2828
+ ...this.config,
2829
+ privacy: { ...this.config.privacy }
2830
+ });
2635
2831
  }
2636
2832
  /**
2637
- * Updates specific config fields at runtime.
2638
- * Replaces the previous pattern of `(nexus as any).config.projectId = x`
2639
- * which bypassed TypeScript and mutated internal state unsafely.
2833
+ * Updates runtime configuration dynamically without breaking active listeners.
2640
2834
  */
2641
2835
  updateConfig(updates) {
2642
- this.config = { ...this.config, ...updates };
2836
+ this.config = {
2837
+ ...this.config,
2838
+ ...updates,
2839
+ privacy: { ...this.config.privacy, ...updates.privacy }
2840
+ };
2841
+ this.http.updateConfig(this.config);
2842
+ this.content.updateConfig?.(this.config);
2843
+ if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false && !this.analytics) {
2844
+ this.analytics = new AnalyticsEngine(this.config);
2845
+ this.analytics.start();
2846
+ }
2847
+ if (this.config.privacy?.analytics === false) {
2848
+ this.analytics?.stop();
2849
+ }
2850
+ }
2851
+ /**
2852
+ * Returns deep system diagnostics including HTTP and cache performance.
2853
+ */
2854
+ diagnostics() {
2855
+ return {
2856
+ ...getDiagnostics(SDK_VERSION, this.config.environment ?? "production"),
2857
+ cache: this.content.getCacheStats(),
2858
+ http: this.http.getStats(),
2859
+ analyticsQueue: this.analytics ? void 0 : 0
2860
+ };
2861
+ }
2862
+ /**
2863
+ * Gracefully terminates background tasks and cleans up listeners.
2864
+ */
2865
+ destroy() {
2866
+ this.analytics?.stop(true);
2867
+ this.content.cleanup?.();
2868
+ this.events.clear();
2643
2869
  }
2644
2870
  };
2645
2871
  var nexus = new NexusClient();
2646
- var createNexusClient = (config) => new NexusClient(config);
2872
+ var createNexusClient = (config = {}) => new NexusClient(config);
2647
2873
 
2648
2874
  // src/index.ts
2649
2875
  init_config();
2650
- var VERSION = "0.0.1";
2876
+
2877
+ // src/notifications/push-client.ts
2878
+ var NexusPushClient = class {
2879
+ constructor(config) {
2880
+ this.config = config;
2881
+ }
2882
+ /**
2883
+ * Helper to convert Base64 VAPID key to Uint8Array for WebPush security
2884
+ */
2885
+ urlBase64ToUint8Array(base64String) {
2886
+ const padding = "=".repeat((4 - base64String.length % 4) % 4);
2887
+ const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");
2888
+ const rawData = window.atob(base64);
2889
+ const outputArray = new Uint8Array(rawData.length);
2890
+ for (let i = 0; i < rawData.length; ++i) {
2891
+ outputArray[i] = rawData.charCodeAt(i);
2892
+ }
2893
+ return outputArray;
2894
+ }
2895
+ /**
2896
+ * Requests browser notification permissions and registers the WebPush subscription
2897
+ */
2898
+ async requestSubscription(serviceWorkerPath = "/sw.js") {
2899
+ if (typeof window === "undefined" || !("serviceWorker" in navigator) || !("PushManager" in window)) {
2900
+ console.warn(
2901
+ "[NexusHub] Push notifications are not supported in this browser environment."
2902
+ );
2903
+ return false;
2904
+ }
2905
+ try {
2906
+ const permission = await Notification.requestPermission();
2907
+ if (permission !== "granted") {
2908
+ console.warn("[NexusHub] Notification permission denied by user.");
2909
+ return false;
2910
+ }
2911
+ const vapidRes = await fetch(
2912
+ `${this.config.apiUrl}/notifications/vapid-key`,
2913
+ {
2914
+ headers: {
2915
+ Authorization: `Bearer ${this.config.apiKey}`,
2916
+ "x-nexus-project": this.config.projectId
2917
+ }
2918
+ }
2919
+ );
2920
+ if (!vapidRes.ok) throw new Error("Failed to fetch VAPID public key.");
2921
+ const { publicKey } = await vapidRes.json();
2922
+ const registration = await navigator.serviceWorker.register(serviceWorkerPath);
2923
+ await navigator.serviceWorker.ready;
2924
+ const subscription = await registration.pushManager.subscribe({
2925
+ userVisibleOnly: true,
2926
+ // 🚀 RESOLVED: Cast as 'any' to bypass strict DOM BufferSource typings
2927
+ applicationServerKey: this.urlBase64ToUint8Array(publicKey)
2928
+ });
2929
+ const rawSub = subscription.toJSON();
2930
+ if (!rawSub.endpoint || !rawSub.keys?.auth || !rawSub.keys?.p256dh) {
2931
+ throw new Error(
2932
+ "Malformed subscription payload received from browser."
2933
+ );
2934
+ }
2935
+ const res = await fetch(
2936
+ `${this.config.apiUrl}/notifications/project/${this.config.projectId}/subscribe`,
2937
+ {
2938
+ method: "POST",
2939
+ headers: {
2940
+ "Content-Type": "application/json",
2941
+ Authorization: `Bearer ${this.config.apiKey}`,
2942
+ "x-nexus-project": this.config.projectId
2943
+ },
2944
+ body: JSON.stringify({
2945
+ endpoint: rawSub.endpoint,
2946
+ auth: rawSub.keys.auth,
2947
+ p256dh: rawSub.keys.p256dh,
2948
+ provider: "WEB_PUSH"
2949
+ })
2950
+ }
2951
+ );
2952
+ return res.ok;
2953
+ } catch (err) {
2954
+ console.error("[NexusHub] WebPush subscription failed:", err);
2955
+ return false;
2956
+ }
2957
+ }
2958
+ };
2959
+
2960
+ // src/index.ts
2961
+ var VERSION = "1.1.0";
2651
2962
  // Annotate the CommonJS export names for ESM import in node:
2652
2963
  0 && (module.exports = {
2653
2964
  AnalyticsEngine,
@@ -2656,15 +2967,25 @@ var VERSION = "0.0.1";
2656
2967
  ContentEngine,
2657
2968
  DEFAULT_ANALYTICS_URL,
2658
2969
  DEFAULT_API_URL,
2970
+ FeatureFlags,
2659
2971
  LOCAL_NEST_URL,
2660
2972
  LOCAL_RUST_URL,
2661
2973
  LocalCache,
2662
2974
  MemoryCache,
2663
2975
  NexusClient,
2976
+ NexusError,
2977
+ NexusEventBus,
2978
+ NexusHttpClient,
2979
+ NexusPushClient,
2980
+ RemoteConfig,
2981
+ SDK_VERSION,
2664
2982
  VERSION,
2665
2983
  createNexusClient,
2984
+ getDiagnostics,
2666
2985
  getEnvConfig,
2667
2986
  getFullConfig,
2987
+ hasRequiredConfig,
2988
+ isNexusError,
2668
2989
  mergeConfigs,
2669
2990
  nexus,
2670
2991
  validateConfig