@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.js CHANGED
@@ -9,27 +9,32 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/config.ts
12
- var DEFAULT_API_URL, DEFAULT_ANALYTICS_URL, LOCAL_NEST_URL, LOCAL_RUST_URL, getEnvConfig, mergeConfigs, getFullConfig, validateConfig;
12
+ var DEFAULT_API_URL, DEFAULT_ANALYTICS_URL, SDK_VERSION, LOCAL_NEST_URL, LOCAL_RUST_URL, getEnvConfig, mergeConfigs, getFullConfig, validateConfig, hasRequiredConfig;
13
13
  var init_config = __esm({
14
14
  "src/config.ts"() {
15
15
  "use strict";
16
16
  DEFAULT_API_URL = "https://api.gnapex.com";
17
17
  DEFAULT_ANALYTICS_URL = "https://sentry.gnapex.com";
18
+ SDK_VERSION = "1.1.0";
18
19
  LOCAL_NEST_URL = "https://api.gnapex.com";
19
20
  LOCAL_RUST_URL = "https://sentry.gnapex.com";
20
21
  getEnvConfig = () => {
21
- const NEXT_PUBLIC_ID = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_ID : void 0;
22
- const NEXT_PUBLIC_KEY = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_KEY : void 0;
23
- const NEXT_PUBLIC_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_API_URL : void 0;
24
- const NODE_ID = typeof process !== "undefined" ? process.env.NEXUS_PROJECT_ID : void 0;
25
- const NODE_KEY = typeof process !== "undefined" ? process.env.NEXUS_API_KEY : void 0;
26
- const NODE_URL = typeof process !== "undefined" ? process.env.NEXUS_API_URL : void 0;
22
+ const NEXT_PUBLIC_ID = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_ID ?? process.env.NEXT_PUBLIC_NEXUS_ID : void 0;
23
+ const NEXT_PUBLIC_KEY = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_KEY ?? process.env.NEXT_PUBLIC_NEXUS_KEY : void 0;
24
+ const NEXT_PUBLIC_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_API_URL ?? process.env.NEXT_PUBLIC_NEXUS_API_URL : void 0;
25
+ const NEXT_PUBLIC_ANALYTICS_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_ANALYTICS_URL ?? process.env.NEXT_PUBLIC_NEXUS_ANALYTICS_URL : void 0;
26
+ const NODE_ID = typeof process !== "undefined" ? process.env.GNAPEX_PROJECT_ID ?? process.env.NEXUS_PROJECT_ID : void 0;
27
+ const NODE_KEY = typeof process !== "undefined" ? process.env.GNAPEX_API_KEY ?? process.env.NEXUS_API_KEY : void 0;
28
+ const NODE_URL = typeof process !== "undefined" ? process.env.GNAPEX_API_URL ?? process.env.NEXUS_API_URL : void 0;
29
+ const NODE_ANALYTICS_URL = typeof process !== "undefined" ? process.env.GNAPEX_ANALYTICS_URL ?? process.env.NEXUS_ANALYTICS_URL : void 0;
27
30
  const NODE_ENV = typeof process !== "undefined" ? process.env.NODE_ENV : "production";
28
31
  const isDev = NODE_ENV === "development" || typeof window !== "undefined" && window.location.hostname === "localhost";
29
- const projectId = NEXT_PUBLIC_ID ?? NODE_ID;
30
- const apiKey = NEXT_PUBLIC_KEY ?? NODE_KEY;
31
- const apiUrl = NEXT_PUBLIC_URL ?? NODE_URL ?? (isDev ? LOCAL_NEST_URL : DEFAULT_API_URL);
32
- const analyticsUrl = apiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL;
32
+ const runtime = typeof window !== "undefined" ? window.__GNAPEX__ ?? window.__NEXUS__ : void 0;
33
+ const meta = typeof document !== "undefined" ? (name) => document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ?? void 0 : (_name) => void 0;
34
+ const projectId = runtime?.projectId ?? meta("gnapex-project") ?? meta("nexus-project") ?? NEXT_PUBLIC_ID ?? NODE_ID;
35
+ const apiKey = runtime?.apiKey ?? meta("gnapex-key") ?? meta("nexus-key") ?? NEXT_PUBLIC_KEY ?? NODE_KEY;
36
+ const apiUrl = runtime?.apiUrl ?? meta("gnapex-api") ?? meta("nexus-api") ?? NEXT_PUBLIC_URL ?? NODE_URL ?? (isDev ? LOCAL_NEST_URL : DEFAULT_API_URL);
37
+ 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);
33
38
  return {
34
39
  projectId,
35
40
  apiKey,
@@ -57,8 +62,12 @@ var init_config = __esm({
57
62
  const errors = [];
58
63
  if (!config.projectId) errors.push("Missing projectId");
59
64
  if (!config.apiUrl) errors.push("Missing apiUrl");
65
+ else if (!/^https?:\/\//.test(config.apiUrl)) {
66
+ errors.push("apiUrl must be a valid URL starting with http:// or https://");
67
+ }
60
68
  return errors;
61
69
  };
70
+ hasRequiredConfig = (config) => validateConfig(config).length === 0;
62
71
  }
63
72
  });
64
73
 
@@ -166,6 +175,61 @@ var init_binary_compiler = __esm({
166
175
  }
167
176
  });
168
177
 
178
+ // src/content/utils.ts
179
+ function validateSlug(slug) {
180
+ if (!slug || typeof slug !== "string") {
181
+ throw new Error("Slug must be a non-empty string");
182
+ }
183
+ if (!/^[a-z0-9-_]+$/.test(slug)) {
184
+ throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
185
+ }
186
+ }
187
+ function normalizeQuery(query) {
188
+ const normalized = { ...query };
189
+ normalized.page = Math.max(1, normalized.page || 1);
190
+ normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
191
+ normalized.order = normalized.order || "desc";
192
+ if (normalized.page < 1) {
193
+ throw new Error("Page must be greater than 0");
194
+ }
195
+ if (normalized.limit < 1 || normalized.limit > 100) {
196
+ throw new Error("Limit must be between 1 and 100");
197
+ }
198
+ return normalized;
199
+ }
200
+ function buildQueryString(query) {
201
+ const params = new URLSearchParams();
202
+ if (query.page) params.append("page", query.page.toString());
203
+ if (query.limit) params.append("limit", query.limit.toString());
204
+ if (query.sort) params.append("sort", query.sort);
205
+ if (query.order) params.append("order", query.order);
206
+ if (query.search) params.append("search", query.search);
207
+ if (query.include?.length) {
208
+ params.append("include", query.include.join(","));
209
+ }
210
+ if (query.fields?.length) {
211
+ params.append("fields", query.fields.join(","));
212
+ }
213
+ if (query.filter) {
214
+ params.append("filter", JSON.stringify(query.filter));
215
+ }
216
+ return params.toString();
217
+ }
218
+ function measurePerformance(name, fn) {
219
+ const start = performance.now();
220
+ const result = fn();
221
+ const end = performance.now();
222
+ if (process.env.NODE_ENV === "development") {
223
+ console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
224
+ }
225
+ return { result, duration: end - start };
226
+ }
227
+ var init_utils = __esm({
228
+ "src/content/utils.ts"() {
229
+ "use strict";
230
+ }
231
+ });
232
+
169
233
  // src/content/local-cache-server.ts
170
234
  var local_cache_server_exports = {};
171
235
  __export(local_cache_server_exports, {
@@ -179,6 +243,7 @@ var init_local_cache_server = __esm({
179
243
  "use strict";
180
244
  init_config();
181
245
  init_binary_compiler();
246
+ init_utils();
182
247
  LocalCache = class {
183
248
  constructor(customPath, apiKey) {
184
249
  this.apiKey = "";
@@ -189,9 +254,6 @@ var init_local_cache_server = __esm({
189
254
  isLoaded() {
190
255
  return fs.existsSync(path.resolve(process.cwd(), this.baseDir));
191
256
  }
192
- /**
193
- * Helper to decrypt and decompile `.nx` files on-the-fly.
194
- */
195
257
  decompileFile(filePath) {
196
258
  if (!this.apiKey) {
197
259
  if (process.env.NODE_ENV === "development") {
@@ -220,9 +282,14 @@ var init_local_cache_server = __esm({
220
282
  }
221
283
  }
222
284
  /**
223
- * Retrieve a specific page directly from its .nx file.
285
+ * Retrieve a specific page safely from its .nx file.
224
286
  */
225
287
  async getPage(slug) {
288
+ try {
289
+ validateSlug(slug);
290
+ } catch {
291
+ return null;
292
+ }
226
293
  const filePath = path.resolve(
227
294
  process.cwd(),
228
295
  this.baseDir,
@@ -241,9 +308,14 @@ var init_local_cache_server = __esm({
241
308
  return null;
242
309
  }
243
310
  /**
244
- * Retrieve an entire collection from its specific .nx file.
311
+ * Retrieve an entire collection safely from its specific .nx file.
245
312
  */
246
313
  async getCollection(collectionId) {
314
+ try {
315
+ validateSlug(collectionId);
316
+ } catch {
317
+ return null;
318
+ }
247
319
  const filePath = path.resolve(
248
320
  process.cwd(),
249
321
  this.baseDir,
@@ -669,90 +741,69 @@ var LocalCacheProxy = class {
669
741
  // src/content/strategies.ts
670
742
  var RateLimiter = class {
671
743
  constructor(config) {
672
- this.requests = [];
673
744
  this.config = config;
745
+ this.requests = [];
674
746
  }
675
747
  async checkLimit() {
676
748
  while (true) {
677
749
  const now = Date.now();
678
- const windowStart = now - this.config.timeWindow;
679
- this.requests = this.requests.filter((time) => time > windowStart);
750
+ const cutoff = now - this.config.timeWindow;
751
+ this.requests = this.requests.filter((t) => t > cutoff);
680
752
  if (this.requests.length < this.config.maxRequests) {
681
753
  this.requests.push(now);
682
754
  return;
683
755
  }
684
- const oldestRequest = this.requests[0];
685
- const waitTime = oldestRequest + this.config.timeWindow - now;
686
- if (waitTime > 0) {
687
- await new Promise((resolve) => setTimeout(resolve, waitTime));
688
- } else {
689
- }
756
+ await new Promise((r) => setTimeout(r, Math.max(1, this.requests[0] + this.config.timeWindow - now)));
690
757
  }
691
758
  }
692
759
  getStats() {
693
760
  const now = Date.now();
694
- const windowStart = now - this.config.timeWindow;
695
- const currentRequests = this.requests.filter(
696
- (time) => time > windowStart
697
- ).length;
698
- return { currentRequests, limit: this.config.maxRequests };
761
+ return { currentRequests: this.requests.filter((t) => t > now - this.config.timeWindow).length, limit: this.config.maxRequests };
699
762
  }
700
763
  };
701
764
  var ExponentialBackoff = class {
702
765
  constructor(config) {
766
+ this.config = config;
703
767
  this.config = { jitter: true, ...config };
704
768
  }
705
769
  async execute(fn, onRetry) {
706
- let lastError = new Error("Unknown error");
707
- for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
770
+ let last;
771
+ for (let a = 0; a <= this.config.maxRetries; a++) {
708
772
  try {
709
773
  return await fn();
710
- } catch (error) {
711
- lastError = error;
712
- if (this.isClientError(error) && !this.isRateLimitError(error)) {
713
- throw error;
714
- }
715
- if (attempt === this.config.maxRetries) break;
716
- const delay = this.calculateDelay(attempt);
717
- if (onRetry) onRetry(attempt + 1, delay, error);
718
- await new Promise((resolve) => setTimeout(resolve, delay));
774
+ } catch (e) {
775
+ last = e;
776
+ if (e?.code === "ABORTED" || e?.code === "VALIDATION_ERROR" || e?.status >= 400 && e?.status < 500 && e?.status !== 429) throw e;
777
+ if (a === this.config.maxRetries) break;
778
+ let delay = Math.min(this.config.maxDelay, this.config.baseDelay * 2 ** a);
779
+ if (this.config.jitter) delay *= 0.5 + Math.random();
780
+ onRetry?.(a + 1, delay, e);
781
+ await new Promise((r) => setTimeout(r, delay));
719
782
  }
720
783
  }
721
- throw lastError;
722
- }
723
- calculateDelay(attempt) {
724
- let delay = this.config.baseDelay * Math.pow(2, attempt);
725
- delay = Math.min(delay, this.config.maxDelay);
726
- if (this.config.jitter) {
727
- delay = delay * (0.5 + Math.random());
728
- }
729
- return delay;
730
- }
731
- isClientError(error) {
732
- return error?.status >= 400 && error?.status < 500;
733
- }
734
- isRateLimitError(error) {
735
- return error?.status === 429;
784
+ throw last;
736
785
  }
737
786
  };
787
+ var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
788
+ CircuitState2[CircuitState2["CLOSED"] = 0] = "CLOSED";
789
+ CircuitState2[CircuitState2["OPEN"] = 1] = "OPEN";
790
+ CircuitState2[CircuitState2["HALF_OPEN"] = 2] = "HALF_OPEN";
791
+ return CircuitState2;
792
+ })(CircuitState || {});
738
793
  var CircuitBreaker = class {
739
- constructor() {
794
+ constructor(threshold = 5, reset = 3e4) {
795
+ this.threshold = threshold;
796
+ this.reset = reset;
740
797
  this.state = 0 /* CLOSED */;
741
798
  this.failures = 0;
742
- this.lastFailureTime = 0;
743
- this.failureThreshold = 5;
744
- this.resetTimeout = 3e4;
799
+ this.lastFailure = 0;
745
800
  }
746
- // 30 seconds
747
801
  isOpen() {
748
- if (this.state === 1 /* OPEN */) {
749
- if (Date.now() - this.lastFailureTime > this.resetTimeout) {
750
- this.state = 2 /* HALF_OPEN */;
751
- return false;
752
- }
753
- return true;
802
+ if (this.state === 1 /* OPEN */ && Date.now() - this.lastFailure >= this.reset) {
803
+ this.state = 2 /* HALF_OPEN */;
804
+ return false;
754
805
  }
755
- return false;
806
+ return this.state === 1 /* OPEN */;
756
807
  }
757
808
  recordSuccess() {
758
809
  this.failures = 0;
@@ -760,111 +811,35 @@ var CircuitBreaker = class {
760
811
  }
761
812
  recordFailure() {
762
813
  this.failures++;
763
- this.lastFailureTime = Date.now();
764
- if (this.failures >= this.failureThreshold) {
765
- this.state = 1 /* OPEN */;
766
- if (process.env.NODE_ENV === "development") {
767
- console.warn(
768
- "[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests."
769
- );
770
- }
771
- }
814
+ this.lastFailure = Date.now();
815
+ if (this.failures >= this.threshold) this.state = 1 /* OPEN */;
816
+ }
817
+ getState() {
818
+ return CircuitState[this.state];
772
819
  }
773
820
  };
774
821
  var RequestBatcher = class {
775
- constructor(batchWindow = 10, maxBatchSize = 20) {
776
- this.batchWindow = batchWindow;
822
+ constructor(windowMs = 10, maxBatchSize = 50) {
823
+ this.windowMs = windowMs;
777
824
  this.maxBatchSize = maxBatchSize;
778
- this.batch = [];
779
- this.processing = false;
780
- }
781
- async schedule(key, request) {
782
- return new Promise((resolve, reject) => {
783
- this.batch.push({ key, resolve, reject });
784
- if (this.batch.length >= this.maxBatchSize) {
785
- this.processBatch(request);
786
- } else if (!this.batchTimeout) {
787
- this.batchTimeout = setTimeout(
788
- () => this.processBatch(request),
789
- this.batchWindow
790
- );
791
- }
825
+ this.pending = /* @__PURE__ */ new Map();
826
+ }
827
+ schedule(key, request) {
828
+ const existing = this.pending.get(key);
829
+ if (existing) return existing;
830
+ const promise = new Promise((resolve, reject) => setTimeout(() => request().then(resolve, reject), this.windowMs));
831
+ this.pending.set(key, promise);
832
+ promise.finally(() => this.pending.delete(key)).catch(() => {
792
833
  });
834
+ return promise;
793
835
  }
794
- async processBatch(request) {
795
- if (this.processing || this.batch.length === 0) return;
796
- this.processing = true;
797
- if (this.batchTimeout) {
798
- clearTimeout(this.batchTimeout);
799
- this.batchTimeout = void 0;
800
- }
801
- const currentBatch = [...this.batch];
802
- this.batch = [];
803
- try {
804
- const result = await request();
805
- currentBatch.forEach((item) => item.resolve(result));
806
- } catch (error) {
807
- currentBatch.forEach((item) => item.reject(error));
808
- } finally {
809
- this.processing = false;
810
- if (this.batch.length > 0) {
811
- setTimeout(() => this.processBatch(request), 0);
812
- }
813
- }
836
+ clear() {
837
+ this.pending.clear();
814
838
  }
815
839
  };
816
840
 
817
- // src/content/utils.ts
818
- function validateSlug(slug) {
819
- if (!slug || typeof slug !== "string") {
820
- throw new Error("Slug must be a non-empty string");
821
- }
822
- if (!/^[a-z0-9-_]+$/.test(slug)) {
823
- throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
824
- }
825
- }
826
- function normalizeQuery(query) {
827
- const normalized = { ...query };
828
- normalized.page = Math.max(1, normalized.page || 1);
829
- normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
830
- normalized.order = normalized.order || "desc";
831
- if (normalized.page < 1) {
832
- throw new Error("Page must be greater than 0");
833
- }
834
- if (normalized.limit < 1 || normalized.limit > 100) {
835
- throw new Error("Limit must be between 1 and 100");
836
- }
837
- return normalized;
838
- }
839
- function buildQueryString(query) {
840
- const params = new URLSearchParams();
841
- if (query.page) params.append("page", query.page.toString());
842
- if (query.limit) params.append("limit", query.limit.toString());
843
- if (query.sort) params.append("sort", query.sort);
844
- if (query.order) params.append("order", query.order);
845
- if (query.search) params.append("search", query.search);
846
- if (query.include?.length) {
847
- params.append("include", query.include.join(","));
848
- }
849
- if (query.fields?.length) {
850
- params.append("fields", query.fields.join(","));
851
- }
852
- if (query.filter) {
853
- params.append("filter", JSON.stringify(query.filter));
854
- }
855
- return params.toString();
856
- }
857
- function measurePerformance(name, fn) {
858
- const start = performance.now();
859
- const result = fn();
860
- const end = performance.now();
861
- if (process.env.NODE_ENV === "development") {
862
- console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
863
- }
864
- return { result, duration: end - start };
865
- }
866
-
867
841
  // src/content/index.ts
842
+ init_utils();
868
843
  var ContentEngine = class {
869
844
  constructor(config) {
870
845
  this.config = config;
@@ -896,6 +871,12 @@ var ContentEngine = class {
896
871
  window.addEventListener("beforeunload", this.cleanup.bind(this));
897
872
  }
898
873
  }
874
+ /** Update runtime configuration without exposing internal mutation. */
875
+ updateConfig(config) {
876
+ this.config = config;
877
+ this.defaultRevalidate = config.revalidateTime ?? false;
878
+ this.cacheStrategy = config.cacheStrategy || "memory";
879
+ }
899
880
  /**
900
881
  * Fetch a Single Page with full strategy pipeline
901
882
  */
@@ -1054,7 +1035,7 @@ var ContentEngine = class {
1054
1035
  }
1055
1036
  }
1056
1037
  /**
1057
- * Fetch Global Settings with nested includes support
1038
+ * Fetch Global Settings
1058
1039
  */
1059
1040
  async getGlobals(options = {}) {
1060
1041
  const {
@@ -1106,6 +1087,9 @@ var ContentEngine = class {
1106
1087
  revalidate
1107
1088
  });
1108
1089
  if (!res.ok) {
1090
+ if (res.status === 408) {
1091
+ throw new Error("Request timeout");
1092
+ }
1109
1093
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1110
1094
  }
1111
1095
  const json = await res.json();
@@ -1154,12 +1138,12 @@ var ContentEngine = class {
1154
1138
  method: "GET",
1155
1139
  headers: this.getHeaders(),
1156
1140
  tags: fetchTags,
1157
- // ?? not || — see the constructor comment on defaultRevalidate for
1158
- // why: an explicit `revalidate: 0` on this call must not be
1159
- // discarded in favor of the engine's default.
1160
1141
  revalidate: options.revalidate ?? this.defaultRevalidate
1161
1142
  });
1162
1143
  if (!res.ok) {
1144
+ if (res.status === 408) {
1145
+ throw new Error("Request timeout");
1146
+ }
1163
1147
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1164
1148
  }
1165
1149
  const json = await res.json();
@@ -1191,12 +1175,15 @@ var ContentEngine = class {
1191
1175
  headers: this.getHeaders()
1192
1176
  });
1193
1177
  if (!res.ok) {
1178
+ if (res.status === 408) {
1179
+ throw new Error("Request timeout");
1180
+ }
1194
1181
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1195
1182
  }
1196
1183
  return await res.json();
1197
1184
  }
1198
1185
  /**
1199
- * Prefetch content for better performance
1186
+ * Prefetch content
1200
1187
  */
1201
1188
  async prefetch(urls) {
1202
1189
  if (typeof window !== "undefined" && "requestIdleCallback" in window) {
@@ -1209,41 +1196,19 @@ var ContentEngine = class {
1209
1196
  }
1210
1197
  /**
1211
1198
  * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
1212
- *
1213
- * ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
1214
- * This connects from the BROWSER TAB it's called in, and on message it
1215
- * only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
1216
- * invalidateCache below) in THAT browser tab's JS heap. In a typical
1217
- * Next.js deployment — and Cloudflare specifically, which is stateless
1218
- * per-request at the edge — that is a different process/isolate than the
1219
- * one that will render the NEXT server request for this content. So:
1220
- * - ✅ Useful for: a client component that reads from `nexus.content`
1221
- * directly in the browser and re-renders in place without a page
1222
- * navigation (e.g. a live-updating dashboard widget).
1223
- * - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
1224
- * already-loaded page updates" via a server-rendered page. That
1225
- * requires the backend's /api/revalidate webhook (see
1226
- * content.service.ts `pingNextRevalidateWebhook`) to have actually
1227
- * cleared the *server's* Data Cache, so the NEXT navigation or
1228
- * server request picks up fresh data. This SSE channel does not
1229
- * replace that — it's a complementary, browser-local optimization.
1230
- * If your symptom was "stale content after editing," fix the webhook
1231
- * wiring first; treat this method as an enhancement layered on top.
1232
1199
  */
1233
1200
  subscribeToUpdates(callback) {
1234
1201
  if (this.isServer || typeof EventSource === "undefined") {
1235
1202
  if (this.config.debug) {
1236
1203
  console.warn(
1237
- "[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."
1204
+ "[NexusHub] subscribeToUpdates() called in a non-browser environment."
1238
1205
  );
1239
1206
  }
1240
1207
  return () => {
1241
1208
  };
1242
1209
  }
1243
1210
  if (!this.config.apiKey) {
1244
- console.warn(
1245
- "[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
1246
- );
1211
+ console.warn("[NexusHub] subscribeToUpdates(): no apiKey configured.");
1247
1212
  }
1248
1213
  let eventSource = null;
1249
1214
  let retryCount = 0;
@@ -1407,6 +1372,9 @@ var ContentEngine = class {
1407
1372
  `Page '${slug}' not found. Check your Dashboard or Seed data.`
1408
1373
  );
1409
1374
  }
1375
+ if (res.status === 408) {
1376
+ throw new Error("Request timeout");
1377
+ }
1410
1378
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1411
1379
  }
1412
1380
  const json = await res.json();
@@ -1441,6 +1409,9 @@ var ContentEngine = class {
1441
1409
  revalidate
1442
1410
  });
1443
1411
  if (!res.ok) {
1412
+ if (res.status === 408) {
1413
+ throw new Error("Request timeout");
1414
+ }
1444
1415
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1445
1416
  }
1446
1417
  const json = await res.json();
@@ -1527,7 +1498,6 @@ var ContentEngine = class {
1527
1498
  const response = await fetch(url, {
1528
1499
  ...fetchOptions,
1529
1500
  ...nextConfig,
1530
- // Inject Next.js tags
1531
1501
  signal: controller.signal
1532
1502
  });
1533
1503
  clearTimeout(id);
@@ -1540,7 +1510,8 @@ var ContentEngine = class {
1540
1510
  getHeaders() {
1541
1511
  const headers = {
1542
1512
  "Content-Type": "application/json",
1543
- "X-Nexus-Client": "client-sdk/1.0.0",
1513
+ "X-Nexus-Client": `client-sdk/${this.config.sdkVersion ?? "1.1.0"}`,
1514
+ "X-Nexus-Request-ID": typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`,
1544
1515
  "X-Nexus-Project": this.config.projectId
1545
1516
  };
1546
1517
  if (this.config.apiKey) {
@@ -1551,13 +1522,22 @@ var ContentEngine = class {
1551
1522
  isCacheValid(metadata) {
1552
1523
  return Date.now() < metadata.expiresAt;
1553
1524
  }
1525
+ /**
1526
+ * 🛡️ FIX: Normalizes timeout errors (AbortError, 408, or Timeout messages)
1527
+ * into clean, standardized "Request timeout" errors.
1528
+ */
1554
1529
  normalizeError(error, context) {
1555
1530
  if (error instanceof Error) {
1556
- if (error.name === "AbortError") {
1531
+ const msg = error.message.toLowerCase();
1532
+ if (error.name === "AbortError" || error.message.includes("408") || msg.includes("timeout") || msg.includes("aborted")) {
1557
1533
  return new Error(`${context}: Request timeout`);
1558
1534
  }
1559
1535
  return error;
1560
1536
  }
1537
+ const str = String(error).toLowerCase();
1538
+ if (str.includes("408") || str.includes("timeout") || str.includes("aborted")) {
1539
+ return new Error(`${context}: Request timeout`);
1540
+ }
1561
1541
  return new Error(`${context}: ${String(error)}`);
1562
1542
  }
1563
1543
  cancelRequests() {
@@ -1574,9 +1554,12 @@ var ContentEngine = class {
1574
1554
  }
1575
1555
  };
1576
1556
 
1557
+ // src/analytics/tracker.ts
1558
+ init_config();
1559
+
1577
1560
  // src/analytics/fingerprint.ts
1578
1561
  var cachedEntropy = null;
1579
- var getDeviceEntropy = async () => {
1562
+ var getDeviceEntropy = async (allowFingerprinting = true) => {
1580
1563
  if (cachedEntropy) return cachedEntropy;
1581
1564
  if (typeof window === "undefined") return {};
1582
1565
  const nav = window.navigator;
@@ -1591,7 +1574,7 @@ var getDeviceEntropy = async () => {
1591
1574
  platform: nav.platform,
1592
1575
  language: nav.language,
1593
1576
  touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
1594
- canvas_hash: await generateCanvasHash()
1577
+ canvas_hash: allowFingerprinting ? await generateCanvasHash() : void 0
1595
1578
  };
1596
1579
  return cachedEntropy;
1597
1580
  };
@@ -1604,7 +1587,6 @@ var generateCanvasHash = async () => {
1604
1587
  canvas.height = 50;
1605
1588
  ctx.textBaseline = "top";
1606
1589
  ctx.font = '16px "Arial"';
1607
- ctx.textBaseline = "alphabetic";
1608
1590
  ctx.fillStyle = "#f60";
1609
1591
  ctx.fillRect(125, 1, 62, 20);
1610
1592
  ctx.fillStyle = "#069";
@@ -1623,12 +1605,12 @@ var generateCanvasHash = async () => {
1623
1605
  return "";
1624
1606
  }
1625
1607
  };
1626
- var getVisitorId = async () => {
1608
+ var getVisitorId = async (allowFingerprinting = true) => {
1627
1609
  if (typeof window === "undefined") return "server_visitor";
1628
1610
  const STORAGE_KEY = "nexus_vid";
1629
1611
  let vid = localStorage.getItem(STORAGE_KEY);
1630
1612
  if (!vid) {
1631
- const entropy = await getDeviceEntropy();
1613
+ const entropy = await getDeviceEntropy(allowFingerprinting);
1632
1614
  const random = Math.random().toString(36).substring(2, 15);
1633
1615
  const timestamp = Date.now().toString(36);
1634
1616
  const fingerprint = [
@@ -1636,7 +1618,7 @@ var getVisitorId = async () => {
1636
1618
  entropy.hardware_concurrency,
1637
1619
  entropy.timezone_offset,
1638
1620
  entropy.platform,
1639
- entropy.canvas_hash
1621
+ entropy.canvas_hash || "standard_entropy"
1640
1622
  ].join("|");
1641
1623
  let hash = 0;
1642
1624
  for (let i = 0; i < fingerprint.length; i++) {
@@ -1847,7 +1829,6 @@ var EventStorage = class {
1847
1829
  var eventStorage = new EventStorage();
1848
1830
 
1849
1831
  // src/analytics/tracker.ts
1850
- var SDK_VERSION = "0.1.0";
1851
1832
  var safeGetItem = (key) => {
1852
1833
  try {
1853
1834
  return localStorage.getItem(key);
@@ -1883,6 +1864,8 @@ var Tracker = class {
1883
1864
  this.visitorId = "";
1884
1865
  this.anonymousId = "";
1885
1866
  this.isFlushing = false;
1867
+ this.flushPromise = null;
1868
+ this.batchSize = 25;
1886
1869
  this.config = config;
1887
1870
  this.endpoint = `${this.config.analyticsUrl}/api/collect`;
1888
1871
  this.circuitBreaker = new CircuitBreaker();
@@ -1893,7 +1876,9 @@ var Tracker = class {
1893
1876
  }
1894
1877
  }
1895
1878
  async initSession() {
1896
- this.visitorId = await getVisitorId();
1879
+ this.visitorId = await getVisitorId(
1880
+ this.config.privacy?.fingerprinting ?? false
1881
+ );
1897
1882
  let anonId = safeGetItem("nexus_anon_id");
1898
1883
  if (!anonId) {
1899
1884
  anonId = `anon_${generateUUID().replace(/-/g, "")}`;
@@ -1906,8 +1891,8 @@ var Tracker = class {
1906
1891
  const SESSION_TIMEOUT = 30 * 60 * 1e3;
1907
1892
  const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
1908
1893
  if (isExpired) {
1909
- const uuid = generateUUID().replace(/-/g, "").substring(0, 16);
1910
- sid = `sess_${uuid}_${now}`;
1894
+ const uuid2 = generateUUID().replace(/-/g, "").substring(0, 16);
1895
+ sid = `sess_${uuid2}_${now}`;
1911
1896
  safeSetItem("nexus_sid", sid);
1912
1897
  this.sessionStart = now;
1913
1898
  }
@@ -1917,7 +1902,9 @@ var Tracker = class {
1917
1902
  async send(eventType, data = {}, eventName, ecommerce) {
1918
1903
  if (typeof window === "undefined") return;
1919
1904
  safeSetItem("nexus_last_active", Date.now().toString());
1920
- const entropy = await getDeviceEntropy();
1905
+ const entropy = await getDeviceEntropy(
1906
+ this.config.privacy?.fingerprinting ?? false
1907
+ );
1921
1908
  const perfMetrics = vitalsCollector.getMetricsSnapshot();
1922
1909
  const utmParams = extractUtmParams(window.location.href);
1923
1910
  const payload = {
@@ -1928,6 +1915,7 @@ var Tracker = class {
1928
1915
  messageId: generateUUID(),
1929
1916
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
1930
1917
  version: SDK_VERSION,
1918
+ // Dynamically synced to SDK 1.1.0
1931
1919
  url: window.location.href,
1932
1920
  referrer: document.referrer,
1933
1921
  userAgent: window.navigator.userAgent,
@@ -1945,8 +1933,8 @@ var Tracker = class {
1945
1933
  hardwareConcurrency: entropy.hardware_concurrency,
1946
1934
  deviceMemory: entropy.device_memory,
1947
1935
  pixelRatio: entropy.pixel_ratio,
1948
- canvasFingerprint: entropy.canvas_hash,
1949
- platform: entropy.platform
1936
+ platform: entropy.platform,
1937
+ ...this.config.privacy?.fingerprinting ? { canvasFingerprint: entropy.canvas_hash } : {}
1950
1938
  },
1951
1939
  visitorIdLocal: this.visitorId,
1952
1940
  utm: utmParams
@@ -1966,53 +1954,69 @@ var Tracker = class {
1966
1954
  });
1967
1955
  }
1968
1956
  async flushQueue(useBeacon = false) {
1969
- if (this.isFlushing) return;
1970
- if (this.circuitBreaker.isOpen()) return;
1971
- this.isFlushing = true;
1972
- try {
1973
- const storedEvents = await eventStorage.peek(20);
1974
- if (storedEvents.length === 0) {
1975
- this.isFlushing = false;
1976
- return;
1977
- }
1978
- const payloads = storedEvents.map((e) => e.payload);
1979
- const promises = payloads.map(
1980
- (event) => fetch(this.endpoint, {
1981
- method: "POST",
1982
- headers: {
1983
- "Content-Type": "application/json",
1984
- Authorization: `Bearer ${this.config.apiKey}`
1985
- },
1986
- body: JSON.stringify(event),
1987
- keepalive: useBeacon
1988
- })
1989
- );
1990
- const results = await Promise.allSettled(promises);
1991
- const successIds = [];
1992
- let failureCount = 0;
1993
- results.forEach((res, index) => {
1994
- if (res.status === "fulfilled" && res.value.ok) {
1995
- successIds.push(storedEvents[index].id);
1957
+ if (this.flushPromise) return this.flushPromise;
1958
+ this.flushPromise = (async () => {
1959
+ if (this.circuitBreaker.isOpen()) return;
1960
+ try {
1961
+ const storedEvents = await eventStorage.peek(this.batchSize);
1962
+ if (!storedEvents.length) return;
1963
+ const payloads = storedEvents.map((e) => e.payload);
1964
+ const headers = {
1965
+ "Content-Type": "application/json",
1966
+ Authorization: this.config.apiKey ? `Bearer ${this.config.apiKey}` : "",
1967
+ "X-Nexus-Client": `analytics/${SDK_VERSION}`,
1968
+ "X-Nexus-Project": this.config.projectId
1969
+ };
1970
+ let response;
1971
+ try {
1972
+ response = await fetch(
1973
+ `${this.config.analyticsUrl}/api/collect/batch`,
1974
+ {
1975
+ method: "POST",
1976
+ headers,
1977
+ body: JSON.stringify({ events: payloads }),
1978
+ keepalive: useBeacon
1979
+ }
1980
+ );
1981
+ } catch {
1982
+ response = void 0;
1983
+ }
1984
+ if (!response || response.status === 404 || response.status === 405) {
1985
+ const results = await Promise.allSettled(
1986
+ payloads.map(
1987
+ (event) => fetch(this.endpoint, {
1988
+ method: "POST",
1989
+ headers,
1990
+ body: JSON.stringify(event),
1991
+ keepalive: useBeacon
1992
+ })
1993
+ )
1994
+ );
1995
+ const successIds = results.flatMap(
1996
+ (r, i) => r.status === "fulfilled" && r.value.ok ? [storedEvents[i].id] : []
1997
+ );
1998
+ if (successIds.length) await eventStorage.remove(successIds);
1999
+ if (successIds.length === storedEvents.length) {
2000
+ this.circuitBreaker.recordSuccess();
2001
+ } else {
2002
+ this.circuitBreaker.recordFailure();
2003
+ }
2004
+ } else if (response.ok) {
2005
+ await eventStorage.remove(storedEvents.map((e) => e.id));
2006
+ this.circuitBreaker.recordSuccess();
1996
2007
  } else {
1997
- failureCount++;
2008
+ this.circuitBreaker.recordFailure();
1998
2009
  }
1999
- });
2000
- if (successIds.length > 0) {
2001
- await eventStorage.remove(successIds);
2002
- this.circuitBreaker.recordSuccess();
2003
- }
2004
- if (failureCount > 0) {
2010
+ } catch (err) {
2005
2011
  this.circuitBreaker.recordFailure();
2012
+ if (this.config.debug) {
2013
+ console.warn("[GN-Apex] Analytics flush deferred:", err);
2014
+ }
2015
+ } finally {
2016
+ this.flushPromise = null;
2006
2017
  }
2007
- } catch (err) {
2008
- console.error("[NexusHub] Network Error:", err);
2009
- this.circuitBreaker.recordFailure();
2010
- } finally {
2011
- this.isFlushing = false;
2012
- if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
2013
- setTimeout(() => this.flushQueue(), 100);
2014
- }
2015
- }
2018
+ })();
2019
+ return this.flushPromise;
2016
2020
  }
2017
2021
  getSession() {
2018
2022
  return this.sessionId;
@@ -2291,70 +2295,35 @@ var AnalyticsEngine = class {
2291
2295
  }
2292
2296
  setupVideoTracking() {
2293
2297
  if (typeof document === "undefined") return;
2294
- const attachVideoListeners = (video) => {
2295
- if (video.__nexus_tracked) return;
2296
- video.__nexus_tracked = true;
2297
- const src = video.src || video.currentSrc || "unknown";
2298
- let milestone50Fired = false;
2299
- video.addEventListener("play", () => {
2300
- this.tracker.send(
2301
- "custom_event",
2302
- { event_name: "video_play", src },
2303
- "video_play"
2304
- );
2305
- });
2306
- video.addEventListener("pause", () => {
2307
- this.tracker.send(
2308
- "custom_event",
2309
- {
2310
- event_name: "video_pause",
2311
- src,
2312
- position_seconds: Math.round(video.currentTime)
2313
- },
2314
- "video_pause"
2315
- );
2316
- });
2298
+ const attach = (video) => {
2299
+ const v = video;
2300
+ if (v.__gnexusTracked) return;
2301
+ v.__gnexusTracked = true;
2302
+ v.__gnexusMilestones = /* @__PURE__ */ new Set();
2303
+ const src = () => video.currentSrc || video.src || "unknown";
2304
+ const emit = (name, data = {}) => this.tracker.send("media", { media_type: "video", event_name: name, src: src(), ...data }, name);
2305
+ video.addEventListener("loadedmetadata", () => emit("video_loaded", { duration_seconds: Number.isFinite(video.duration) ? Math.round(video.duration) : void 0 }));
2306
+ video.addEventListener("play", () => emit("video_play", { position_seconds: Math.round(video.currentTime) }));
2307
+ video.addEventListener("pause", () => emit("video_pause", { position_seconds: Math.round(video.currentTime) }));
2308
+ video.addEventListener("seeking", () => emit("video_seek", { position_seconds: Math.round(video.currentTime) }));
2309
+ video.addEventListener("ended", () => emit("video_complete", { duration_seconds: Math.round(video.duration) }));
2310
+ video.addEventListener("error", () => emit("video_error"));
2317
2311
  video.addEventListener("timeupdate", () => {
2318
- if (!video.duration || video.duration === Infinity) return;
2319
- const pct = video.currentTime / video.duration;
2320
- if (pct >= 0.5 && !milestone50Fired) {
2321
- milestone50Fired = true;
2322
- this.tracker.send(
2323
- "custom_event",
2324
- {
2325
- event_name: "video_50_percent",
2326
- src
2327
- },
2328
- "video_50_percent"
2329
- );
2312
+ if (!Number.isFinite(video.duration) || video.duration <= 0) return;
2313
+ for (const milestone of [25, 50, 75, 90, 100]) {
2314
+ if (video.currentTime / video.duration * 100 >= milestone && !v.__gnexusMilestones.has(milestone)) {
2315
+ v.__gnexusMilestones.add(milestone);
2316
+ emit(`video_${milestone}_percent`, { progress: milestone / 100 });
2317
+ }
2330
2318
  }
2331
2319
  });
2332
- video.addEventListener("ended", () => {
2333
- this.tracker.send(
2334
- "custom_event",
2335
- {
2336
- event_name: "video_complete",
2337
- src,
2338
- duration_seconds: Math.round(video.duration)
2339
- },
2340
- "video_complete"
2341
- );
2342
- });
2343
2320
  };
2344
- document.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2345
- const observer = new MutationObserver((mutations) => {
2346
- mutations.forEach((m) => {
2347
- m.addedNodes.forEach((node) => {
2348
- if (node instanceof HTMLVideoElement) {
2349
- attachVideoListeners(node);
2350
- }
2351
- if (node instanceof Element) {
2352
- node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2353
- }
2354
- });
2355
- });
2356
- });
2357
- observer.observe(document.body, { childList: true, subtree: true });
2321
+ document.querySelectorAll("video").forEach((v) => attach(v));
2322
+ const observer = new MutationObserver((ms) => ms.forEach((m) => m.addedNodes.forEach((n) => {
2323
+ if (n instanceof HTMLVideoElement) attach(n);
2324
+ if (n instanceof Element) n.querySelectorAll("video").forEach((v) => attach(v));
2325
+ })));
2326
+ if (document.body) observer.observe(document.body, { childList: true, subtree: true });
2358
2327
  this.cleanupFns.push(() => observer.disconnect());
2359
2328
  }
2360
2329
  setupErrorTracking() {
@@ -2543,65 +2512,397 @@ function getSelector(el, depth = 0) {
2543
2512
  return `${parent}${self}`;
2544
2513
  }
2545
2514
 
2515
+ // src/errors.ts
2516
+ var NexusError = class extends Error {
2517
+ constructor(message, code = "UNKNOWN", status, requestId, details, retryable = false, cause) {
2518
+ super(message);
2519
+ this.code = code;
2520
+ this.status = status;
2521
+ this.requestId = requestId;
2522
+ this.details = details;
2523
+ this.retryable = retryable;
2524
+ this.cause = cause;
2525
+ this.name = "NexusError";
2526
+ Object.setPrototypeOf(this, new.target.prototype);
2527
+ }
2528
+ };
2529
+ var isNexusError = (e) => e instanceof NexusError;
2530
+
2531
+ // src/events.ts
2532
+ var NexusEventBus = class {
2533
+ constructor() {
2534
+ this.listeners = /* @__PURE__ */ new Map();
2535
+ }
2536
+ on(event, listener) {
2537
+ const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
2538
+ set.add(listener);
2539
+ this.listeners.set(event, set);
2540
+ return () => set.delete(listener);
2541
+ }
2542
+ emit(event, payload) {
2543
+ this.listeners.get(event)?.forEach((l) => {
2544
+ try {
2545
+ l(payload);
2546
+ } catch {
2547
+ }
2548
+ });
2549
+ }
2550
+ clear() {
2551
+ this.listeners.clear();
2552
+ }
2553
+ };
2554
+
2555
+ // src/http.ts
2556
+ var uuid = () => typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`;
2557
+ var NexusHttpClient = class {
2558
+ constructor(config, events = new NexusEventBus()) {
2559
+ this.config = config;
2560
+ this.events = events;
2561
+ this.breaker = new CircuitBreaker();
2562
+ this.limiter = new RateLimiter({
2563
+ maxRequests: config.debug ? 200 : 100,
2564
+ timeWindow: 6e4
2565
+ });
2566
+ this.backoff = new ExponentialBackoff({
2567
+ maxRetries: config.retries ?? 3,
2568
+ baseDelay: 150,
2569
+ maxDelay: 8e3,
2570
+ jitter: true
2571
+ });
2572
+ }
2573
+ updateConfig(config) {
2574
+ this.config = config;
2575
+ }
2576
+ async request(input, options = {}) {
2577
+ if (this.breaker.isOpen()) {
2578
+ throw new NexusError(
2579
+ "GN-Apex service circuit is open",
2580
+ "CIRCUIT_OPEN",
2581
+ void 0,
2582
+ void 0,
2583
+ void 0,
2584
+ true
2585
+ );
2586
+ }
2587
+ const requestId = uuid();
2588
+ const url = input;
2589
+ const started = Date.now();
2590
+ const headers = new Headers(options.headers);
2591
+ headers.set("X-Nexus-Request-ID", requestId);
2592
+ headers.set(
2593
+ "X-Nexus-Client",
2594
+ `gnexus-sdk/${this.config.sdkVersion ?? "1.1.0"}`
2595
+ );
2596
+ headers.set("X-Nexus-Project", this.config.projectId);
2597
+ if (this.config.apiKey)
2598
+ headers.set("Authorization", `Bearer ${this.config.apiKey}`);
2599
+ this.events.emit("request:start", {
2600
+ requestId,
2601
+ url,
2602
+ method: options.method ?? "GET"
2603
+ });
2604
+ try {
2605
+ await this.limiter.checkLimit();
2606
+ const response = await this.backoff.execute(async () => {
2607
+ const controller = new AbortController();
2608
+ const timeout = options.timeout ?? this.config.timeout ?? 1e4;
2609
+ const timer = setTimeout(() => controller.abort(), timeout);
2610
+ try {
2611
+ return await fetch(url, {
2612
+ ...options,
2613
+ headers,
2614
+ signal: options.signal ?? controller.signal
2615
+ });
2616
+ } catch (e) {
2617
+ if (e?.name === "AbortError") {
2618
+ throw new NexusError(
2619
+ `Request timed out after ${timeout}ms`,
2620
+ "TIMEOUT",
2621
+ void 0,
2622
+ requestId,
2623
+ void 0,
2624
+ true,
2625
+ e
2626
+ );
2627
+ }
2628
+ throw new NexusError(
2629
+ "Network request failed",
2630
+ "NETWORK_ERROR",
2631
+ void 0,
2632
+ requestId,
2633
+ void 0,
2634
+ true,
2635
+ e
2636
+ );
2637
+ } finally {
2638
+ clearTimeout(timer);
2639
+ }
2640
+ });
2641
+ if (!response.ok) {
2642
+ const retryable = response.status === 408 || response.status === 429 || response.status >= 500;
2643
+ let details;
2644
+ try {
2645
+ details = await response.clone().json();
2646
+ } catch {
2647
+ }
2648
+ const code = response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR";
2649
+ const err = new NexusError(
2650
+ `API request failed (${response.status})`,
2651
+ code,
2652
+ response.status,
2653
+ response.headers.get("x-nexus-request-id") ?? requestId,
2654
+ details,
2655
+ retryable
2656
+ );
2657
+ throw err;
2658
+ }
2659
+ this.breaker.recordSuccess();
2660
+ this.events.emit("request:end", {
2661
+ requestId,
2662
+ url,
2663
+ status: response.status,
2664
+ duration: Date.now() - started
2665
+ });
2666
+ return response;
2667
+ } catch (e) {
2668
+ if (e instanceof NexusError) {
2669
+ if (e.retryable || e.status && (e.status >= 500 || e.status === 429)) {
2670
+ this.breaker.recordFailure();
2671
+ } else {
2672
+ this.breaker.recordSuccess();
2673
+ }
2674
+ } else {
2675
+ this.breaker.recordFailure();
2676
+ }
2677
+ this.events.emit("error", { error: e });
2678
+ throw e;
2679
+ }
2680
+ }
2681
+ getStats() {
2682
+ return {
2683
+ rateLimit: this.limiter.getStats(),
2684
+ circuit: this.breaker.getState()
2685
+ };
2686
+ }
2687
+ };
2688
+
2689
+ // src/flags.ts
2690
+ var FeatureFlags = class {
2691
+ constructor(initial) {
2692
+ this.values = {};
2693
+ this.values = { ...initial };
2694
+ }
2695
+ set(values) {
2696
+ this.values = { ...this.values, ...values };
2697
+ }
2698
+ isEnabled(key, fallback = false) {
2699
+ const v = this.values[key];
2700
+ return typeof v === "boolean" ? v : fallback;
2701
+ }
2702
+ get(key, fallback) {
2703
+ return this.values[key] ?? fallback;
2704
+ }
2705
+ all() {
2706
+ return { ...this.values };
2707
+ }
2708
+ };
2709
+ var RemoteConfig = class {
2710
+ constructor() {
2711
+ this.values = {};
2712
+ }
2713
+ set(values) {
2714
+ this.values = { ...this.values, ...values };
2715
+ }
2716
+ get(key, fallback) {
2717
+ return this.values[key] ?? fallback;
2718
+ }
2719
+ all() {
2720
+ return { ...this.values };
2721
+ }
2722
+ };
2723
+
2724
+ // src/diagnostics.ts
2725
+ 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 });
2726
+
2546
2727
  // src/client.ts
2547
- var DEFAULT_REVALIDATE_SECONDS = false;
2548
2728
  var NexusClient = class {
2549
- constructor(config) {
2550
- const fullConfig = getFullConfig(config);
2729
+ constructor(config = {}) {
2730
+ const full = getFullConfig(config);
2551
2731
  this.config = {
2552
- debug: config?.debug ?? false,
2553
- cacheStrategy: config?.cacheStrategy ?? "memory",
2554
- // Only fall through to the safe default when revalidateTime is
2555
- // genuinely *unset* (undefined). An explicit `false` from the caller
2556
- // is a deliberate "never revalidate" choice and must be respected,
2557
- // not silently upgraded — `??` (not `||`) is required here so that
2558
- // `0` (revalidate on every request) also passes through untouched
2559
- // instead of being treated as falsy.
2560
- revalidateTime: config?.revalidateTime ?? DEFAULT_REVALIDATE_SECONDS,
2561
- timeout: config?.timeout ?? 1e4,
2562
- retries: config?.retries ?? 3,
2563
- ...fullConfig
2732
+ debug: false,
2733
+ cacheStrategy: "memory",
2734
+ revalidateTime: false,
2735
+ timeout: 1e4,
2736
+ retries: 3,
2737
+ sdkVersion: SDK_VERSION,
2738
+ cacheInvalidation: "platform",
2739
+ environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production",
2740
+ autoTracking: true,
2741
+ publicKeyOnly: true,
2742
+ // 🚀 Default: fingerprinting: true (Active by default for high-precision analytics)
2743
+ privacy: { analytics: true, fingerprinting: true, redact: true },
2744
+ ...full,
2745
+ ...config
2564
2746
  };
2565
2747
  const errors = validateConfig(this.config);
2566
- if (errors.length > 0) {
2567
- console.warn("\u26A0\uFE0F NexusHub: Configuration issues:", errors.join(", "));
2568
- if (!this.config.projectId) {
2569
- console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Tracking will fail.");
2570
- }
2748
+ if (errors.length && this.config.debug) {
2749
+ console.warn("[GN-Apex] Configuration warnings:", errors);
2571
2750
  }
2751
+ this.events = new NexusEventBus();
2752
+ this.http = new NexusHttpClient(this.config, this.events);
2753
+ this.flags = new FeatureFlags();
2754
+ this.remoteConfig = new RemoteConfig();
2572
2755
  this.content = new ContentEngine(this.config);
2573
- if (typeof window !== "undefined") {
2756
+ if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false) {
2574
2757
  this.analytics = new AnalyticsEngine(this.config);
2575
2758
  this.analytics.start();
2576
2759
  }
2577
2760
  }
2578
2761
  /**
2579
- * Helper alias for cleaner content fetching.
2762
+ * Helper alias for cleaner page content fetching.
2580
2763
  */
2581
2764
  getPage(slug, options) {
2582
2765
  return this.content.getPage(slug, options);
2583
2766
  }
2584
2767
  /**
2585
- * Returns a readonly snapshot of the current config.
2768
+ * Returns a readonly snapshot of the active configuration.
2586
2769
  */
2587
2770
  getConfig() {
2588
- return { ...this.config };
2771
+ return Object.freeze({
2772
+ ...this.config,
2773
+ privacy: { ...this.config.privacy }
2774
+ });
2589
2775
  }
2590
2776
  /**
2591
- * Updates specific config fields at runtime.
2592
- * Replaces the previous pattern of `(nexus as any).config.projectId = x`
2593
- * which bypassed TypeScript and mutated internal state unsafely.
2777
+ * Updates runtime configuration dynamically without breaking active listeners.
2594
2778
  */
2595
2779
  updateConfig(updates) {
2596
- this.config = { ...this.config, ...updates };
2780
+ this.config = {
2781
+ ...this.config,
2782
+ ...updates,
2783
+ privacy: { ...this.config.privacy, ...updates.privacy }
2784
+ };
2785
+ this.http.updateConfig(this.config);
2786
+ this.content.updateConfig?.(this.config);
2787
+ if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false && !this.analytics) {
2788
+ this.analytics = new AnalyticsEngine(this.config);
2789
+ this.analytics.start();
2790
+ }
2791
+ if (this.config.privacy?.analytics === false) {
2792
+ this.analytics?.stop();
2793
+ }
2794
+ }
2795
+ /**
2796
+ * Returns deep system diagnostics including HTTP and cache performance.
2797
+ */
2798
+ diagnostics() {
2799
+ return {
2800
+ ...getDiagnostics(SDK_VERSION, this.config.environment ?? "production"),
2801
+ cache: this.content.getCacheStats(),
2802
+ http: this.http.getStats(),
2803
+ analyticsQueue: this.analytics ? void 0 : 0
2804
+ };
2805
+ }
2806
+ /**
2807
+ * Gracefully terminates background tasks and cleans up listeners.
2808
+ */
2809
+ destroy() {
2810
+ this.analytics?.stop(true);
2811
+ this.content.cleanup?.();
2812
+ this.events.clear();
2597
2813
  }
2598
2814
  };
2599
2815
  var nexus = new NexusClient();
2600
- var createNexusClient = (config) => new NexusClient(config);
2816
+ var createNexusClient = (config = {}) => new NexusClient(config);
2601
2817
 
2602
2818
  // src/index.ts
2603
2819
  init_config();
2604
- var VERSION = "0.0.1";
2820
+
2821
+ // src/notifications/push-client.ts
2822
+ var NexusPushClient = class {
2823
+ constructor(config) {
2824
+ this.config = config;
2825
+ }
2826
+ /**
2827
+ * Helper to convert Base64 VAPID key to Uint8Array for WebPush security
2828
+ */
2829
+ urlBase64ToUint8Array(base64String) {
2830
+ const padding = "=".repeat((4 - base64String.length % 4) % 4);
2831
+ const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");
2832
+ const rawData = window.atob(base64);
2833
+ const outputArray = new Uint8Array(rawData.length);
2834
+ for (let i = 0; i < rawData.length; ++i) {
2835
+ outputArray[i] = rawData.charCodeAt(i);
2836
+ }
2837
+ return outputArray;
2838
+ }
2839
+ /**
2840
+ * Requests browser notification permissions and registers the WebPush subscription
2841
+ */
2842
+ async requestSubscription(serviceWorkerPath = "/sw.js") {
2843
+ if (typeof window === "undefined" || !("serviceWorker" in navigator) || !("PushManager" in window)) {
2844
+ console.warn(
2845
+ "[NexusHub] Push notifications are not supported in this browser environment."
2846
+ );
2847
+ return false;
2848
+ }
2849
+ try {
2850
+ const permission = await Notification.requestPermission();
2851
+ if (permission !== "granted") {
2852
+ console.warn("[NexusHub] Notification permission denied by user.");
2853
+ return false;
2854
+ }
2855
+ const vapidRes = await fetch(
2856
+ `${this.config.apiUrl}/notifications/vapid-key`,
2857
+ {
2858
+ headers: {
2859
+ Authorization: `Bearer ${this.config.apiKey}`,
2860
+ "x-nexus-project": this.config.projectId
2861
+ }
2862
+ }
2863
+ );
2864
+ if (!vapidRes.ok) throw new Error("Failed to fetch VAPID public key.");
2865
+ const { publicKey } = await vapidRes.json();
2866
+ const registration = await navigator.serviceWorker.register(serviceWorkerPath);
2867
+ await navigator.serviceWorker.ready;
2868
+ const subscription = await registration.pushManager.subscribe({
2869
+ userVisibleOnly: true,
2870
+ // 🚀 RESOLVED: Cast as 'any' to bypass strict DOM BufferSource typings
2871
+ applicationServerKey: this.urlBase64ToUint8Array(publicKey)
2872
+ });
2873
+ const rawSub = subscription.toJSON();
2874
+ if (!rawSub.endpoint || !rawSub.keys?.auth || !rawSub.keys?.p256dh) {
2875
+ throw new Error(
2876
+ "Malformed subscription payload received from browser."
2877
+ );
2878
+ }
2879
+ const res = await fetch(
2880
+ `${this.config.apiUrl}/notifications/project/${this.config.projectId}/subscribe`,
2881
+ {
2882
+ method: "POST",
2883
+ headers: {
2884
+ "Content-Type": "application/json",
2885
+ Authorization: `Bearer ${this.config.apiKey}`,
2886
+ "x-nexus-project": this.config.projectId
2887
+ },
2888
+ body: JSON.stringify({
2889
+ endpoint: rawSub.endpoint,
2890
+ auth: rawSub.keys.auth,
2891
+ p256dh: rawSub.keys.p256dh,
2892
+ provider: "WEB_PUSH"
2893
+ })
2894
+ }
2895
+ );
2896
+ return res.ok;
2897
+ } catch (err) {
2898
+ console.error("[NexusHub] WebPush subscription failed:", err);
2899
+ return false;
2900
+ }
2901
+ }
2902
+ };
2903
+
2904
+ // src/index.ts
2905
+ var VERSION = "1.1.0";
2605
2906
  export {
2606
2907
  AnalyticsEngine,
2607
2908
  BrowserCache,
@@ -2609,15 +2910,25 @@ export {
2609
2910
  ContentEngine,
2610
2911
  DEFAULT_ANALYTICS_URL,
2611
2912
  DEFAULT_API_URL,
2913
+ FeatureFlags,
2612
2914
  LOCAL_NEST_URL,
2613
2915
  LOCAL_RUST_URL,
2614
2916
  LocalCacheProxy as LocalCache,
2615
2917
  MemoryCache,
2616
2918
  NexusClient,
2919
+ NexusError,
2920
+ NexusEventBus,
2921
+ NexusHttpClient,
2922
+ NexusPushClient,
2923
+ RemoteConfig,
2924
+ SDK_VERSION,
2617
2925
  VERSION,
2618
2926
  createNexusClient,
2927
+ getDiagnostics,
2619
2928
  getEnvConfig,
2620
2929
  getFullConfig,
2930
+ hasRequiredConfig,
2931
+ isNexusError,
2621
2932
  mergeConfigs,
2622
2933
  nexus,
2623
2934
  validateConfig