@nexushub/client 0.8.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -1,16 +1,426 @@
1
1
  import {
2
+ SDK_VERSION,
3
+ buildQueryString,
2
4
  getFullConfig,
3
- validateConfig
4
- } from "./chunk-HUABKET6.js";
5
+ measurePerformance,
6
+ normalizeQuery,
7
+ validateConfig,
8
+ validateSlug
9
+ } from "./chunk-CBSACQXV.js";
10
+
11
+ // src/components/NexusRenderer.tsx
12
+ import {
13
+ useCallback,
14
+ useEffect,
15
+ useMemo,
16
+ useRef,
17
+ useState
18
+ } from "react";
19
+ import { createPortal } from "react-dom";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+ function cn(...classes) {
22
+ return classes.filter(Boolean).join(" ");
23
+ }
24
+ var DEFAULT_CHART = {
25
+ chartType: "bar",
26
+ title: "Untitled Chart",
27
+ categories: ["Category A", "Category B", "Category C", "Category D"],
28
+ series: [
29
+ {
30
+ label: "Series 1",
31
+ color: "#6366f1",
32
+ data: [40, 65, 30, 80]
33
+ }
34
+ ],
35
+ showLegend: true,
36
+ showGrid: true,
37
+ width: 560,
38
+ height: 320,
39
+ sourceNote: null
40
+ };
41
+ var FALLBACK_CHART_COLORS = [
42
+ "#6366f1",
43
+ "#f97316",
44
+ "#10b981",
45
+ "#ec4899",
46
+ "#0ea5e9",
47
+ "#eab308",
48
+ "#a855f7",
49
+ "#ef4444",
50
+ "#14b8a6",
51
+ "#84cc16"
52
+ ];
53
+ function decodeChartSpec(raw) {
54
+ if (!raw) return null;
55
+ try {
56
+ const decoded = JSON.parse(
57
+ decodeURIComponent(atob(raw))
58
+ );
59
+ if (!decoded || typeof decoded !== "object") return null;
60
+ const categories = Array.isArray(decoded.categories) ? decoded.categories.map(String) : DEFAULT_CHART.categories;
61
+ const series = Array.isArray(decoded.series) ? decoded.series.filter(Boolean).map((s, index) => ({
62
+ label: typeof s.label === "string" ? s.label : `Series ${index + 1}`,
63
+ color: typeof s.color === "string" ? s.color : FALLBACK_CHART_COLORS[index % FALLBACK_CHART_COLORS.length],
64
+ data: Array.isArray(s.data) ? s.data.map((v) => {
65
+ const n = Number(v);
66
+ return Number.isFinite(n) ? n : 0;
67
+ }) : []
68
+ })) : DEFAULT_CHART.series;
69
+ return {
70
+ ...DEFAULT_CHART,
71
+ ...decoded,
72
+ categories,
73
+ series: series.length ? series : DEFAULT_CHART.series,
74
+ chartType: ["bar", "line", "pie", "donut", "area"].includes(decoded.chartType) ? decoded.chartType : DEFAULT_CHART.chartType,
75
+ title: typeof decoded.title === "string" ? decoded.title : DEFAULT_CHART.title,
76
+ showLegend: typeof decoded.showLegend === "boolean" ? decoded.showLegend : DEFAULT_CHART.showLegend,
77
+ showGrid: typeof decoded.showGrid === "boolean" ? decoded.showGrid : DEFAULT_CHART.showGrid,
78
+ width: typeof decoded.width === "number" && decoded.width > 0 ? decoded.width : DEFAULT_CHART.width,
79
+ height: typeof decoded.height === "number" && decoded.height > 0 ? decoded.height : DEFAULT_CHART.height,
80
+ sourceNote: typeof decoded.sourceNote === "string" ? decoded.sourceNote : null
81
+ };
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function recoverChartFromElement(element) {
87
+ const spec = decodeChartSpec(element.getAttribute("data-chart-spec"));
88
+ if (spec) return spec;
89
+ const title = element.getAttribute("data-chart-title") || DEFAULT_CHART.title;
90
+ const chartType = element.getAttribute("data-chart-type");
91
+ return {
92
+ ...DEFAULT_CHART,
93
+ title,
94
+ chartType: ["bar", "line", "pie", "donut", "area"].includes(chartType) ? chartType : "bar"
95
+ };
96
+ }
97
+ function prepareDocumentHTML(html) {
98
+ if (typeof window === "undefined") return html;
99
+ const parser = new DOMParser();
100
+ const doc = parser.parseFromString(html, "text/html");
101
+ doc.querySelectorAll("script, noscript").forEach((node) => node.remove());
102
+ doc.querySelectorAll("*").forEach((element) => {
103
+ [...element.attributes].forEach((attr) => {
104
+ if (attr.name.toLowerCase().startsWith("on")) {
105
+ element.removeAttribute(attr.name);
106
+ }
107
+ });
108
+ const href = element.getAttribute("href");
109
+ const src = element.getAttribute("src");
110
+ if (href && /^\s*javascript:/i.test(href)) element.removeAttribute("href");
111
+ if (src && /^\s*javascript:/i.test(src)) element.removeAttribute("src");
112
+ });
113
+ return doc.body.innerHTML;
114
+ }
115
+ function NativeNexusChart({ chart }) {
116
+ const [hoveredValue, setHoveredValue] = useState(null);
117
+ const categories = chart.categories;
118
+ const series = chart.series;
119
+ const allNumbers = series.flatMap((s) => s.data);
120
+ const maxVal = Math.max(...allNumbers, 10);
121
+ const minVal = Math.min(0, ...allNumbers);
122
+ const width = 560;
123
+ const height = Math.max(220, Math.min(chart.height || 300, 450));
124
+ const padLeft = 45;
125
+ const padRight = 20;
126
+ const padTop = 20;
127
+ const padBottom = 35;
128
+ const plotW = width - padLeft - padRight;
129
+ const plotH = height - padTop - padBottom;
130
+ const getY = (val) => {
131
+ const range = maxVal - minVal;
132
+ return padTop + plotH - (val - minVal) / range * plotH;
133
+ };
134
+ const getX = (catIndex) => {
135
+ return padLeft + catIndex / Math.max(categories.length - 1, 1) * plotW;
136
+ };
137
+ const renderGridAndAxes = () => /* @__PURE__ */ jsxs(Fragment, { children: [
138
+ chart.showGrid && /* @__PURE__ */ jsx(Fragment, { children: [0, 0.25, 0.5, 0.75, 1].map((pct, i) => {
139
+ const y = padTop + plotH * (1 - pct);
140
+ const val = Math.round(minVal + (maxVal - minVal) * pct);
141
+ return /* @__PURE__ */ jsxs("g", { children: [
142
+ /* @__PURE__ */ jsx(
143
+ "line",
144
+ {
145
+ x1: padLeft,
146
+ y1: y,
147
+ x2: width - padRight,
148
+ y2: y,
149
+ stroke: "currentColor",
150
+ strokeOpacity: 0.08,
151
+ strokeDasharray: "3 3"
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx(
155
+ "text",
156
+ {
157
+ x: padLeft - 8,
158
+ y: y + 3,
159
+ textAnchor: "end",
160
+ fontSize: 10,
161
+ fill: "currentColor",
162
+ opacity: 0.5,
163
+ className: "font-mono",
164
+ children: val
165
+ }
166
+ )
167
+ ] }, `grid-${i}`);
168
+ }) }),
169
+ categories.map((cat, i) => {
170
+ const x = categories.length === 1 ? padLeft + plotW / 2 : getX(i);
171
+ return /* @__PURE__ */ jsx(
172
+ "text",
173
+ {
174
+ x,
175
+ y: height - 10,
176
+ textAnchor: "middle",
177
+ fontSize: 11,
178
+ fill: "currentColor",
179
+ opacity: 0.6,
180
+ className: "font-mono",
181
+ children: cat
182
+ },
183
+ `cat-${i}`
184
+ );
185
+ })
186
+ ] });
187
+ const renderBars = () => {
188
+ const groupW = plotW / categories.length;
189
+ const barWidth = Math.max(6, Math.min(28, groupW * 0.7 / series.length));
190
+ return categories.map((cat, catIdx) => {
191
+ const groupCenterX = padLeft + catIdx * groupW + groupW / 2;
192
+ const startX = groupCenterX - series.length * barWidth / 2;
193
+ return series.map((s, sIdx) => {
194
+ const val = s.data[catIdx] || 0;
195
+ const barH = (val - minVal) / (maxVal - minVal) * plotH;
196
+ const x = startX + sIdx * barWidth;
197
+ const y = padTop + plotH - barH;
198
+ return /* @__PURE__ */ jsx(
199
+ "rect",
200
+ {
201
+ x,
202
+ y,
203
+ width: barWidth - 2,
204
+ height: Math.max(barH, 2),
205
+ rx: 3,
206
+ fill: s.color,
207
+ opacity: 0.9,
208
+ className: "transition-all duration-300 hover:opacity-100 cursor-pointer",
209
+ onMouseEnter: () => setHoveredValue(`${cat} \u2022 ${s.label}: ${val.toLocaleString()}`),
210
+ onMouseLeave: () => setHoveredValue(null)
211
+ },
212
+ `bar-${catIdx}-${sIdx}`
213
+ );
214
+ });
215
+ });
216
+ };
217
+ const renderLinesOrArea = (isArea) => {
218
+ return series.map((s, sIdx) => {
219
+ const points = s.data.map((val, catIdx) => `${getX(catIdx)},${getY(val)}`).join(" ");
220
+ const areaPoints = `${getX(0)},${padTop + plotH} ${points} ${getX(s.data.length - 1)},${padTop + plotH}`;
221
+ return /* @__PURE__ */ jsxs("g", { children: [
222
+ isArea && /* @__PURE__ */ jsx("polygon", { points: areaPoints, fill: s.color, fillOpacity: 0.15 }),
223
+ /* @__PURE__ */ jsx(
224
+ "polyline",
225
+ {
226
+ points,
227
+ fill: "none",
228
+ stroke: s.color,
229
+ strokeWidth: 2.5,
230
+ strokeLinecap: "round",
231
+ strokeLinejoin: "round"
232
+ }
233
+ ),
234
+ s.data.map((val, catIdx) => /* @__PURE__ */ jsx(
235
+ "circle",
236
+ {
237
+ cx: getX(catIdx),
238
+ cy: getY(val),
239
+ r: 4,
240
+ fill: s.color,
241
+ className: "cursor-pointer transition-transform hover:scale-150",
242
+ onMouseEnter: () => setHoveredValue(
243
+ `${categories[catIdx]} \u2022 ${s.label}: ${val.toLocaleString()}`
244
+ ),
245
+ onMouseLeave: () => setHoveredValue(null)
246
+ },
247
+ `dot-${catIdx}`
248
+ ))
249
+ ] }, `series-${sIdx}`);
250
+ });
251
+ };
252
+ const renderPieOrDonut = () => {
253
+ const s = series[0] || { data: [] };
254
+ const total = s.data.reduce((a, b) => a + (Number(b) || 0), 0) || 1;
255
+ let accumulated = 0;
256
+ const cx = width / 2;
257
+ const cy = height / 2;
258
+ const r = Math.min(cx, cy) - 30;
259
+ const isDonut = chart.chartType === "donut";
260
+ return /* @__PURE__ */ jsxs("g", { transform: `translate(${cx}, ${cy})`, children: [
261
+ categories.map((cat, idx) => {
262
+ const val = s.data[idx] || 0;
263
+ const sliceAngle = val / total * 2 * Math.PI;
264
+ const startAngle = accumulated;
265
+ const endAngle = accumulated + sliceAngle;
266
+ accumulated += sliceAngle;
267
+ const x1 = Math.cos(startAngle) * r;
268
+ const y1 = Math.sin(startAngle) * r;
269
+ const x2 = Math.cos(endAngle) * r;
270
+ const y2 = Math.sin(endAngle) * r;
271
+ const largeArc = sliceAngle > Math.PI ? 1 : 0;
272
+ const color = FALLBACK_CHART_COLORS[idx % FALLBACK_CHART_COLORS.length];
273
+ return /* @__PURE__ */ jsx(
274
+ "path",
275
+ {
276
+ d: `M 0 0 L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`,
277
+ fill: color,
278
+ className: "cursor-pointer transition-transform duration-200 hover:scale-105",
279
+ onMouseEnter: () => setHoveredValue(
280
+ `${cat}: ${val.toLocaleString()} (${Math.round(val / total * 100)}%)`
281
+ ),
282
+ onMouseLeave: () => setHoveredValue(null)
283
+ },
284
+ `pie-${idx}`
285
+ );
286
+ }),
287
+ isDonut && /* @__PURE__ */ jsx(
288
+ "circle",
289
+ {
290
+ cx: 0,
291
+ cy: 0,
292
+ r: r * 0.55,
293
+ className: "fill-white dark:fill-zinc-900"
294
+ }
295
+ )
296
+ ] });
297
+ };
298
+ return /* @__PURE__ */ jsxs("figure", { className: "nexus-chart-rendered my-6 w-full rounded-2xl border border-zinc-200 bg-white/70 p-4 backdrop-blur-md dark:border-zinc-800 dark:bg-zinc-900/60", children: [
299
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
300
+ chart.title && /* @__PURE__ */ jsx("figcaption", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: chart.title }),
301
+ hoveredValue && /* @__PURE__ */ jsx("span", { className: "font-mono text-xs font-medium text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/50 px-2 py-0.5 rounded-md", children: hoveredValue })
302
+ ] }),
303
+ /* @__PURE__ */ jsx("div", { className: "w-full overflow-hidden text-zinc-700 dark:text-zinc-300", children: /* @__PURE__ */ jsx(
304
+ "svg",
305
+ {
306
+ viewBox: `0 0 ${width} ${height}`,
307
+ className: "w-full h-auto max-h-[360px] overflow-visible",
308
+ children: chart.chartType === "pie" || chart.chartType === "donut" ? renderPieOrDonut() : /* @__PURE__ */ jsxs(Fragment, { children: [
309
+ renderGridAndAxes(),
310
+ chart.chartType === "bar" && renderBars(),
311
+ chart.chartType === "line" && renderLinesOrArea(false),
312
+ chart.chartType === "area" && renderLinesOrArea(true)
313
+ ] })
314
+ }
315
+ ) }),
316
+ chart.showLegend && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-center gap-3 mt-3 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: (chart.chartType === "pie" || chart.chartType === "donut" ? categories : series).map((item, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
317
+ /* @__PURE__ */ jsx(
318
+ "span",
319
+ {
320
+ className: "h-2.5 w-2.5 rounded-full",
321
+ style: {
322
+ backgroundColor: item.color || FALLBACK_CHART_COLORS[idx % FALLBACK_CHART_COLORS.length]
323
+ }
324
+ }
325
+ ),
326
+ /* @__PURE__ */ jsx("span", { children: item.label || item })
327
+ ] }, `legend-${idx}`)) }),
328
+ chart.sourceNote && /* @__PURE__ */ jsx("figcaption", { className: "mt-2 text-center text-xs text-zinc-400", children: chart.sourceNote })
329
+ ] });
330
+ }
331
+ function NexusRenderer({
332
+ content,
333
+ className,
334
+ onCommentClick,
335
+ onImageClick,
336
+ hydrateCharts = true,
337
+ enableImageInteraction = true,
338
+ documentClassName
339
+ }) {
340
+ const containerRef = useRef(null);
341
+ const mounted = typeof window !== "undefined";
342
+ const preparedHTML = useMemo(
343
+ () => prepareDocumentHTML(content || ""),
344
+ [content]
345
+ );
346
+ const isEmpty = !content || content.trim() === "" || content.trim() === "<p></p>" || content.trim() === "<p><br></p>";
347
+ const handleDocumentClick = useCallback(
348
+ (event) => {
349
+ const target = event.target;
350
+ if (!(target instanceof Element)) return;
351
+ const commentElement = target.closest("[data-comment-id]");
352
+ if (commentElement && onCommentClick) {
353
+ const commentId = commentElement.getAttribute("data-comment-id");
354
+ if (commentId) {
355
+ event.preventDefault();
356
+ onCommentClick(commentId);
357
+ return;
358
+ }
359
+ }
360
+ if (enableImageInteraction && onImageClick && target instanceof HTMLImageElement) {
361
+ const src = target.currentSrc || target.src;
362
+ if (src) {
363
+ event.preventDefault();
364
+ onImageClick(src, target.alt || void 0);
365
+ }
366
+ }
367
+ },
368
+ [enableImageInteraction, onCommentClick, onImageClick]
369
+ );
370
+ const [chartTargets, setChartTargets] = useState([]);
371
+ const updateChartTargets = useCallback(() => {
372
+ if (!hydrateCharts) {
373
+ setChartTargets([]);
374
+ return;
375
+ }
376
+ const container = containerRef.current;
377
+ if (!container) return;
378
+ setChartTargets(
379
+ Array.from(
380
+ container.querySelectorAll("[data-nexus-chart]")
381
+ ).map((element) => ({
382
+ element,
383
+ chart: recoverChartFromElement(element)
384
+ }))
385
+ );
386
+ }, [hydrateCharts]);
387
+ useEffect(() => {
388
+ updateChartTargets();
389
+ }, [preparedHTML, updateChartTargets]);
390
+ if (isEmpty) {
391
+ return /* @__PURE__ */ jsx("p", { className: cn("text-sm italic text-zinc-500", className), children: "No descriptive data provisioned for this node." });
392
+ }
393
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
394
+ /* @__PURE__ */ jsx(
395
+ "article",
396
+ {
397
+ ref: containerRef,
398
+ onClick: handleDocumentClick,
399
+ className: cn(
400
+ "nexus-renderer max-w-none w-full min-w-0 text-[15px] leading-7 md:text-base text-zinc-700 dark:text-zinc-300",
401
+ documentClassName,
402
+ className
403
+ ),
404
+ dangerouslySetInnerHTML: { __html: preparedHTML }
405
+ }
406
+ ),
407
+ mounted && hydrateCharts && chartTargets.map(
408
+ ({ element, chart }, index) => createPortal(
409
+ /* @__PURE__ */ jsx(NativeNexusChart, { chart }, `nexus-chart-${index}`),
410
+ element
411
+ )
412
+ )
413
+ ] });
414
+ }
5
415
 
6
416
  // src/components/NexusProvider.tsx
7
- import React2, {
417
+ import React3, {
8
418
  createContext as createContext2,
9
- useEffect as useEffect2,
10
- useRef,
11
- useMemo,
12
- useState as useState2,
13
- useCallback as useCallback2,
419
+ useEffect as useEffect3,
420
+ useRef as useRef2,
421
+ useMemo as useMemo2,
422
+ useState as useState3,
423
+ useCallback as useCallback3,
14
424
  Suspense
15
425
  } from "react";
16
426
  import { usePathname, useSearchParams } from "next/navigation";
@@ -282,7 +692,7 @@ var LocalCacheProxy = class {
282
692
  async getInstance() {
283
693
  if (this.instance) return this.instance;
284
694
  if (typeof window === "undefined") {
285
- const mod = await import("./local-cache-server-667T23JJ.js");
695
+ const mod = await import("./local-cache-server-SD3K7MZG.js");
286
696
  this.instance = new mod.LocalCache(this.cachePath);
287
697
  } else {
288
698
  const mod = await import("./local-cache-client-I6UYVJJV.js");
@@ -314,90 +724,69 @@ var LocalCacheProxy = class {
314
724
  // src/content/strategies.ts
315
725
  var RateLimiter = class {
316
726
  constructor(config) {
317
- this.requests = [];
318
727
  this.config = config;
728
+ this.requests = [];
319
729
  }
320
730
  async checkLimit() {
321
731
  while (true) {
322
732
  const now = Date.now();
323
- const windowStart = now - this.config.timeWindow;
324
- this.requests = this.requests.filter((time) => time > windowStart);
733
+ const cutoff = now - this.config.timeWindow;
734
+ this.requests = this.requests.filter((t) => t > cutoff);
325
735
  if (this.requests.length < this.config.maxRequests) {
326
736
  this.requests.push(now);
327
737
  return;
328
738
  }
329
- const oldestRequest = this.requests[0];
330
- const waitTime = oldestRequest + this.config.timeWindow - now;
331
- if (waitTime > 0) {
332
- await new Promise((resolve) => setTimeout(resolve, waitTime));
333
- } else {
334
- }
739
+ await new Promise((r) => setTimeout(r, Math.max(1, this.requests[0] + this.config.timeWindow - now)));
335
740
  }
336
741
  }
337
742
  getStats() {
338
743
  const now = Date.now();
339
- const windowStart = now - this.config.timeWindow;
340
- const currentRequests = this.requests.filter(
341
- (time) => time > windowStart
342
- ).length;
343
- return { currentRequests, limit: this.config.maxRequests };
744
+ return { currentRequests: this.requests.filter((t) => t > now - this.config.timeWindow).length, limit: this.config.maxRequests };
344
745
  }
345
746
  };
346
747
  var ExponentialBackoff = class {
347
748
  constructor(config) {
749
+ this.config = config;
348
750
  this.config = { jitter: true, ...config };
349
751
  }
350
752
  async execute(fn, onRetry) {
351
- let lastError = new Error("Unknown error");
352
- for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
753
+ let last;
754
+ for (let a = 0; a <= this.config.maxRetries; a++) {
353
755
  try {
354
756
  return await fn();
355
- } catch (error) {
356
- lastError = error;
357
- if (this.isClientError(error) && !this.isRateLimitError(error)) {
358
- throw error;
359
- }
360
- if (attempt === this.config.maxRetries) break;
361
- const delay = this.calculateDelay(attempt);
362
- if (onRetry) onRetry(attempt + 1, delay, error);
363
- await new Promise((resolve) => setTimeout(resolve, delay));
757
+ } catch (e) {
758
+ last = e;
759
+ if (e?.code === "ABORTED" || e?.code === "VALIDATION_ERROR" || e?.status >= 400 && e?.status < 500 && e?.status !== 429) throw e;
760
+ if (a === this.config.maxRetries) break;
761
+ let delay = Math.min(this.config.maxDelay, this.config.baseDelay * 2 ** a);
762
+ if (this.config.jitter) delay *= 0.5 + Math.random();
763
+ onRetry?.(a + 1, delay, e);
764
+ await new Promise((r) => setTimeout(r, delay));
364
765
  }
365
766
  }
366
- throw lastError;
367
- }
368
- calculateDelay(attempt) {
369
- let delay = this.config.baseDelay * Math.pow(2, attempt);
370
- delay = Math.min(delay, this.config.maxDelay);
371
- if (this.config.jitter) {
372
- delay = delay * (0.5 + Math.random());
373
- }
374
- return delay;
375
- }
376
- isClientError(error) {
377
- return error?.status >= 400 && error?.status < 500;
378
- }
379
- isRateLimitError(error) {
380
- return error?.status === 429;
767
+ throw last;
381
768
  }
382
769
  };
770
+ var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
771
+ CircuitState2[CircuitState2["CLOSED"] = 0] = "CLOSED";
772
+ CircuitState2[CircuitState2["OPEN"] = 1] = "OPEN";
773
+ CircuitState2[CircuitState2["HALF_OPEN"] = 2] = "HALF_OPEN";
774
+ return CircuitState2;
775
+ })(CircuitState || {});
383
776
  var CircuitBreaker = class {
384
- constructor() {
777
+ constructor(threshold = 5, reset = 3e4) {
778
+ this.threshold = threshold;
779
+ this.reset = reset;
385
780
  this.state = 0 /* CLOSED */;
386
781
  this.failures = 0;
387
- this.lastFailureTime = 0;
388
- this.failureThreshold = 5;
389
- this.resetTimeout = 3e4;
782
+ this.lastFailure = 0;
390
783
  }
391
- // 30 seconds
392
784
  isOpen() {
393
- if (this.state === 1 /* OPEN */) {
394
- if (Date.now() - this.lastFailureTime > this.resetTimeout) {
395
- this.state = 2 /* HALF_OPEN */;
396
- return false;
397
- }
398
- return true;
785
+ if (this.state === 1 /* OPEN */ && Date.now() - this.lastFailure >= this.reset) {
786
+ this.state = 2 /* HALF_OPEN */;
787
+ return false;
399
788
  }
400
- return false;
789
+ return this.state === 1 /* OPEN */;
401
790
  }
402
791
  recordSuccess() {
403
792
  this.failures = 0;
@@ -405,110 +794,33 @@ var CircuitBreaker = class {
405
794
  }
406
795
  recordFailure() {
407
796
  this.failures++;
408
- this.lastFailureTime = Date.now();
409
- if (this.failures >= this.failureThreshold) {
410
- this.state = 1 /* OPEN */;
411
- if (process.env.NODE_ENV === "development") {
412
- console.warn(
413
- "[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests."
414
- );
415
- }
416
- }
797
+ this.lastFailure = Date.now();
798
+ if (this.failures >= this.threshold) this.state = 1 /* OPEN */;
799
+ }
800
+ getState() {
801
+ return CircuitState[this.state];
417
802
  }
418
803
  };
419
804
  var RequestBatcher = class {
420
- constructor(batchWindow = 10, maxBatchSize = 20) {
421
- this.batchWindow = batchWindow;
805
+ constructor(windowMs = 10, maxBatchSize = 50) {
806
+ this.windowMs = windowMs;
422
807
  this.maxBatchSize = maxBatchSize;
423
- this.batch = [];
424
- this.processing = false;
425
- }
426
- async schedule(key, request) {
427
- return new Promise((resolve, reject) => {
428
- this.batch.push({ key, resolve, reject });
429
- if (this.batch.length >= this.maxBatchSize) {
430
- this.processBatch(request);
431
- } else if (!this.batchTimeout) {
432
- this.batchTimeout = setTimeout(
433
- () => this.processBatch(request),
434
- this.batchWindow
435
- );
436
- }
808
+ this.pending = /* @__PURE__ */ new Map();
809
+ }
810
+ schedule(key, request) {
811
+ const existing = this.pending.get(key);
812
+ if (existing) return existing;
813
+ const promise = new Promise((resolve, reject) => setTimeout(() => request().then(resolve, reject), this.windowMs));
814
+ this.pending.set(key, promise);
815
+ promise.finally(() => this.pending.delete(key)).catch(() => {
437
816
  });
817
+ return promise;
438
818
  }
439
- async processBatch(request) {
440
- if (this.processing || this.batch.length === 0) return;
441
- this.processing = true;
442
- if (this.batchTimeout) {
443
- clearTimeout(this.batchTimeout);
444
- this.batchTimeout = void 0;
445
- }
446
- const currentBatch = [...this.batch];
447
- this.batch = [];
448
- try {
449
- const result = await request();
450
- currentBatch.forEach((item) => item.resolve(result));
451
- } catch (error) {
452
- currentBatch.forEach((item) => item.reject(error));
453
- } finally {
454
- this.processing = false;
455
- if (this.batch.length > 0) {
456
- setTimeout(() => this.processBatch(request), 0);
457
- }
458
- }
819
+ clear() {
820
+ this.pending.clear();
459
821
  }
460
822
  };
461
823
 
462
- // src/content/utils.ts
463
- function validateSlug(slug) {
464
- if (!slug || typeof slug !== "string") {
465
- throw new Error("Slug must be a non-empty string");
466
- }
467
- if (!/^[a-z0-9-_]+$/.test(slug)) {
468
- throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
469
- }
470
- }
471
- function normalizeQuery(query) {
472
- const normalized = { ...query };
473
- normalized.page = Math.max(1, normalized.page || 1);
474
- normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
475
- normalized.order = normalized.order || "desc";
476
- if (normalized.page < 1) {
477
- throw new Error("Page must be greater than 0");
478
- }
479
- if (normalized.limit < 1 || normalized.limit > 100) {
480
- throw new Error("Limit must be between 1 and 100");
481
- }
482
- return normalized;
483
- }
484
- function buildQueryString(query) {
485
- const params = new URLSearchParams();
486
- if (query.page) params.append("page", query.page.toString());
487
- if (query.limit) params.append("limit", query.limit.toString());
488
- if (query.sort) params.append("sort", query.sort);
489
- if (query.order) params.append("order", query.order);
490
- if (query.search) params.append("search", query.search);
491
- if (query.include?.length) {
492
- params.append("include", query.include.join(","));
493
- }
494
- if (query.fields?.length) {
495
- params.append("fields", query.fields.join(","));
496
- }
497
- if (query.filter) {
498
- params.append("filter", JSON.stringify(query.filter));
499
- }
500
- return params.toString();
501
- }
502
- function measurePerformance(name, fn) {
503
- const start = performance.now();
504
- const result = fn();
505
- const end = performance.now();
506
- if (process.env.NODE_ENV === "development") {
507
- console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
508
- }
509
- return { result, duration: end - start };
510
- }
511
-
512
824
  // src/content/index.ts
513
825
  var ContentEngine = class {
514
826
  constructor(config) {
@@ -541,6 +853,12 @@ var ContentEngine = class {
541
853
  window.addEventListener("beforeunload", this.cleanup.bind(this));
542
854
  }
543
855
  }
856
+ /** Update runtime configuration without exposing internal mutation. */
857
+ updateConfig(config) {
858
+ this.config = config;
859
+ this.defaultRevalidate = config.revalidateTime ?? false;
860
+ this.cacheStrategy = config.cacheStrategy || "memory";
861
+ }
544
862
  /**
545
863
  * Fetch a Single Page with full strategy pipeline
546
864
  */
@@ -699,7 +1017,7 @@ var ContentEngine = class {
699
1017
  }
700
1018
  }
701
1019
  /**
702
- * Fetch Global Settings with nested includes support
1020
+ * Fetch Global Settings
703
1021
  */
704
1022
  async getGlobals(options = {}) {
705
1023
  const {
@@ -751,6 +1069,9 @@ var ContentEngine = class {
751
1069
  revalidate
752
1070
  });
753
1071
  if (!res.ok) {
1072
+ if (res.status === 408) {
1073
+ throw new Error("Request timeout");
1074
+ }
754
1075
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
755
1076
  }
756
1077
  const json = await res.json();
@@ -799,12 +1120,12 @@ var ContentEngine = class {
799
1120
  method: "GET",
800
1121
  headers: this.getHeaders(),
801
1122
  tags: fetchTags,
802
- // ?? not || — see the constructor comment on defaultRevalidate for
803
- // why: an explicit `revalidate: 0` on this call must not be
804
- // discarded in favor of the engine's default.
805
1123
  revalidate: options.revalidate ?? this.defaultRevalidate
806
1124
  });
807
1125
  if (!res.ok) {
1126
+ if (res.status === 408) {
1127
+ throw new Error("Request timeout");
1128
+ }
808
1129
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
809
1130
  }
810
1131
  const json = await res.json();
@@ -836,12 +1157,15 @@ var ContentEngine = class {
836
1157
  headers: this.getHeaders()
837
1158
  });
838
1159
  if (!res.ok) {
1160
+ if (res.status === 408) {
1161
+ throw new Error("Request timeout");
1162
+ }
839
1163
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
840
1164
  }
841
1165
  return await res.json();
842
1166
  }
843
1167
  /**
844
- * Prefetch content for better performance
1168
+ * Prefetch content
845
1169
  */
846
1170
  async prefetch(urls) {
847
1171
  if (typeof window !== "undefined" && "requestIdleCallback" in window) {
@@ -854,41 +1178,19 @@ var ContentEngine = class {
854
1178
  }
855
1179
  /**
856
1180
  * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
857
- *
858
- * ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
859
- * This connects from the BROWSER TAB it's called in, and on message it
860
- * only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
861
- * invalidateCache below) in THAT browser tab's JS heap. In a typical
862
- * Next.js deployment — and Cloudflare specifically, which is stateless
863
- * per-request at the edge — that is a different process/isolate than the
864
- * one that will render the NEXT server request for this content. So:
865
- * - ✅ Useful for: a client component that reads from `nexus.content`
866
- * directly in the browser and re-renders in place without a page
867
- * navigation (e.g. a live-updating dashboard widget).
868
- * - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
869
- * already-loaded page updates" via a server-rendered page. That
870
- * requires the backend's /api/revalidate webhook (see
871
- * content.service.ts `pingNextRevalidateWebhook`) to have actually
872
- * cleared the *server's* Data Cache, so the NEXT navigation or
873
- * server request picks up fresh data. This SSE channel does not
874
- * replace that — it's a complementary, browser-local optimization.
875
- * If your symptom was "stale content after editing," fix the webhook
876
- * wiring first; treat this method as an enhancement layered on top.
877
1181
  */
878
1182
  subscribeToUpdates(callback) {
879
1183
  if (this.isServer || typeof EventSource === "undefined") {
880
1184
  if (this.config.debug) {
881
1185
  console.warn(
882
- "[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."
1186
+ "[NexusHub] subscribeToUpdates() called in a non-browser environment."
883
1187
  );
884
1188
  }
885
1189
  return () => {
886
1190
  };
887
1191
  }
888
1192
  if (!this.config.apiKey) {
889
- console.warn(
890
- "[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
891
- );
1193
+ console.warn("[NexusHub] subscribeToUpdates(): no apiKey configured.");
892
1194
  }
893
1195
  let eventSource = null;
894
1196
  let retryCount = 0;
@@ -1052,6 +1354,9 @@ var ContentEngine = class {
1052
1354
  `Page '${slug}' not found. Check your Dashboard or Seed data.`
1053
1355
  );
1054
1356
  }
1357
+ if (res.status === 408) {
1358
+ throw new Error("Request timeout");
1359
+ }
1055
1360
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1056
1361
  }
1057
1362
  const json = await res.json();
@@ -1086,6 +1391,9 @@ var ContentEngine = class {
1086
1391
  revalidate
1087
1392
  });
1088
1393
  if (!res.ok) {
1394
+ if (res.status === 408) {
1395
+ throw new Error("Request timeout");
1396
+ }
1089
1397
  throw new Error(`API Error ${res.status}: ${res.statusText}`);
1090
1398
  }
1091
1399
  const json = await res.json();
@@ -1172,7 +1480,6 @@ var ContentEngine = class {
1172
1480
  const response = await fetch(url, {
1173
1481
  ...fetchOptions,
1174
1482
  ...nextConfig,
1175
- // Inject Next.js tags
1176
1483
  signal: controller.signal
1177
1484
  });
1178
1485
  clearTimeout(id);
@@ -1185,7 +1492,8 @@ var ContentEngine = class {
1185
1492
  getHeaders() {
1186
1493
  const headers = {
1187
1494
  "Content-Type": "application/json",
1188
- "X-Nexus-Client": "client-sdk/1.0.0",
1495
+ "X-Nexus-Client": `client-sdk/${this.config.sdkVersion ?? "1.1.0"}`,
1496
+ "X-Nexus-Request-ID": typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`,
1189
1497
  "X-Nexus-Project": this.config.projectId
1190
1498
  };
1191
1499
  if (this.config.apiKey) {
@@ -1196,13 +1504,22 @@ var ContentEngine = class {
1196
1504
  isCacheValid(metadata) {
1197
1505
  return Date.now() < metadata.expiresAt;
1198
1506
  }
1507
+ /**
1508
+ * 🛡️ FIX: Normalizes timeout errors (AbortError, 408, or Timeout messages)
1509
+ * into clean, standardized "Request timeout" errors.
1510
+ */
1199
1511
  normalizeError(error, context) {
1200
1512
  if (error instanceof Error) {
1201
- if (error.name === "AbortError") {
1513
+ const msg = error.message.toLowerCase();
1514
+ if (error.name === "AbortError" || error.message.includes("408") || msg.includes("timeout") || msg.includes("aborted")) {
1202
1515
  return new Error(`${context}: Request timeout`);
1203
1516
  }
1204
1517
  return error;
1205
1518
  }
1519
+ const str = String(error).toLowerCase();
1520
+ if (str.includes("408") || str.includes("timeout") || str.includes("aborted")) {
1521
+ return new Error(`${context}: Request timeout`);
1522
+ }
1206
1523
  return new Error(`${context}: ${String(error)}`);
1207
1524
  }
1208
1525
  cancelRequests() {
@@ -1221,7 +1538,7 @@ var ContentEngine = class {
1221
1538
 
1222
1539
  // src/analytics/fingerprint.ts
1223
1540
  var cachedEntropy = null;
1224
- var getDeviceEntropy = async () => {
1541
+ var getDeviceEntropy = async (allowFingerprinting = true) => {
1225
1542
  if (cachedEntropy) return cachedEntropy;
1226
1543
  if (typeof window === "undefined") return {};
1227
1544
  const nav = window.navigator;
@@ -1236,7 +1553,7 @@ var getDeviceEntropy = async () => {
1236
1553
  platform: nav.platform,
1237
1554
  language: nav.language,
1238
1555
  touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
1239
- canvas_hash: await generateCanvasHash()
1556
+ canvas_hash: allowFingerprinting ? await generateCanvasHash() : void 0
1240
1557
  };
1241
1558
  return cachedEntropy;
1242
1559
  };
@@ -1249,7 +1566,6 @@ var generateCanvasHash = async () => {
1249
1566
  canvas.height = 50;
1250
1567
  ctx.textBaseline = "top";
1251
1568
  ctx.font = '16px "Arial"';
1252
- ctx.textBaseline = "alphabetic";
1253
1569
  ctx.fillStyle = "#f60";
1254
1570
  ctx.fillRect(125, 1, 62, 20);
1255
1571
  ctx.fillStyle = "#069";
@@ -1268,12 +1584,12 @@ var generateCanvasHash = async () => {
1268
1584
  return "";
1269
1585
  }
1270
1586
  };
1271
- var getVisitorId = async () => {
1587
+ var getVisitorId = async (allowFingerprinting = true) => {
1272
1588
  if (typeof window === "undefined") return "server_visitor";
1273
1589
  const STORAGE_KEY = "nexus_vid";
1274
1590
  let vid = localStorage.getItem(STORAGE_KEY);
1275
1591
  if (!vid) {
1276
- const entropy = await getDeviceEntropy();
1592
+ const entropy = await getDeviceEntropy(allowFingerprinting);
1277
1593
  const random = Math.random().toString(36).substring(2, 15);
1278
1594
  const timestamp = Date.now().toString(36);
1279
1595
  const fingerprint = [
@@ -1281,7 +1597,7 @@ var getVisitorId = async () => {
1281
1597
  entropy.hardware_concurrency,
1282
1598
  entropy.timezone_offset,
1283
1599
  entropy.platform,
1284
- entropy.canvas_hash
1600
+ entropy.canvas_hash || "standard_entropy"
1285
1601
  ].join("|");
1286
1602
  let hash = 0;
1287
1603
  for (let i = 0; i < fingerprint.length; i++) {
@@ -1492,7 +1808,6 @@ var EventStorage = class {
1492
1808
  var eventStorage = new EventStorage();
1493
1809
 
1494
1810
  // src/analytics/tracker.ts
1495
- var SDK_VERSION = "0.1.0";
1496
1811
  var safeGetItem = (key) => {
1497
1812
  try {
1498
1813
  return localStorage.getItem(key);
@@ -1528,6 +1843,8 @@ var Tracker = class {
1528
1843
  this.visitorId = "";
1529
1844
  this.anonymousId = "";
1530
1845
  this.isFlushing = false;
1846
+ this.flushPromise = null;
1847
+ this.batchSize = 25;
1531
1848
  this.config = config;
1532
1849
  this.endpoint = `${this.config.analyticsUrl}/api/collect`;
1533
1850
  this.circuitBreaker = new CircuitBreaker();
@@ -1538,7 +1855,9 @@ var Tracker = class {
1538
1855
  }
1539
1856
  }
1540
1857
  async initSession() {
1541
- this.visitorId = await getVisitorId();
1858
+ this.visitorId = await getVisitorId(
1859
+ this.config.privacy?.fingerprinting ?? false
1860
+ );
1542
1861
  let anonId = safeGetItem("nexus_anon_id");
1543
1862
  if (!anonId) {
1544
1863
  anonId = `anon_${generateUUID().replace(/-/g, "")}`;
@@ -1551,8 +1870,8 @@ var Tracker = class {
1551
1870
  const SESSION_TIMEOUT = 30 * 60 * 1e3;
1552
1871
  const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
1553
1872
  if (isExpired) {
1554
- const uuid = generateUUID().replace(/-/g, "").substring(0, 16);
1555
- sid = `sess_${uuid}_${now}`;
1873
+ const uuid2 = generateUUID().replace(/-/g, "").substring(0, 16);
1874
+ sid = `sess_${uuid2}_${now}`;
1556
1875
  safeSetItem("nexus_sid", sid);
1557
1876
  this.sessionStart = now;
1558
1877
  }
@@ -1562,7 +1881,9 @@ var Tracker = class {
1562
1881
  async send(eventType, data = {}, eventName, ecommerce) {
1563
1882
  if (typeof window === "undefined") return;
1564
1883
  safeSetItem("nexus_last_active", Date.now().toString());
1565
- const entropy = await getDeviceEntropy();
1884
+ const entropy = await getDeviceEntropy(
1885
+ this.config.privacy?.fingerprinting ?? false
1886
+ );
1566
1887
  const perfMetrics = vitalsCollector.getMetricsSnapshot();
1567
1888
  const utmParams = extractUtmParams(window.location.href);
1568
1889
  const payload = {
@@ -1573,6 +1894,7 @@ var Tracker = class {
1573
1894
  messageId: generateUUID(),
1574
1895
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
1575
1896
  version: SDK_VERSION,
1897
+ // Dynamically synced to SDK 1.1.0
1576
1898
  url: window.location.href,
1577
1899
  referrer: document.referrer,
1578
1900
  userAgent: window.navigator.userAgent,
@@ -1590,8 +1912,8 @@ var Tracker = class {
1590
1912
  hardwareConcurrency: entropy.hardware_concurrency,
1591
1913
  deviceMemory: entropy.device_memory,
1592
1914
  pixelRatio: entropy.pixel_ratio,
1593
- canvasFingerprint: entropy.canvas_hash,
1594
- platform: entropy.platform
1915
+ platform: entropy.platform,
1916
+ ...this.config.privacy?.fingerprinting ? { canvasFingerprint: entropy.canvas_hash } : {}
1595
1917
  },
1596
1918
  visitorIdLocal: this.visitorId,
1597
1919
  utm: utmParams
@@ -1611,53 +1933,69 @@ var Tracker = class {
1611
1933
  });
1612
1934
  }
1613
1935
  async flushQueue(useBeacon = false) {
1614
- if (this.isFlushing) return;
1615
- if (this.circuitBreaker.isOpen()) return;
1616
- this.isFlushing = true;
1617
- try {
1618
- const storedEvents = await eventStorage.peek(20);
1619
- if (storedEvents.length === 0) {
1620
- this.isFlushing = false;
1621
- return;
1622
- }
1623
- const payloads = storedEvents.map((e) => e.payload);
1624
- const promises = payloads.map(
1625
- (event) => fetch(this.endpoint, {
1626
- method: "POST",
1627
- headers: {
1628
- "Content-Type": "application/json",
1629
- Authorization: `Bearer ${this.config.apiKey}`
1630
- },
1631
- body: JSON.stringify(event),
1632
- keepalive: useBeacon
1633
- })
1634
- );
1635
- const results = await Promise.allSettled(promises);
1636
- const successIds = [];
1637
- let failureCount = 0;
1638
- results.forEach((res, index) => {
1639
- if (res.status === "fulfilled" && res.value.ok) {
1640
- successIds.push(storedEvents[index].id);
1936
+ if (this.flushPromise) return this.flushPromise;
1937
+ this.flushPromise = (async () => {
1938
+ if (this.circuitBreaker.isOpen()) return;
1939
+ try {
1940
+ const storedEvents = await eventStorage.peek(this.batchSize);
1941
+ if (!storedEvents.length) return;
1942
+ const payloads = storedEvents.map((e) => e.payload);
1943
+ const headers = {
1944
+ "Content-Type": "application/json",
1945
+ Authorization: this.config.apiKey ? `Bearer ${this.config.apiKey}` : "",
1946
+ "X-Nexus-Client": `analytics/${SDK_VERSION}`,
1947
+ "X-Nexus-Project": this.config.projectId
1948
+ };
1949
+ let response;
1950
+ try {
1951
+ response = await fetch(
1952
+ `${this.config.analyticsUrl}/api/collect/batch`,
1953
+ {
1954
+ method: "POST",
1955
+ headers,
1956
+ body: JSON.stringify({ events: payloads }),
1957
+ keepalive: useBeacon
1958
+ }
1959
+ );
1960
+ } catch {
1961
+ response = void 0;
1962
+ }
1963
+ if (!response || response.status === 404 || response.status === 405) {
1964
+ const results = await Promise.allSettled(
1965
+ payloads.map(
1966
+ (event) => fetch(this.endpoint, {
1967
+ method: "POST",
1968
+ headers,
1969
+ body: JSON.stringify(event),
1970
+ keepalive: useBeacon
1971
+ })
1972
+ )
1973
+ );
1974
+ const successIds = results.flatMap(
1975
+ (r, i) => r.status === "fulfilled" && r.value.ok ? [storedEvents[i].id] : []
1976
+ );
1977
+ if (successIds.length) await eventStorage.remove(successIds);
1978
+ if (successIds.length === storedEvents.length) {
1979
+ this.circuitBreaker.recordSuccess();
1980
+ } else {
1981
+ this.circuitBreaker.recordFailure();
1982
+ }
1983
+ } else if (response.ok) {
1984
+ await eventStorage.remove(storedEvents.map((e) => e.id));
1985
+ this.circuitBreaker.recordSuccess();
1641
1986
  } else {
1642
- failureCount++;
1987
+ this.circuitBreaker.recordFailure();
1643
1988
  }
1644
- });
1645
- if (successIds.length > 0) {
1646
- await eventStorage.remove(successIds);
1647
- this.circuitBreaker.recordSuccess();
1648
- }
1649
- if (failureCount > 0) {
1989
+ } catch (err) {
1650
1990
  this.circuitBreaker.recordFailure();
1991
+ if (this.config.debug) {
1992
+ console.warn("[GN-Apex] Analytics flush deferred:", err);
1993
+ }
1994
+ } finally {
1995
+ this.flushPromise = null;
1651
1996
  }
1652
- } catch (err) {
1653
- console.error("[NexusHub] Network Error:", err);
1654
- this.circuitBreaker.recordFailure();
1655
- } finally {
1656
- this.isFlushing = false;
1657
- if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
1658
- setTimeout(() => this.flushQueue(), 100);
1659
- }
1660
- }
1997
+ })();
1998
+ return this.flushPromise;
1661
1999
  }
1662
2000
  getSession() {
1663
2001
  return this.sessionId;
@@ -1936,70 +2274,35 @@ var AnalyticsEngine = class {
1936
2274
  }
1937
2275
  setupVideoTracking() {
1938
2276
  if (typeof document === "undefined") return;
1939
- const attachVideoListeners = (video) => {
1940
- if (video.__nexus_tracked) return;
1941
- video.__nexus_tracked = true;
1942
- const src = video.src || video.currentSrc || "unknown";
1943
- let milestone50Fired = false;
1944
- video.addEventListener("play", () => {
1945
- this.tracker.send(
1946
- "custom_event",
1947
- { event_name: "video_play", src },
1948
- "video_play"
1949
- );
1950
- });
1951
- video.addEventListener("pause", () => {
1952
- this.tracker.send(
1953
- "custom_event",
1954
- {
1955
- event_name: "video_pause",
1956
- src,
1957
- position_seconds: Math.round(video.currentTime)
1958
- },
1959
- "video_pause"
1960
- );
1961
- });
2277
+ const attach = (video) => {
2278
+ const v = video;
2279
+ if (v.__gnexusTracked) return;
2280
+ v.__gnexusTracked = true;
2281
+ v.__gnexusMilestones = /* @__PURE__ */ new Set();
2282
+ const src = () => video.currentSrc || video.src || "unknown";
2283
+ const emit = (name, data = {}) => this.tracker.send("media", { media_type: "video", event_name: name, src: src(), ...data }, name);
2284
+ video.addEventListener("loadedmetadata", () => emit("video_loaded", { duration_seconds: Number.isFinite(video.duration) ? Math.round(video.duration) : void 0 }));
2285
+ video.addEventListener("play", () => emit("video_play", { position_seconds: Math.round(video.currentTime) }));
2286
+ video.addEventListener("pause", () => emit("video_pause", { position_seconds: Math.round(video.currentTime) }));
2287
+ video.addEventListener("seeking", () => emit("video_seek", { position_seconds: Math.round(video.currentTime) }));
2288
+ video.addEventListener("ended", () => emit("video_complete", { duration_seconds: Math.round(video.duration) }));
2289
+ video.addEventListener("error", () => emit("video_error"));
1962
2290
  video.addEventListener("timeupdate", () => {
1963
- if (!video.duration || video.duration === Infinity) return;
1964
- const pct = video.currentTime / video.duration;
1965
- if (pct >= 0.5 && !milestone50Fired) {
1966
- milestone50Fired = true;
1967
- this.tracker.send(
1968
- "custom_event",
1969
- {
1970
- event_name: "video_50_percent",
1971
- src
1972
- },
1973
- "video_50_percent"
1974
- );
2291
+ if (!Number.isFinite(video.duration) || video.duration <= 0) return;
2292
+ for (const milestone of [25, 50, 75, 90, 100]) {
2293
+ if (video.currentTime / video.duration * 100 >= milestone && !v.__gnexusMilestones.has(milestone)) {
2294
+ v.__gnexusMilestones.add(milestone);
2295
+ emit(`video_${milestone}_percent`, { progress: milestone / 100 });
2296
+ }
1975
2297
  }
1976
2298
  });
1977
- video.addEventListener("ended", () => {
1978
- this.tracker.send(
1979
- "custom_event",
1980
- {
1981
- event_name: "video_complete",
1982
- src,
1983
- duration_seconds: Math.round(video.duration)
1984
- },
1985
- "video_complete"
1986
- );
1987
- });
1988
2299
  };
1989
- document.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
1990
- const observer = new MutationObserver((mutations) => {
1991
- mutations.forEach((m) => {
1992
- m.addedNodes.forEach((node) => {
1993
- if (node instanceof HTMLVideoElement) {
1994
- attachVideoListeners(node);
1995
- }
1996
- if (node instanceof Element) {
1997
- node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
1998
- }
1999
- });
2000
- });
2001
- });
2002
- observer.observe(document.body, { childList: true, subtree: true });
2300
+ document.querySelectorAll("video").forEach((v) => attach(v));
2301
+ const observer = new MutationObserver((ms) => ms.forEach((m) => m.addedNodes.forEach((n) => {
2302
+ if (n instanceof HTMLVideoElement) attach(n);
2303
+ if (n instanceof Element) n.querySelectorAll("video").forEach((v) => attach(v));
2304
+ })));
2305
+ if (document.body) observer.observe(document.body, { childList: true, subtree: true });
2003
2306
  this.cleanupFns.push(() => observer.disconnect());
2004
2307
  }
2005
2308
  setupErrorTracking() {
@@ -2188,57 +2491,303 @@ function getSelector(el, depth = 0) {
2188
2491
  return `${parent}${self}`;
2189
2492
  }
2190
2493
 
2494
+ // src/errors.ts
2495
+ var NexusError = class extends Error {
2496
+ constructor(message, code = "UNKNOWN", status, requestId, details, retryable = false, cause) {
2497
+ super(message);
2498
+ this.code = code;
2499
+ this.status = status;
2500
+ this.requestId = requestId;
2501
+ this.details = details;
2502
+ this.retryable = retryable;
2503
+ this.cause = cause;
2504
+ this.name = "NexusError";
2505
+ Object.setPrototypeOf(this, new.target.prototype);
2506
+ }
2507
+ };
2508
+
2509
+ // src/events.ts
2510
+ var NexusEventBus = class {
2511
+ constructor() {
2512
+ this.listeners = /* @__PURE__ */ new Map();
2513
+ }
2514
+ on(event, listener) {
2515
+ const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
2516
+ set.add(listener);
2517
+ this.listeners.set(event, set);
2518
+ return () => set.delete(listener);
2519
+ }
2520
+ emit(event, payload) {
2521
+ this.listeners.get(event)?.forEach((l) => {
2522
+ try {
2523
+ l(payload);
2524
+ } catch {
2525
+ }
2526
+ });
2527
+ }
2528
+ clear() {
2529
+ this.listeners.clear();
2530
+ }
2531
+ };
2532
+
2533
+ // src/http.ts
2534
+ var uuid = () => typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`;
2535
+ var NexusHttpClient = class {
2536
+ constructor(config, events = new NexusEventBus()) {
2537
+ this.config = config;
2538
+ this.events = events;
2539
+ this.breaker = new CircuitBreaker();
2540
+ this.limiter = new RateLimiter({
2541
+ maxRequests: config.debug ? 200 : 100,
2542
+ timeWindow: 6e4
2543
+ });
2544
+ this.backoff = new ExponentialBackoff({
2545
+ maxRetries: config.retries ?? 3,
2546
+ baseDelay: 150,
2547
+ maxDelay: 8e3,
2548
+ jitter: true
2549
+ });
2550
+ }
2551
+ updateConfig(config) {
2552
+ this.config = config;
2553
+ }
2554
+ async request(input, options = {}) {
2555
+ if (this.breaker.isOpen()) {
2556
+ throw new NexusError(
2557
+ "GN-Apex service circuit is open",
2558
+ "CIRCUIT_OPEN",
2559
+ void 0,
2560
+ void 0,
2561
+ void 0,
2562
+ true
2563
+ );
2564
+ }
2565
+ const requestId = uuid();
2566
+ const url = input;
2567
+ const started = Date.now();
2568
+ const headers = new Headers(options.headers);
2569
+ headers.set("X-Nexus-Request-ID", requestId);
2570
+ headers.set(
2571
+ "X-Nexus-Client",
2572
+ `gnexus-sdk/${this.config.sdkVersion ?? "1.1.0"}`
2573
+ );
2574
+ headers.set("X-Nexus-Project", this.config.projectId);
2575
+ if (this.config.apiKey)
2576
+ headers.set("Authorization", `Bearer ${this.config.apiKey}`);
2577
+ this.events.emit("request:start", {
2578
+ requestId,
2579
+ url,
2580
+ method: options.method ?? "GET"
2581
+ });
2582
+ try {
2583
+ await this.limiter.checkLimit();
2584
+ const response = await this.backoff.execute(async () => {
2585
+ const controller = new AbortController();
2586
+ const timeout = options.timeout ?? this.config.timeout ?? 1e4;
2587
+ const timer = setTimeout(() => controller.abort(), timeout);
2588
+ try {
2589
+ return await fetch(url, {
2590
+ ...options,
2591
+ headers,
2592
+ signal: options.signal ?? controller.signal
2593
+ });
2594
+ } catch (e) {
2595
+ if (e?.name === "AbortError") {
2596
+ throw new NexusError(
2597
+ `Request timed out after ${timeout}ms`,
2598
+ "TIMEOUT",
2599
+ void 0,
2600
+ requestId,
2601
+ void 0,
2602
+ true,
2603
+ e
2604
+ );
2605
+ }
2606
+ throw new NexusError(
2607
+ "Network request failed",
2608
+ "NETWORK_ERROR",
2609
+ void 0,
2610
+ requestId,
2611
+ void 0,
2612
+ true,
2613
+ e
2614
+ );
2615
+ } finally {
2616
+ clearTimeout(timer);
2617
+ }
2618
+ });
2619
+ if (!response.ok) {
2620
+ const retryable = response.status === 408 || response.status === 429 || response.status >= 500;
2621
+ let details;
2622
+ try {
2623
+ details = await response.clone().json();
2624
+ } catch {
2625
+ }
2626
+ const code = response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR";
2627
+ const err = new NexusError(
2628
+ `API request failed (${response.status})`,
2629
+ code,
2630
+ response.status,
2631
+ response.headers.get("x-nexus-request-id") ?? requestId,
2632
+ details,
2633
+ retryable
2634
+ );
2635
+ throw err;
2636
+ }
2637
+ this.breaker.recordSuccess();
2638
+ this.events.emit("request:end", {
2639
+ requestId,
2640
+ url,
2641
+ status: response.status,
2642
+ duration: Date.now() - started
2643
+ });
2644
+ return response;
2645
+ } catch (e) {
2646
+ if (e instanceof NexusError) {
2647
+ if (e.retryable || e.status && (e.status >= 500 || e.status === 429)) {
2648
+ this.breaker.recordFailure();
2649
+ } else {
2650
+ this.breaker.recordSuccess();
2651
+ }
2652
+ } else {
2653
+ this.breaker.recordFailure();
2654
+ }
2655
+ this.events.emit("error", { error: e });
2656
+ throw e;
2657
+ }
2658
+ }
2659
+ getStats() {
2660
+ return {
2661
+ rateLimit: this.limiter.getStats(),
2662
+ circuit: this.breaker.getState()
2663
+ };
2664
+ }
2665
+ };
2666
+
2667
+ // src/flags.ts
2668
+ var FeatureFlags = class {
2669
+ constructor(initial) {
2670
+ this.values = {};
2671
+ this.values = { ...initial };
2672
+ }
2673
+ set(values) {
2674
+ this.values = { ...this.values, ...values };
2675
+ }
2676
+ isEnabled(key, fallback = false) {
2677
+ const v = this.values[key];
2678
+ return typeof v === "boolean" ? v : fallback;
2679
+ }
2680
+ get(key, fallback) {
2681
+ return this.values[key] ?? fallback;
2682
+ }
2683
+ all() {
2684
+ return { ...this.values };
2685
+ }
2686
+ };
2687
+ var RemoteConfig = class {
2688
+ constructor() {
2689
+ this.values = {};
2690
+ }
2691
+ set(values) {
2692
+ this.values = { ...this.values, ...values };
2693
+ }
2694
+ get(key, fallback) {
2695
+ return this.values[key] ?? fallback;
2696
+ }
2697
+ all() {
2698
+ return { ...this.values };
2699
+ }
2700
+ };
2701
+
2702
+ // src/diagnostics.ts
2703
+ 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 });
2704
+
2191
2705
  // src/client.ts
2192
- var DEFAULT_REVALIDATE_SECONDS = false;
2193
2706
  var NexusClient = class {
2194
- constructor(config) {
2195
- const fullConfig = getFullConfig(config);
2707
+ constructor(config = {}) {
2708
+ const full = getFullConfig(config);
2196
2709
  this.config = {
2197
- debug: config?.debug ?? false,
2198
- cacheStrategy: config?.cacheStrategy ?? "memory",
2199
- // Only fall through to the safe default when revalidateTime is
2200
- // genuinely *unset* (undefined). An explicit `false` from the caller
2201
- // is a deliberate "never revalidate" choice and must be respected,
2202
- // not silently upgraded — `??` (not `||`) is required here so that
2203
- // `0` (revalidate on every request) also passes through untouched
2204
- // instead of being treated as falsy.
2205
- revalidateTime: config?.revalidateTime ?? DEFAULT_REVALIDATE_SECONDS,
2206
- timeout: config?.timeout ?? 1e4,
2207
- retries: config?.retries ?? 3,
2208
- ...fullConfig
2710
+ debug: false,
2711
+ cacheStrategy: "memory",
2712
+ revalidateTime: false,
2713
+ timeout: 1e4,
2714
+ retries: 3,
2715
+ sdkVersion: SDK_VERSION,
2716
+ cacheInvalidation: "platform",
2717
+ environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production",
2718
+ autoTracking: true,
2719
+ publicKeyOnly: true,
2720
+ // 🚀 Default: fingerprinting: true (Active by default for high-precision analytics)
2721
+ privacy: { analytics: true, fingerprinting: true, redact: true },
2722
+ ...full,
2723
+ ...config
2209
2724
  };
2210
2725
  const errors = validateConfig(this.config);
2211
- if (errors.length > 0) {
2212
- console.warn("\u26A0\uFE0F NexusHub: Configuration issues:", errors.join(", "));
2213
- if (!this.config.projectId) {
2214
- console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Tracking will fail.");
2215
- }
2726
+ if (errors.length && this.config.debug) {
2727
+ console.warn("[GN-Apex] Configuration warnings:", errors);
2216
2728
  }
2729
+ this.events = new NexusEventBus();
2730
+ this.http = new NexusHttpClient(this.config, this.events);
2731
+ this.flags = new FeatureFlags();
2732
+ this.remoteConfig = new RemoteConfig();
2217
2733
  this.content = new ContentEngine(this.config);
2218
- if (typeof window !== "undefined") {
2734
+ if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false) {
2219
2735
  this.analytics = new AnalyticsEngine(this.config);
2220
2736
  this.analytics.start();
2221
2737
  }
2222
2738
  }
2223
2739
  /**
2224
- * Helper alias for cleaner content fetching.
2740
+ * Helper alias for cleaner page content fetching.
2225
2741
  */
2226
2742
  getPage(slug, options) {
2227
2743
  return this.content.getPage(slug, options);
2228
2744
  }
2229
2745
  /**
2230
- * Returns a readonly snapshot of the current config.
2746
+ * Returns a readonly snapshot of the active configuration.
2231
2747
  */
2232
2748
  getConfig() {
2233
- return { ...this.config };
2749
+ return Object.freeze({
2750
+ ...this.config,
2751
+ privacy: { ...this.config.privacy }
2752
+ });
2234
2753
  }
2235
2754
  /**
2236
- * Updates specific config fields at runtime.
2237
- * Replaces the previous pattern of `(nexus as any).config.projectId = x`
2238
- * which bypassed TypeScript and mutated internal state unsafely.
2755
+ * Updates runtime configuration dynamically without breaking active listeners.
2239
2756
  */
2240
2757
  updateConfig(updates) {
2241
- this.config = { ...this.config, ...updates };
2758
+ this.config = {
2759
+ ...this.config,
2760
+ ...updates,
2761
+ privacy: { ...this.config.privacy, ...updates.privacy }
2762
+ };
2763
+ this.http.updateConfig(this.config);
2764
+ this.content.updateConfig?.(this.config);
2765
+ if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false && !this.analytics) {
2766
+ this.analytics = new AnalyticsEngine(this.config);
2767
+ this.analytics.start();
2768
+ }
2769
+ if (this.config.privacy?.analytics === false) {
2770
+ this.analytics?.stop();
2771
+ }
2772
+ }
2773
+ /**
2774
+ * Returns deep system diagnostics including HTTP and cache performance.
2775
+ */
2776
+ diagnostics() {
2777
+ return {
2778
+ ...getDiagnostics(SDK_VERSION, this.config.environment ?? "production"),
2779
+ cache: this.content.getCacheStats(),
2780
+ http: this.http.getStats(),
2781
+ analyticsQueue: this.analytics ? void 0 : 0
2782
+ };
2783
+ }
2784
+ /**
2785
+ * Gracefully terminates background tasks and cleans up listeners.
2786
+ */
2787
+ destroy() {
2788
+ this.analytics?.stop(true);
2789
+ this.content.cleanup?.();
2790
+ this.events.clear();
2242
2791
  }
2243
2792
  };
2244
2793
  var nexus = new NexusClient();
@@ -2247,21 +2796,21 @@ var nexus = new NexusClient();
2247
2796
  import {
2248
2797
  createContext,
2249
2798
  useContext,
2250
- useEffect,
2251
- useState,
2252
- useCallback
2799
+ useEffect as useEffect2,
2800
+ useState as useState2,
2801
+ useCallback as useCallback2
2253
2802
  } from "react";
2254
- import { jsx } from "react/jsx-runtime";
2803
+ import { jsx as jsx2 } from "react/jsx-runtime";
2255
2804
  var AuthContext = createContext(null);
2256
2805
  var AuthProvider = ({
2257
2806
  children,
2258
2807
  config
2259
2808
  }) => {
2260
- const [user, setUser] = useState(null);
2261
- const [isLoading, setIsLoading] = useState(true);
2262
- const [error, setError] = useState(null);
2809
+ const [user, setUser] = useState2(null);
2810
+ const [isLoading, setIsLoading] = useState2(true);
2811
+ const [error, setError] = useState2(null);
2263
2812
  const AUTH_BASE = `${config.apiUrl}/auth/project/${config.projectId}`;
2264
- const checkSession = useCallback(async () => {
2813
+ const checkSession = useCallback2(async () => {
2265
2814
  try {
2266
2815
  const res = await fetch(`${AUTH_BASE}/me`, {
2267
2816
  headers: getHeaders(config)
@@ -2286,7 +2835,7 @@ var AuthProvider = ({
2286
2835
  setIsLoading(false);
2287
2836
  }
2288
2837
  }, [AUTH_BASE, config]);
2289
- useEffect(() => {
2838
+ useEffect2(() => {
2290
2839
  checkSession();
2291
2840
  }, [checkSession]);
2292
2841
  const login = async (creds) => {
@@ -2386,7 +2935,7 @@ var AuthProvider = ({
2386
2935
  });
2387
2936
  if (!res.ok) throw await parseError(res);
2388
2937
  };
2389
- return /* @__PURE__ */ jsx(
2938
+ return /* @__PURE__ */ jsx2(
2390
2939
  AuthContext.Provider,
2391
2940
  {
2392
2941
  value: {
@@ -2522,27 +3071,27 @@ var NexusPushClient = class {
2522
3071
  };
2523
3072
 
2524
3073
  // src/components/NexusProvider.tsx
2525
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
3074
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2526
3075
  var NexusContext = createContext2(nexus);
2527
3076
  var NexusLiveFeedContext = createContext2({
2528
3077
  latestEvent: null,
2529
3078
  isConnected: false
2530
3079
  });
2531
- var useNexusLiveFeed = () => React2.useContext(NexusLiveFeedContext);
3080
+ var useNexusLiveFeed = () => React3.useContext(NexusLiveFeedContext);
2532
3081
  function NexusAnalyticsTracker({
2533
3082
  disableAnalytics
2534
3083
  }) {
2535
3084
  const pathname = usePathname();
2536
3085
  const searchParams = useSearchParams();
2537
- const prevPathRef = useRef("");
3086
+ const prevPathRef = useRef2("");
2538
3087
  const searchParamsString = searchParams.toString();
2539
- useEffect2(() => {
3088
+ useEffect3(() => {
2540
3089
  if (nexus.analytics && !disableAnalytics) {
2541
3090
  nexus.analytics.pageView(prevPathRef.current || document.referrer);
2542
3091
  prevPathRef.current = pathname;
2543
3092
  }
2544
3093
  }, [pathname, searchParamsString, disableAnalytics]);
2545
- return /* @__PURE__ */ jsx2("div", { id: "__nexus_react_active", style: { display: "none" } });
3094
+ return /* @__PURE__ */ jsx3("div", { id: "__nexus_react_active", style: { display: "none" } });
2546
3095
  }
2547
3096
  var NexusProvider = ({
2548
3097
  children,
@@ -2554,23 +3103,23 @@ var NexusProvider = ({
2554
3103
  autoPromptPush = true
2555
3104
  // 🚀 Default: true for zero-config automatic visitor subscription
2556
3105
  }) => {
2557
- const isInitialized = useRef(false);
2558
- const socketRef = useRef(null);
2559
- const [latestEvent, setLatestEvent] = useState2(
3106
+ const isInitialized = useRef2(false);
3107
+ const socketRef = useRef2(null);
3108
+ const [latestEvent, setLatestEvent] = useState3(
2560
3109
  null
2561
3110
  );
2562
- const [isConnected, setIsConnected] = useState2(false);
2563
- const liveFeedValue = useMemo(
3111
+ const [isConnected, setIsConnected] = useState3(false);
3112
+ const liveFeedValue = useMemo2(
2564
3113
  () => ({ latestEvent, isConnected }),
2565
3114
  [latestEvent, isConnected]
2566
3115
  );
2567
- const config = useMemo(() => {
3116
+ const config = useMemo2(() => {
2568
3117
  if (projectId && nexus.getConfig().projectId !== projectId) {
2569
3118
  nexus.updateConfig({ projectId });
2570
3119
  }
2571
3120
  return nexus.getConfig();
2572
3121
  }, [projectId]);
2573
- useEffect2(() => {
3122
+ useEffect3(() => {
2574
3123
  if (typeof window === "undefined" || !("serviceWorker" in navigator))
2575
3124
  return;
2576
3125
  navigator.serviceWorker.register("/sw.js").then(async (registration) => {
@@ -2593,7 +3142,7 @@ var NexusProvider = ({
2593
3142
  console.warn("[NexusHub] Service Worker registration failed:", err);
2594
3143
  });
2595
3144
  }, [autoPromptPush]);
2596
- useEffect2(() => {
3145
+ useEffect3(() => {
2597
3146
  if (typeof window === "undefined" || disableAnalytics || !hasConsent)
2598
3147
  return;
2599
3148
  if (!isInitialized.current) {
@@ -2613,7 +3162,7 @@ var NexusProvider = ({
2613
3162
  }
2614
3163
  };
2615
3164
  }, [disableAnalytics, hasConsent]);
2616
- const connectLiveFeed = useCallback2(async () => {
3165
+ const connectLiveFeed = useCallback3(async () => {
2617
3166
  if (typeof window === "undefined" || !enableLiveFeed) return;
2618
3167
  try {
2619
3168
  const { io } = await import("socket.io-client");
@@ -2643,7 +3192,7 @@ var NexusProvider = ({
2643
3192
  console.error("[NexusHub] Live feed connection failed:", err);
2644
3193
  }
2645
3194
  }, [enableLiveFeed, onLiveEvent]);
2646
- useEffect2(() => {
3195
+ useEffect3(() => {
2647
3196
  if (enableLiveFeed) {
2648
3197
  connectLiveFeed();
2649
3198
  }
@@ -2655,13 +3204,13 @@ var NexusProvider = ({
2655
3204
  }
2656
3205
  };
2657
3206
  }, [enableLiveFeed, connectLiveFeed]);
2658
- return /* @__PURE__ */ jsx2(NexusContext.Provider, { value: nexus, children: /* @__PURE__ */ jsx2(NexusLiveFeedContext.Provider, { value: liveFeedValue, children: /* @__PURE__ */ jsxs(AuthProvider, { config, children: [
2659
- /* @__PURE__ */ jsx2(Suspense, { fallback: null, children: /* @__PURE__ */ jsx2(NexusAnalyticsTracker, { disableAnalytics }) }),
3207
+ return /* @__PURE__ */ jsx3(NexusContext.Provider, { value: nexus, children: /* @__PURE__ */ jsx3(NexusLiveFeedContext.Provider, { value: liveFeedValue, children: /* @__PURE__ */ jsxs2(AuthProvider, { config, children: [
3208
+ /* @__PURE__ */ jsx3(Suspense, { fallback: null, children: /* @__PURE__ */ jsx3(NexusAnalyticsTracker, { disableAnalytics }) }),
2660
3209
  children
2661
3210
  ] }) }) });
2662
3211
  };
2663
3212
  var useNexus = () => {
2664
- const context = React2.useContext(NexusContext);
3213
+ const context = React3.useContext(NexusContext);
2665
3214
  if (!context) {
2666
3215
  throw new Error("useNexus must be used within a NexusProvider");
2667
3216
  }
@@ -2696,26 +3245,56 @@ var useNexusAnalytics = () => {
2696
3245
  };
2697
3246
 
2698
3247
  // src/components/NexusRenders.tsx
2699
- import { useState as useState3 } from "react";
3248
+ import { useState as useState4 } from "react";
2700
3249
  import * as LucideIcons from "lucide-react";
2701
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2702
- function NexusRichText({ value, className = "" }) {
3250
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3251
+ function sanitizeHtml(html) {
3252
+ if (!html) return "";
3253
+ if (typeof window === "undefined") {
3254
+ return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, "").replace(/\bon\w+\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]+)/gi, "").replace(/href\s*=\s*['"]\s*javascript:[^'"]*['"]/gi, 'href="#"').replace(/src\s*=\s*['"]\s*javascript:[^'"]*['"]/gi, 'src=""');
3255
+ }
3256
+ const parser = new DOMParser();
3257
+ const doc = parser.parseFromString(html, "text/html");
3258
+ doc.querySelectorAll("script, noscript").forEach((n) => n.remove());
3259
+ doc.querySelectorAll("*").forEach((el) => {
3260
+ [...el.attributes].forEach((attr) => {
3261
+ if (attr.name.toLowerCase().startsWith("on")) {
3262
+ el.removeAttribute(attr.name);
3263
+ }
3264
+ });
3265
+ const href = el.getAttribute("href");
3266
+ const src = el.getAttribute("src");
3267
+ if (href && /^\s*javascript:/i.test(href)) el.removeAttribute("href");
3268
+ if (src && /^\s*javascript:/i.test(src)) el.removeAttribute("src");
3269
+ });
3270
+ return doc.body.innerHTML;
3271
+ }
3272
+ function NexusRichText({
3273
+ value,
3274
+ className = "",
3275
+ hydrateCharts = true,
3276
+ onCommentClick,
3277
+ onImageClick
3278
+ }) {
2703
3279
  if (!value) return null;
2704
- return /* @__PURE__ */ jsx3(
2705
- "div",
3280
+ return /* @__PURE__ */ jsx4(
3281
+ NexusRenderer,
2706
3282
  {
2707
- className: `prose dark:prose-invert max-w-none text-foreground leading-relaxed ${className}`,
2708
- dangerouslySetInnerHTML: { __html: value }
3283
+ content: value,
3284
+ className,
3285
+ hydrateCharts,
3286
+ onCommentClick,
3287
+ onImageClick
2709
3288
  }
2710
3289
  );
2711
3290
  }
2712
3291
  function NexusLongText({ value, className = "" }) {
2713
3292
  if (!value) return null;
2714
- return /* @__PURE__ */ jsx3(
3293
+ return /* @__PURE__ */ jsx4(
2715
3294
  "p",
2716
3295
  {
2717
3296
  className: `text-sm text-muted-foreground whitespace-pre-line leading-relaxed ${className}`,
2718
- dangerouslySetInnerHTML: { __html: value }
3297
+ dangerouslySetInnerHTML: { __html: sanitizeHtml(value) }
2719
3298
  }
2720
3299
  );
2721
3300
  }
@@ -2729,9 +3308,9 @@ function NexusIcon({
2729
3308
  const IconComponent = LucideIcons[name];
2730
3309
  if (!IconComponent) {
2731
3310
  const Fallback = LucideIcons.HelpCircle;
2732
- return /* @__PURE__ */ jsx3(Fallback, { className, size, strokeWidth });
3311
+ return /* @__PURE__ */ jsx4(Fallback, { className, size, strokeWidth });
2733
3312
  }
2734
- return /* @__PURE__ */ jsx3(
3313
+ return /* @__PURE__ */ jsx4(
2735
3314
  IconComponent,
2736
3315
  {
2737
3316
  className,
@@ -2752,11 +3331,13 @@ function NexusImage({
2752
3331
  if (!imageUrl) return null;
2753
3332
  return (
2754
3333
  /* eslint-disable-next-line @next/next/no-img-element */
2755
- /* @__PURE__ */ jsx3(
3334
+ /* @__PURE__ */ jsx4(
2756
3335
  "img",
2757
3336
  {
2758
3337
  src: imageUrl,
2759
3338
  alt: imageAlt,
3339
+ loading: "lazy",
3340
+ decoding: "async",
2760
3341
  className: `max-w-full h-auto object-cover rounded-xl ${className}`,
2761
3342
  ...props
2762
3343
  }
@@ -2769,15 +3350,15 @@ function NexusGallery({
2769
3350
  imageClassName = ""
2770
3351
  }) {
2771
3352
  if (!value || value.length === 0) return null;
2772
- return /* @__PURE__ */ jsx3(
3353
+ return /* @__PURE__ */ jsx4(
2773
3354
  "div",
2774
3355
  {
2775
3356
  className: `grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 ${className}`,
2776
- children: value.map((img, idx) => /* @__PURE__ */ jsx3(
3357
+ children: value.map((img, idx) => /* @__PURE__ */ jsx4(
2777
3358
  "div",
2778
3359
  {
2779
3360
  className: "overflow-hidden rounded-xl aspect-square bg-muted/20 border border-border",
2780
- children: /* @__PURE__ */ jsx3(
3361
+ children: /* @__PURE__ */ jsx4(
2781
3362
  NexusImage,
2782
3363
  {
2783
3364
  value: img,
@@ -2799,8 +3380,8 @@ function NexusVideo({
2799
3380
  const isYouTube = value.includes("youtube.com") || value.includes("youtu.be");
2800
3381
  const isVimeo = value.includes("vimeo.com");
2801
3382
  if (isYouTube) {
2802
- const videoId = value.includes("youtu.be") ? value.split("/").pop() : value.split("v=")[1]?.split("&")[0];
2803
- return /* @__PURE__ */ jsx3(
3383
+ const videoId = value.includes("youtu.be") ? value.split("/").pop()?.split("?")[0] : value.split("v=")[1]?.split("&")[0];
3384
+ return /* @__PURE__ */ jsx4(
2804
3385
  "iframe",
2805
3386
  {
2806
3387
  src: `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}`,
@@ -2812,8 +3393,8 @@ function NexusVideo({
2812
3393
  );
2813
3394
  }
2814
3395
  if (isVimeo) {
2815
- const videoId = value.split("/").pop();
2816
- return /* @__PURE__ */ jsx3(
3396
+ const videoId = value.split("/").pop()?.split("?")[0];
3397
+ return /* @__PURE__ */ jsx4(
2817
3398
  "iframe",
2818
3399
  {
2819
3400
  src: `https://player.vimeo.com/video/${videoId}?autoplay=${autoplay ? 1 : 0}`,
@@ -2824,7 +3405,7 @@ function NexusVideo({
2824
3405
  }
2825
3406
  );
2826
3407
  }
2827
- return /* @__PURE__ */ jsx3(
3408
+ return /* @__PURE__ */ jsx4(
2828
3409
  "video",
2829
3410
  {
2830
3411
  src: value,
@@ -2842,11 +3423,11 @@ function NexusMap({ value, className = "" }) {
2842
3423
  value.address || `${value.lat},${value.lng}`
2843
3424
  );
2844
3425
  const embedUrl = `https://maps.google.com/maps?q=${query}&t=&z=13&ie=UTF8&iwloc=&output=embed`;
2845
- return /* @__PURE__ */ jsx3(
3426
+ return /* @__PURE__ */ jsx4(
2846
3427
  "div",
2847
3428
  {
2848
3429
  className: `overflow-hidden rounded-xl border border-border aspect-video w-full ${className}`,
2849
- children: /* @__PURE__ */ jsx3(
3430
+ children: /* @__PURE__ */ jsx4(
2850
3431
  "iframe",
2851
3432
  {
2852
3433
  title: "Embedded Map",
@@ -2867,15 +3448,15 @@ function NexusColor({
2867
3448
  showHexLabel = true
2868
3449
  }) {
2869
3450
  if (!value) return null;
2870
- return /* @__PURE__ */ jsxs2("div", { className: `flex items-center gap-2.5 ${className}`, children: [
2871
- /* @__PURE__ */ jsx3(
3451
+ return /* @__PURE__ */ jsxs3("div", { className: `flex items-center gap-2.5 ${className}`, children: [
3452
+ /* @__PURE__ */ jsx4(
2872
3453
  "div",
2873
3454
  {
2874
3455
  className: "h-6 w-6 rounded-full border border-border/80 shadow-xs shrink-0",
2875
3456
  style: { backgroundColor: value }
2876
3457
  }
2877
3458
  ),
2878
- showHexLabel && /* @__PURE__ */ jsx3("span", { className: "font-mono text-xs font-semibold text-foreground/80 uppercase", children: value })
3459
+ showHexLabel && /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs font-semibold text-foreground/80 uppercase", children: value })
2879
3460
  ] });
2880
3461
  }
2881
3462
  function NexusGradient({
@@ -2884,9 +3465,9 @@ function NexusGradient({
2884
3465
  className = "",
2885
3466
  asTextMask = false
2886
3467
  }) {
2887
- if (!value) return /* @__PURE__ */ jsx3(Fragment, { children });
3468
+ if (!value) return /* @__PURE__ */ jsx4(Fragment2, { children });
2888
3469
  if (asTextMask) {
2889
- return /* @__PURE__ */ jsx3(
3470
+ return /* @__PURE__ */ jsx4(
2890
3471
  "span",
2891
3472
  {
2892
3473
  className: `bg-clip-text text-transparent font-bold ${className}`,
@@ -2895,7 +3476,7 @@ function NexusGradient({
2895
3476
  }
2896
3477
  );
2897
3478
  }
2898
- return /* @__PURE__ */ jsx3(
3479
+ return /* @__PURE__ */ jsx4(
2899
3480
  "div",
2900
3481
  {
2901
3482
  className: `rounded-xl ${className}`,
@@ -2912,26 +3493,26 @@ function NexusAddress({ value, className = "" }) {
2912
3493
  value.country
2913
3494
  ].filter(Boolean);
2914
3495
  if (lines.length === 0) return null;
2915
- return /* @__PURE__ */ jsxs2(
3496
+ return /* @__PURE__ */ jsxs3(
2916
3497
  "div",
2917
3498
  {
2918
3499
  className: `flex items-start gap-2.5 p-3 rounded-xl border border-border bg-card/50 ${className}`,
2919
3500
  children: [
2920
- /* @__PURE__ */ jsx3(LucideIcons.MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0 mt-0.5" }),
2921
- /* @__PURE__ */ jsx3("div", { className: "text-xs text-foreground/80 leading-relaxed font-medium", children: lines.map((line, i) => /* @__PURE__ */ jsx3("div", { children: line }, i)) })
3501
+ /* @__PURE__ */ jsx4(LucideIcons.MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0 mt-0.5" }),
3502
+ /* @__PURE__ */ jsx4("div", { className: "text-xs text-foreground/80 leading-relaxed font-medium", children: lines.map((line, i) => /* @__PURE__ */ jsx4("div", { children: line }, i)) })
2922
3503
  ]
2923
3504
  }
2924
3505
  );
2925
3506
  }
2926
3507
  function NexusKeyValue({ value, className = "" }) {
2927
3508
  if (!value || Object.keys(value).length === 0) return null;
2928
- return /* @__PURE__ */ jsx3(
3509
+ return /* @__PURE__ */ jsx4(
2929
3510
  "div",
2930
3511
  {
2931
3512
  className: `border border-border rounded-xl overflow-hidden bg-card/30 divide-y divide-border ${className}`,
2932
- children: Object.entries(value).map(([k, v]) => /* @__PURE__ */ jsxs2("div", { className: "flex text-xs px-4 py-3 gap-4", children: [
2933
- /* @__PURE__ */ jsx3("div", { className: "w-1/3 font-bold text-muted-foreground select-none uppercase tracking-wider text-[10px]", children: k }),
2934
- /* @__PURE__ */ jsx3("div", { className: "flex-1 font-medium text-foreground", children: v })
3513
+ children: Object.entries(value).map(([k, v]) => /* @__PURE__ */ jsxs3("div", { className: "flex text-xs px-4 py-3 gap-4", children: [
3514
+ /* @__PURE__ */ jsx4("div", { className: "w-1/3 font-bold text-muted-foreground select-none uppercase tracking-wider text-[10px]", children: k }),
3515
+ /* @__PURE__ */ jsx4("div", { className: "flex-1 font-medium text-foreground", children: v })
2935
3516
  ] }, k))
2936
3517
  }
2937
3518
  );
@@ -2942,7 +3523,7 @@ function NexusTags({
2942
3523
  badgeClassName = ""
2943
3524
  }) {
2944
3525
  if (!value || value.length === 0) return null;
2945
- return /* @__PURE__ */ jsx3("div", { className: `flex flex-wrap gap-1.5 ${className}`, children: value.map((tag) => /* @__PURE__ */ jsx3(
3526
+ return /* @__PURE__ */ jsx4("div", { className: `flex flex-wrap gap-1.5 ${className}`, children: value.map((tag) => /* @__PURE__ */ jsx4(
2946
3527
  "span",
2947
3528
  {
2948
3529
  className: `inline-flex items-center rounded-lg border border-border bg-muted/40 px-2.5 py-1 text-xs font-semibold text-foreground/80 ${badgeClassName}`,
@@ -2958,8 +3539,8 @@ function NexusProgress({
2958
3539
  color = "bg-primary"
2959
3540
  }) {
2960
3541
  if (value === null) return null;
2961
- const percent = Math.min(value / max * 100, 100);
2962
- return /* @__PURE__ */ jsx3("div", { className: `w-full ${className}`, children: /* @__PURE__ */ jsx3("div", { className: "h-2 w-full rounded-full bg-muted overflow-hidden border border-border/30", children: /* @__PURE__ */ jsx3(
3542
+ const percent = Math.min(Math.max(value / max * 100, 0), 100);
3543
+ return /* @__PURE__ */ jsx4("div", { className: `w-full ${className}`, children: /* @__PURE__ */ jsx4("div", { className: "h-2 w-full rounded-full bg-muted overflow-hidden border border-border/30", children: /* @__PURE__ */ jsx4(
2963
3544
  "div",
2964
3545
  {
2965
3546
  className: `h-full rounded-full transition-all duration-500 ${color}`,
@@ -2973,7 +3554,7 @@ function NexusBlendContainer({
2973
3554
  className = ""
2974
3555
  }) {
2975
3556
  const blendStyle = mode ? { mixBlendMode: mode } : {};
2976
- return /* @__PURE__ */ jsx3("div", { className, style: blendStyle, children });
3557
+ return /* @__PURE__ */ jsx4("div", { className, style: blendStyle, children });
2977
3558
  }
2978
3559
  function NexusCode({
2979
3560
  value,
@@ -2981,39 +3562,54 @@ function NexusCode({
2981
3562
  className = "",
2982
3563
  showLineNumbers = false
2983
3564
  }) {
2984
- const [copied, setCopied] = useState3(false);
3565
+ const [copied, setCopied] = useState4(false);
2985
3566
  if (!value) return null;
2986
3567
  const handleCopy = () => {
2987
- navigator.clipboard.writeText(value);
3568
+ if (navigator.clipboard && window.isSecureContext) {
3569
+ navigator.clipboard.writeText(value);
3570
+ } else {
3571
+ const textArea = document.createElement("textarea");
3572
+ textArea.value = value;
3573
+ textArea.style.position = "fixed";
3574
+ textArea.style.left = "-999999px";
3575
+ document.body.appendChild(textArea);
3576
+ textArea.focus();
3577
+ textArea.select();
3578
+ try {
3579
+ document.execCommand("copy");
3580
+ } catch {
3581
+ }
3582
+ textArea.remove();
3583
+ }
2988
3584
  setCopied(true);
2989
3585
  setTimeout(() => setCopied(false), 2e3);
2990
3586
  };
2991
3587
  const lines = value.split("\n");
2992
- return /* @__PURE__ */ jsxs2(
3588
+ return /* @__PURE__ */ jsxs3(
2993
3589
  "div",
2994
3590
  {
2995
3591
  className: `relative rounded-xl border border-border bg-[#090A0F] overflow-hidden ${className}`,
2996
3592
  children: [
2997
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border bg-white/2 select-none", children: [
2998
- /* @__PURE__ */ jsx3("span", { className: "text-[10px] font-black uppercase text-muted-foreground tracking-wider", children: language }),
2999
- /* @__PURE__ */ jsx3(
3593
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border bg-white/2 select-none", children: [
3594
+ /* @__PURE__ */ jsx4("span", { className: "text-[10px] font-black uppercase text-muted-foreground tracking-wider", children: language }),
3595
+ /* @__PURE__ */ jsx4(
3000
3596
  "button",
3001
3597
  {
3002
3598
  onClick: handleCopy,
3003
3599
  className: "flex items-center gap-1.5 text-[10px] font-bold text-muted-foreground hover:text-primary transition-colors cursor-pointer",
3004
- children: copied ? /* @__PURE__ */ jsxs2(Fragment, { children: [
3005
- /* @__PURE__ */ jsx3(LucideIcons.Check, { className: "h-3 w-3 text-emerald-400" }),
3006
- /* @__PURE__ */ jsx3("span", { className: "text-emerald-400", children: "Copied!" })
3007
- ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
3008
- /* @__PURE__ */ jsx3(LucideIcons.Copy, { className: "h-3 w-3" }),
3009
- /* @__PURE__ */ jsx3("span", { children: "Copy Code" })
3600
+ children: copied ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
3601
+ /* @__PURE__ */ jsx4(LucideIcons.Check, { className: "h-3 w-3 text-emerald-400" }),
3602
+ /* @__PURE__ */ jsx4("span", { className: "text-emerald-400", children: "Copied!" })
3603
+ ] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
3604
+ /* @__PURE__ */ jsx4(LucideIcons.Copy, { className: "h-3 w-3" }),
3605
+ /* @__PURE__ */ jsx4("span", { children: "Copy Code" })
3010
3606
  ] })
3011
3607
  }
3012
3608
  )
3013
3609
  ] }),
3014
- /* @__PURE__ */ jsxs2("div", { className: "flex overflow-x-auto p-4 font-mono text-[11px] leading-relaxed text-emerald-400 select-text", children: [
3015
- showLineNumbers && /* @__PURE__ */ jsx3("div", { className: "flex flex-col text-right text-gray-600 select-none pr-3.5 border-r border-border/30 mr-3.5", children: lines.map((_, i) => /* @__PURE__ */ jsx3("span", { children: i + 1 }, i)) }),
3016
- /* @__PURE__ */ jsx3("pre", { className: "flex-1 whitespace-pre", children: value })
3610
+ /* @__PURE__ */ jsxs3("div", { className: "flex overflow-x-auto p-4 font-mono text-[11px] leading-relaxed text-emerald-400 select-text", children: [
3611
+ showLineNumbers && /* @__PURE__ */ jsx4("div", { className: "flex flex-col text-right text-gray-600 select-none pr-3.5 border-r border-border/30 mr-3.5", children: lines.map((_, i) => /* @__PURE__ */ jsx4("span", { children: i + 1 }, i)) }),
3612
+ /* @__PURE__ */ jsx4("pre", { className: "flex-1 whitespace-pre", children: value })
3017
3613
  ] })
3018
3614
  ]
3019
3615
  }
@@ -3026,12 +3622,12 @@ function NexusBoolean({
3026
3622
  falseLabel = "Inactive"
3027
3623
  }) {
3028
3624
  if (value === null) return null;
3029
- return /* @__PURE__ */ jsxs2(
3625
+ return /* @__PURE__ */ jsxs3(
3030
3626
  "span",
3031
3627
  {
3032
3628
  className: `inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-black uppercase tracking-wider border shadow-sm ${value ? "border-emerald-500/20 bg-emerald-500/10 text-emerald-400" : "border-muted-foreground/20 bg-muted-foreground/10 text-muted-foreground"} ${className}`,
3033
3629
  children: [
3034
- /* @__PURE__ */ jsx3(
3630
+ /* @__PURE__ */ jsx4(
3035
3631
  "span",
3036
3632
  {
3037
3633
  className: `h-1.5 w-1.5 rounded-full ${value ? "bg-emerald-500 animate-pulse" : "bg-muted-foreground"}`
@@ -3058,6 +3654,7 @@ export {
3058
3654
  NexusMap,
3059
3655
  NexusProgress,
3060
3656
  NexusProvider,
3657
+ NexusRenderer,
3061
3658
  NexusRichText,
3062
3659
  NexusTags,
3063
3660
  NexusVideo,