@nexushub/client 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,51 +1,2649 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
-
3
-
4
-
5
-
6
-
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
7
32
 
33
+ // src/config.ts
34
+ var DEFAULT_API_URL, DEFAULT_ANALYTICS_URL, LOCAL_NEST_URL, LOCAL_RUST_URL, getEnvConfig, mergeConfigs, getFullConfig, validateConfig;
35
+ var init_config = __esm({
36
+ "src/config.ts"() {
37
+ "use strict";
38
+ DEFAULT_API_URL = "https://endpoints.gnexus.co.tz";
39
+ DEFAULT_ANALYTICS_URL = "https://sentry.gnexus.co.tz";
40
+ LOCAL_NEST_URL = "https://endpoints.gnexus.co.tz";
41
+ LOCAL_RUST_URL = "https://sentry.gnexus.co.tz";
42
+ getEnvConfig = () => {
43
+ const NEXT_PUBLIC_ID = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_ID : void 0;
44
+ const NEXT_PUBLIC_KEY = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_KEY : void 0;
45
+ const NEXT_PUBLIC_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_API_URL : void 0;
46
+ const NODE_ID = typeof process !== "undefined" ? process.env.NEXUS_PROJECT_ID : void 0;
47
+ const NODE_KEY = typeof process !== "undefined" ? process.env.NEXUS_API_KEY : void 0;
48
+ const NODE_URL = typeof process !== "undefined" ? process.env.NEXUS_API_URL : void 0;
49
+ const NODE_ENV = typeof process !== "undefined" ? process.env.NODE_ENV : "production";
50
+ const isDev = NODE_ENV === "development" || typeof window !== "undefined" && window.location.hostname === "localhost";
51
+ const projectId = NEXT_PUBLIC_ID ?? NODE_ID;
52
+ const apiKey = NEXT_PUBLIC_KEY ?? NODE_KEY;
53
+ const apiUrl = NEXT_PUBLIC_URL ?? NODE_URL ?? (isDev ? LOCAL_NEST_URL : DEFAULT_API_URL);
54
+ const analyticsUrl = apiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL;
55
+ return {
56
+ projectId,
57
+ apiKey,
58
+ apiUrl,
59
+ analyticsUrl
60
+ };
61
+ };
62
+ mergeConfigs = (base, override) => {
63
+ const mergedApiUrl = override.apiUrl || base.apiUrl || DEFAULT_API_URL;
64
+ const mergedAnalyticsUrl = override.analyticsUrl || base.analyticsUrl || (mergedApiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL);
65
+ return {
66
+ ...base,
67
+ ...override,
68
+ apiUrl: mergedApiUrl,
69
+ analyticsUrl: mergedAnalyticsUrl,
70
+ projectId: override.projectId || base.projectId,
71
+ apiKey: override.apiKey || base.apiKey
72
+ };
73
+ };
74
+ getFullConfig = (partialConfig) => {
75
+ const envConfig = getEnvConfig();
76
+ return mergeConfigs(envConfig, partialConfig || {});
77
+ };
78
+ validateConfig = (config) => {
79
+ const errors = [];
80
+ if (!config.projectId) errors.push("Missing projectId");
81
+ if (!config.apiUrl) errors.push("Missing apiUrl");
82
+ return errors;
83
+ };
84
+ }
85
+ });
8
86
 
87
+ // src/content/binary-compiler.ts
88
+ var crypto2, BinaryCompiler;
89
+ var init_binary_compiler = __esm({
90
+ "src/content/binary-compiler.ts"() {
91
+ "use strict";
92
+ crypto2 = __toESM(require("crypto"), 1);
93
+ BinaryCompiler = class {
94
+ /**
95
+ * Generates a secure, deterministic cryptographic key from the project's API key.
96
+ */
97
+ static deriveKeys(apiKey) {
98
+ const hash = crypto2.createHash("sha256").update(apiKey).digest();
99
+ const encryptionKey = hash;
100
+ const hmacKey = crypto2.createHmac("sha256", apiKey).update("nexus-integrity-key").digest();
101
+ return { encryptionKey, hmacKey };
102
+ }
103
+ /**
104
+ * Compiles JSON content data into a secure, signed, and encrypted .nx binary buffer.
105
+ */
106
+ static compile(data, apiKey, projectId, metadataOverrides = {}) {
107
+ const { encryptionKey, hmacKey } = this.deriveKeys(apiKey);
108
+ const metadata = {
109
+ version: "2.0.0",
110
+ compiledAt: (/* @__PURE__ */ new Date()).toISOString(),
111
+ projectId,
112
+ ...metadataOverrides
113
+ };
114
+ const metadataStr = JSON.stringify(metadata);
115
+ const metadataBuffer = Buffer.from(metadataStr, "utf-8");
116
+ const iv = crypto2.randomBytes(16);
117
+ const cipher = crypto2.createCipheriv(this.ALGORITHM, encryptionKey, iv);
118
+ const plainTextPayload = JSON.stringify(data);
119
+ let encryptedPayload = cipher.update(plainTextPayload, "utf8");
120
+ encryptedPayload = Buffer.concat([encryptedPayload, cipher.final()]);
121
+ const payloadBuffer = Buffer.concat([iv, encryptedPayload]);
122
+ const headerBuffer = Buffer.from(this.MAGIC_HEADER, "ascii");
123
+ const metaLengthBuffer = Buffer.alloc(4);
124
+ metaLengthBuffer.writeUInt32BE(metadataBuffer.length, 0);
125
+ const payloadLengthBuffer = Buffer.alloc(4);
126
+ payloadLengthBuffer.writeUInt32BE(payloadBuffer.length, 0);
127
+ const prefixBlock = Buffer.concat([
128
+ headerBuffer,
129
+ metaLengthBuffer,
130
+ metadataBuffer,
131
+ payloadLengthBuffer,
132
+ payloadBuffer
133
+ ]);
134
+ const hmac = crypto2.createHmac("sha256", hmacKey);
135
+ hmac.update(prefixBlock);
136
+ const signature = hmac.digest();
137
+ return Buffer.concat([prefixBlock, signature]);
138
+ }
139
+ /**
140
+ * Verifies, decrypts, and decompiles a .nx binary buffer back into clean, type-safe JSON.
141
+ * Throws structured errors on integrity verification failures or file corruption.
142
+ */
143
+ static decompile(buffer, apiKey) {
144
+ if (buffer.length < 44) {
145
+ throw new Error(
146
+ "Corrupted File: Binary payload is too short to be a valid Nexus node."
147
+ );
148
+ }
149
+ const { encryptionKey, hmacKey } = this.deriveKeys(apiKey);
150
+ const prefixBlockLength = buffer.length - 32;
151
+ const prefixBlock = buffer.subarray(0, prefixBlockLength);
152
+ const expectedSignature = buffer.subarray(prefixBlockLength);
153
+ const hmac = crypto2.createHmac("sha256", hmacKey);
154
+ hmac.update(prefixBlock);
155
+ const actualSignature = hmac.digest();
156
+ if (!crypto2.timingSafeEqual(expectedSignature, actualSignature)) {
157
+ throw new Error(
158
+ "DATA_INTEGRITY_VIOLATION: Cryptographic signature mismatch. This local node has been modified externally or tampered with."
159
+ );
160
+ }
161
+ const magicHeader = prefixBlock.subarray(0, 4).toString("ascii");
162
+ if (magicHeader !== this.MAGIC_HEADER) {
163
+ throw new Error(
164
+ "Invalid Format: File lacks the correct Nexus binary magic header."
165
+ );
166
+ }
167
+ const metaLength = prefixBlock.readUInt32BE(4);
168
+ const metaStart = 8;
169
+ const metaEnd = metaStart + metaLength;
170
+ const metadataStr = prefixBlock.subarray(metaStart, metaEnd).toString("utf-8");
171
+ const metadata = JSON.parse(metadataStr);
172
+ const payloadLength = prefixBlock.readUInt32BE(metaEnd);
173
+ const payloadStart = metaEnd + 4;
174
+ const payloadEnd = payloadStart + payloadLength;
175
+ const payloadBuffer = prefixBlock.subarray(payloadStart, payloadEnd);
176
+ const iv = payloadBuffer.subarray(0, 16);
177
+ const cipherText = payloadBuffer.subarray(16);
178
+ const decipher = crypto2.createDecipheriv(this.ALGORITHM, encryptionKey, iv);
179
+ let decrypted = decipher.update(cipherText);
180
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
181
+ const data = JSON.parse(decrypted.toString("utf8"));
182
+ return { data, metadata };
183
+ }
184
+ };
185
+ BinaryCompiler.MAGIC_HEADER = "NEXS";
186
+ // 4-byte ASCII magic identifier
187
+ BinaryCompiler.ALGORITHM = "aes-256-cbc";
188
+ }
189
+ });
9
190
 
191
+ // src/content/local-cache-server.ts
192
+ var local_cache_server_exports = {};
193
+ __export(local_cache_server_exports, {
194
+ LocalCache: () => LocalCache
195
+ });
196
+ var import_fs, import_path, LocalCache;
197
+ var init_local_cache_server = __esm({
198
+ "src/content/local-cache-server.ts"() {
199
+ "use strict";
200
+ import_fs = __toESM(require("fs"), 1);
201
+ import_path = __toESM(require("path"), 1);
202
+ init_config();
203
+ init_binary_compiler();
204
+ LocalCache = class {
205
+ constructor(customPath) {
206
+ this.apiKey = "";
207
+ this.baseDir = customPath || ".nexus/local";
208
+ const config = getEnvConfig();
209
+ this.apiKey = config.apiKey || "";
210
+ }
211
+ isLoaded() {
212
+ return import_fs.default.existsSync(import_path.default.resolve(process.cwd(), this.baseDir));
213
+ }
214
+ /**
215
+ * Helper to decrypt and decompile `.nx` files on-the-fly.
216
+ */
217
+ decompileFile(filePath) {
218
+ if (!this.apiKey) {
219
+ if (process.env.NODE_ENV === "development") {
220
+ console.warn(
221
+ "\u26A0\uFE0F NexusHub: Missing NEXUS_API_KEY. Local decryption bypassed."
222
+ );
223
+ }
224
+ return null;
225
+ }
226
+ try {
227
+ const encryptedBuffer = import_fs.default.readFileSync(filePath);
228
+ const decompiled = BinaryCompiler.decompile(encryptedBuffer, this.apiKey);
229
+ return decompiled.data;
230
+ } catch (error) {
231
+ if (process.env.NODE_ENV === "development") {
232
+ console.error(
233
+ `
234
+ \u{1F6A8} NexusHub Security Alert: Data Integrity Violation in file:
235
+ ${filePath}
236
+ Error: ${error.message}
237
+ This file has been disabled until regenerated via npx nexus pull.
238
+ `
239
+ );
240
+ }
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Retrieve a specific page directly from its .nx file.
246
+ */
247
+ async getPage(slug) {
248
+ const filePath = import_path.default.resolve(
249
+ process.cwd(),
250
+ this.baseDir,
251
+ "pages",
252
+ `${slug}.nx`
253
+ );
254
+ if (import_fs.default.existsSync(filePath)) {
255
+ const pageData = this.decompileFile(filePath);
256
+ if (!pageData) return null;
257
+ return pageData.data !== void 0 ? pageData.data : pageData;
258
+ }
259
+ return null;
260
+ }
261
+ /**
262
+ * Retrieve an entire collection from its specific .nx file.
263
+ */
264
+ async getCollection(collectionId) {
265
+ const filePath = import_path.default.resolve(
266
+ process.cwd(),
267
+ this.baseDir,
268
+ "collections",
269
+ `${collectionId}.nx`
270
+ );
271
+ if (import_fs.default.existsSync(filePath)) {
272
+ return this.decompileFile(filePath);
273
+ }
274
+ return null;
275
+ }
276
+ /**
277
+ * Retrieve global settings from its .nx file.
278
+ */
279
+ async getGlobals() {
280
+ const filePath = import_path.default.resolve(process.cwd(), this.baseDir, "globals.nx");
281
+ if (import_fs.default.existsSync(filePath)) {
282
+ return this.decompileFile(filePath);
283
+ }
284
+ return null;
285
+ }
286
+ /**
287
+ * Returns a merged JSON of the entire workspace structure.
288
+ */
289
+ async getAllData() {
290
+ const pages = {};
291
+ const collections = {};
292
+ let globals = {};
293
+ try {
294
+ const pagesDir = import_path.default.resolve(process.cwd(), this.baseDir, "pages");
295
+ if (import_fs.default.existsSync(pagesDir)) {
296
+ for (const file of import_fs.default.readdirSync(pagesDir)) {
297
+ if (file.endsWith(".nx")) {
298
+ const slug = import_path.default.basename(file, ".nx");
299
+ pages[slug] = await this.getPage(slug);
300
+ }
301
+ }
302
+ }
303
+ const colsDir = import_path.default.resolve(process.cwd(), this.baseDir, "collections");
304
+ if (import_fs.default.existsSync(colsDir)) {
305
+ for (const file of import_fs.default.readdirSync(colsDir)) {
306
+ if (file.endsWith(".nx")) {
307
+ const id = import_path.default.basename(file, ".nx");
308
+ collections[id] = await this.getCollection(id) || [];
309
+ }
310
+ }
311
+ }
312
+ globals = await this.getGlobals() || {};
313
+ } catch {
314
+ }
315
+ return { pages, collections, globals };
316
+ }
317
+ };
318
+ }
319
+ });
10
320
 
321
+ // src/content/local-cache-client.ts
322
+ var local_cache_client_exports = {};
323
+ __export(local_cache_client_exports, {
324
+ LocalCache: () => LocalCache2
325
+ });
326
+ var LocalCache2;
327
+ var init_local_cache_client = __esm({
328
+ "src/content/local-cache-client.ts"() {
329
+ "use strict";
330
+ LocalCache2 = class {
331
+ constructor(customPath) {
332
+ // Maintain private properties for type parity with the Server version
333
+ this.data = null;
334
+ this.hasLoaded = false;
335
+ this.cachePath = customPath || ".nexus/cache.json";
336
+ }
337
+ /**
338
+ * Always returns false in the browser as local file caching
339
+ * is a server-side only feature.
340
+ */
341
+ isLoaded() {
342
+ return false;
343
+ }
344
+ /**
345
+ * No-op method to maintain internal API compatibility.
346
+ */
347
+ load() {
348
+ return;
349
+ }
350
+ /**
351
+ * Returns null. Browsers should fetch pages from the Remote API
352
+ * or the Memory/Browser cache.
353
+ */
354
+ async getPage(_slug) {
355
+ return null;
356
+ }
357
+ /**
358
+ * Returns null. Browsers should fetch collections from the Remote API.
359
+ */
360
+ async getCollection(_collectionId) {
361
+ return null;
362
+ }
363
+ /**
364
+ * Returns null.
365
+ */
366
+ async getGlobals() {
367
+ return null;
368
+ }
369
+ /**
370
+ * Returns null.
371
+ */
372
+ async getAllData() {
373
+ return null;
374
+ }
375
+ /**
376
+ * Dummy helper to match Node implementation.
377
+ */
378
+ shouldWarnAboutMissingCache(error) {
379
+ const isMissing = error.code === "ENOENT";
380
+ const isMalformed = error instanceof SyntaxError;
381
+ if (process.env.NODE_ENV !== "development") return false;
382
+ return !isMissing || isMalformed;
383
+ }
384
+ };
385
+ }
386
+ });
11
387
 
388
+ // src/index.ts
389
+ var index_exports = {};
390
+ __export(index_exports, {
391
+ AnalyticsEngine: () => AnalyticsEngine,
392
+ BrowserCache: () => BrowserCache,
393
+ CacheTags: () => CacheTags,
394
+ ContentEngine: () => ContentEngine,
395
+ DEFAULT_ANALYTICS_URL: () => DEFAULT_ANALYTICS_URL,
396
+ DEFAULT_API_URL: () => DEFAULT_API_URL,
397
+ LOCAL_NEST_URL: () => LOCAL_NEST_URL,
398
+ LOCAL_RUST_URL: () => LOCAL_RUST_URL,
399
+ LocalCache: () => LocalCacheProxy,
400
+ MemoryCache: () => MemoryCache,
401
+ NexusClient: () => NexusClient,
402
+ VERSION: () => VERSION,
403
+ createNexusClient: () => createNexusClient,
404
+ getEnvConfig: () => getEnvConfig,
405
+ getFullConfig: () => getFullConfig,
406
+ mergeConfigs: () => mergeConfigs,
407
+ nexus: () => nexus,
408
+ validateConfig: () => validateConfig
409
+ });
410
+ module.exports = __toCommonJS(index_exports);
12
411
 
412
+ // src/client.ts
413
+ init_config();
13
414
 
415
+ // src/content/cache-implementations.ts
416
+ var CacheTags = {
417
+ project: (projectId) => `nexus_project_${projectId}`,
418
+ content: (slug) => `content_${slug}`,
419
+ collection: (collectionId) => `collection_${collectionId}`,
420
+ global: "nexus_global_config",
421
+ user: (userId) => `user_${userId}`,
422
+ media: (mediaId) => `media_${mediaId}`
423
+ };
424
+ var MemoryCache = class {
425
+ constructor(options = {}) {
426
+ this.cache = /* @__PURE__ */ new Map();
427
+ this.tagIndex = /* @__PURE__ */ new Map();
428
+ this.stats = {
429
+ size: 0,
430
+ hits: 0,
431
+ misses: 0,
432
+ hitRate: 0,
433
+ evictions: 0
434
+ };
435
+ this.maxSize = options.maxSize || 1e3;
436
+ this.defaultTTL = options.ttl || 5 * 60 * 1e3;
437
+ }
438
+ set(key, data, options = {}) {
439
+ if (this.cache.size >= this.maxSize) {
440
+ this.evictLRU();
441
+ }
442
+ const metadata = {
443
+ timestamp: Date.now(),
444
+ expiresAt: Date.now() + (options.ttl || this.defaultTTL),
445
+ tags: options.tags || [],
446
+ size: this.calculateSize(data)
447
+ };
448
+ this.cache.set(key, { data, metadata });
449
+ this.stats.size = this.cache.size;
450
+ metadata.tags.forEach((tag) => {
451
+ if (!this.tagIndex.has(tag)) {
452
+ this.tagIndex.set(tag, /* @__PURE__ */ new Set());
453
+ }
454
+ this.tagIndex.get(tag).add(key);
455
+ });
456
+ }
457
+ get(key) {
458
+ const entry = this.cache.get(key);
459
+ if (!entry) {
460
+ this.stats.misses++;
461
+ this.updateHitRate();
462
+ return null;
463
+ }
464
+ if (Date.now() > entry.metadata.expiresAt) {
465
+ this.delete(key);
466
+ this.stats.misses++;
467
+ this.updateHitRate();
468
+ return null;
469
+ }
470
+ entry.metadata.timestamp = Date.now();
471
+ this.stats.hits++;
472
+ this.updateHitRate();
473
+ return entry;
474
+ }
475
+ delete(key) {
476
+ const entry = this.cache.get(key);
477
+ if (!entry) return false;
478
+ entry.metadata.tags.forEach((tag) => {
479
+ const keys = this.tagIndex.get(tag);
480
+ if (keys) {
481
+ keys.delete(key);
482
+ if (keys.size === 0) {
483
+ this.tagIndex.delete(tag);
484
+ }
485
+ }
486
+ });
487
+ this.cache.delete(key);
488
+ this.stats.size = this.cache.size;
489
+ return true;
490
+ }
491
+ invalidateByTags(tags) {
492
+ const keysToDelete = /* @__PURE__ */ new Set();
493
+ tags.forEach((tag) => {
494
+ const keys = this.tagIndex.get(tag);
495
+ if (keys) {
496
+ keys.forEach((key) => keysToDelete.add(key));
497
+ this.tagIndex.delete(tag);
498
+ }
499
+ });
500
+ keysToDelete.forEach((key) => this.delete(key));
501
+ if (process.env.NODE_ENV === "development") {
502
+ console.log(
503
+ `[MemoryCache] Invalidated ${keysToDelete.size} entries by tags: ${tags.join(", ")}`
504
+ );
505
+ }
506
+ }
507
+ clear() {
508
+ this.cache.clear();
509
+ this.tagIndex.clear();
510
+ this.stats = {
511
+ size: 0,
512
+ hits: 0,
513
+ misses: 0,
514
+ hitRate: 0,
515
+ evictions: 0
516
+ };
517
+ }
518
+ getStats() {
519
+ return { ...this.stats };
520
+ }
521
+ evictLRU() {
522
+ let oldestKey = null;
523
+ let oldestTime = Infinity;
524
+ for (const [key, entry] of this.cache.entries()) {
525
+ if (entry.metadata.timestamp < oldestTime) {
526
+ oldestTime = entry.metadata.timestamp;
527
+ oldestKey = key;
528
+ }
529
+ }
530
+ if (oldestKey) {
531
+ this.delete(oldestKey);
532
+ this.stats.evictions++;
533
+ }
534
+ }
535
+ calculateSize(data) {
536
+ try {
537
+ const jsonString = JSON.stringify(data);
538
+ return new Blob([jsonString]).size;
539
+ } catch {
540
+ return 0;
541
+ }
542
+ }
543
+ updateHitRate() {
544
+ const total = this.stats.hits + this.stats.misses;
545
+ this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
546
+ }
547
+ };
548
+ var BrowserCache = class {
549
+ constructor(projectId) {
550
+ this.projectId = projectId;
551
+ this.prefix = "nexushub_";
552
+ this.maxSize = 5 * 1024 * 1024;
553
+ // 5MB standard LocalStorage limit
554
+ this.currentSize = 0;
555
+ if (typeof window !== "undefined") {
556
+ this.calculateCurrentSize();
557
+ }
558
+ }
559
+ set(key, data, ttl = 5 * 60 * 1e3) {
560
+ if (typeof window === "undefined") return;
561
+ const storageKey = this.getStorageKey(key);
562
+ const entry = {
563
+ data,
564
+ metadata: {
565
+ timestamp: Date.now(),
566
+ expiresAt: Date.now() + ttl,
567
+ size: this.calculateStorageSize(data)
568
+ }
569
+ };
570
+ const serialized = JSON.stringify(entry);
571
+ const newSize = new Blob([serialized]).size;
572
+ if (this.currentSize + newSize > this.maxSize) {
573
+ this.evictOldest();
574
+ }
575
+ try {
576
+ localStorage.setItem(storageKey, serialized);
577
+ this.currentSize += newSize;
578
+ } catch (error) {
579
+ console.warn("[BrowserCache] Failed to save to localStorage:", error);
580
+ this.evictOldest();
581
+ try {
582
+ localStorage.setItem(storageKey, serialized);
583
+ } catch (e) {
584
+ }
585
+ }
586
+ }
587
+ get(key) {
588
+ if (typeof window === "undefined") return null;
589
+ const storageKey = this.getStorageKey(key);
590
+ const item = localStorage.getItem(storageKey);
591
+ if (!item) return null;
592
+ try {
593
+ const entry = JSON.parse(item);
594
+ if (Date.now() > entry.metadata.expiresAt) {
595
+ this.delete(key);
596
+ return null;
597
+ }
598
+ return entry.data;
599
+ } catch {
600
+ this.delete(key);
601
+ return null;
602
+ }
603
+ }
604
+ delete(key) {
605
+ if (typeof window === "undefined") return;
606
+ const storageKey = this.getStorageKey(key);
607
+ const item = localStorage.getItem(storageKey);
608
+ if (item) {
609
+ this.currentSize -= new Blob([item]).size;
610
+ }
611
+ localStorage.removeItem(storageKey);
612
+ }
613
+ clear() {
614
+ if (typeof window === "undefined") return;
615
+ const keysToRemove = [];
616
+ for (let i = 0; i < localStorage.length; i++) {
617
+ const key = localStorage.key(i);
618
+ if (key?.startsWith(this.prefix)) {
619
+ keysToRemove.push(key);
620
+ }
621
+ }
622
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
623
+ this.currentSize = 0;
624
+ }
625
+ getStorageKey(key) {
626
+ return `${this.prefix}${this.projectId}_${key}`;
627
+ }
628
+ calculateCurrentSize() {
629
+ this.currentSize = 0;
630
+ for (let i = 0; i < localStorage.length; i++) {
631
+ const key = localStorage.key(i);
632
+ if (key?.startsWith(this.prefix)) {
633
+ const item = localStorage.getItem(key);
634
+ if (item) {
635
+ this.currentSize += new Blob([item]).size;
636
+ }
637
+ }
638
+ }
639
+ }
640
+ calculateStorageSize(data) {
641
+ return new Blob([JSON.stringify(data)]).size;
642
+ }
643
+ evictOldest() {
644
+ let oldestKey = null;
645
+ let oldestTime = Infinity;
646
+ for (let i = 0; i < localStorage.length; i++) {
647
+ const key = localStorage.key(i);
648
+ if (key?.startsWith(this.prefix)) {
649
+ const item = localStorage.getItem(key);
650
+ if (item) {
651
+ try {
652
+ const entry = JSON.parse(item);
653
+ if (entry.metadata.timestamp < oldestTime) {
654
+ oldestTime = entry.metadata.timestamp;
655
+ oldestKey = key;
656
+ }
657
+ } catch {
658
+ localStorage.removeItem(key);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ if (oldestKey) {
664
+ const item = localStorage.getItem(oldestKey);
665
+ if (item) {
666
+ this.currentSize -= new Blob([item]).size;
667
+ }
668
+ localStorage.removeItem(oldestKey);
669
+ }
670
+ }
671
+ };
14
672
 
673
+ // src/content/cache.ts
674
+ var LocalCacheProxy = class {
675
+ constructor(cachePath) {
676
+ this.instance = null;
677
+ this.cachePath = cachePath;
678
+ }
679
+ async getInstance() {
680
+ if (this.instance) return this.instance;
681
+ if (typeof window === "undefined") {
682
+ const mod = await Promise.resolve().then(() => (init_local_cache_server(), local_cache_server_exports));
683
+ this.instance = new mod.LocalCache(this.cachePath);
684
+ } else {
685
+ const mod = await Promise.resolve().then(() => (init_local_cache_client(), local_cache_client_exports));
686
+ this.instance = new mod.LocalCache(this.cachePath);
687
+ }
688
+ return this.instance;
689
+ }
690
+ isLoaded() {
691
+ return this.instance?.isLoaded() || false;
692
+ }
693
+ async getPage(slug) {
694
+ const inst = await this.getInstance();
695
+ return inst.getPage(slug);
696
+ }
697
+ async getCollection(id) {
698
+ const inst = await this.getInstance();
699
+ return inst.getCollection(id);
700
+ }
701
+ async getGlobals() {
702
+ const inst = await this.getInstance();
703
+ return inst.getGlobals();
704
+ }
705
+ async getAllData() {
706
+ const inst = await this.getInstance();
707
+ return inst.getAllData();
708
+ }
709
+ };
15
710
 
711
+ // src/content/strategies.ts
712
+ var RateLimiter = class {
713
+ constructor(config) {
714
+ this.requests = [];
715
+ this.config = config;
716
+ }
717
+ async checkLimit() {
718
+ while (true) {
719
+ const now = Date.now();
720
+ const windowStart = now - this.config.timeWindow;
721
+ this.requests = this.requests.filter((time) => time > windowStart);
722
+ if (this.requests.length < this.config.maxRequests) {
723
+ this.requests.push(now);
724
+ return;
725
+ }
726
+ const oldestRequest = this.requests[0];
727
+ const waitTime = oldestRequest + this.config.timeWindow - now;
728
+ if (waitTime > 0) {
729
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
730
+ } else {
731
+ }
732
+ }
733
+ }
734
+ getStats() {
735
+ const now = Date.now();
736
+ const windowStart = now - this.config.timeWindow;
737
+ const currentRequests = this.requests.filter(
738
+ (time) => time > windowStart
739
+ ).length;
740
+ return { currentRequests, limit: this.config.maxRequests };
741
+ }
742
+ };
743
+ var ExponentialBackoff = class {
744
+ constructor(config) {
745
+ this.config = { jitter: true, ...config };
746
+ }
747
+ async execute(fn, onRetry) {
748
+ let lastError = new Error("Unknown error");
749
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
750
+ try {
751
+ return await fn();
752
+ } catch (error) {
753
+ lastError = error;
754
+ if (this.isClientError(error) && !this.isRateLimitError(error)) {
755
+ throw error;
756
+ }
757
+ if (attempt === this.config.maxRetries) break;
758
+ const delay = this.calculateDelay(attempt);
759
+ if (onRetry) onRetry(attempt + 1, delay, error);
760
+ await new Promise((resolve) => setTimeout(resolve, delay));
761
+ }
762
+ }
763
+ throw lastError;
764
+ }
765
+ calculateDelay(attempt) {
766
+ let delay = this.config.baseDelay * Math.pow(2, attempt);
767
+ delay = Math.min(delay, this.config.maxDelay);
768
+ if (this.config.jitter) {
769
+ delay = delay * (0.5 + Math.random());
770
+ }
771
+ return delay;
772
+ }
773
+ isClientError(error) {
774
+ return error?.status >= 400 && error?.status < 500;
775
+ }
776
+ isRateLimitError(error) {
777
+ return error?.status === 429;
778
+ }
779
+ };
780
+ var CircuitBreaker = class {
781
+ constructor() {
782
+ this.state = 0 /* CLOSED */;
783
+ this.failures = 0;
784
+ this.lastFailureTime = 0;
785
+ this.failureThreshold = 5;
786
+ this.resetTimeout = 3e4;
787
+ }
788
+ // 30 seconds
789
+ isOpen() {
790
+ if (this.state === 1 /* OPEN */) {
791
+ if (Date.now() - this.lastFailureTime > this.resetTimeout) {
792
+ this.state = 2 /* HALF_OPEN */;
793
+ return false;
794
+ }
795
+ return true;
796
+ }
797
+ return false;
798
+ }
799
+ recordSuccess() {
800
+ this.failures = 0;
801
+ this.state = 0 /* CLOSED */;
802
+ }
803
+ recordFailure() {
804
+ this.failures++;
805
+ this.lastFailureTime = Date.now();
806
+ if (this.failures >= this.failureThreshold) {
807
+ this.state = 1 /* OPEN */;
808
+ if (process.env.NODE_ENV === "development") {
809
+ console.warn(
810
+ "[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests."
811
+ );
812
+ }
813
+ }
814
+ }
815
+ };
816
+ var RequestBatcher = class {
817
+ constructor(batchWindow = 10, maxBatchSize = 20) {
818
+ this.batchWindow = batchWindow;
819
+ this.maxBatchSize = maxBatchSize;
820
+ this.batch = [];
821
+ this.processing = false;
822
+ }
823
+ async schedule(key, request) {
824
+ return new Promise((resolve, reject) => {
825
+ this.batch.push({ key, resolve, reject });
826
+ if (this.batch.length >= this.maxBatchSize) {
827
+ this.processBatch(request);
828
+ } else if (!this.batchTimeout) {
829
+ this.batchTimeout = setTimeout(
830
+ () => this.processBatch(request),
831
+ this.batchWindow
832
+ );
833
+ }
834
+ });
835
+ }
836
+ async processBatch(request) {
837
+ if (this.processing || this.batch.length === 0) return;
838
+ this.processing = true;
839
+ if (this.batchTimeout) {
840
+ clearTimeout(this.batchTimeout);
841
+ this.batchTimeout = void 0;
842
+ }
843
+ const currentBatch = [...this.batch];
844
+ this.batch = [];
845
+ try {
846
+ const result = await request();
847
+ currentBatch.forEach((item) => item.resolve(result));
848
+ } catch (error) {
849
+ currentBatch.forEach((item) => item.reject(error));
850
+ } finally {
851
+ this.processing = false;
852
+ if (this.batch.length > 0) {
853
+ setTimeout(() => this.processBatch(request), 0);
854
+ }
855
+ }
856
+ }
857
+ };
16
858
 
859
+ // src/content/utils.ts
860
+ function validateSlug(slug) {
861
+ if (!slug || typeof slug !== "string") {
862
+ throw new Error("Slug must be a non-empty string");
863
+ }
864
+ if (!/^[a-z0-9-_]+$/.test(slug)) {
865
+ throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
866
+ }
867
+ }
868
+ function normalizeQuery(query) {
869
+ const normalized = { ...query };
870
+ normalized.page = Math.max(1, normalized.page || 1);
871
+ normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
872
+ normalized.order = normalized.order || "desc";
873
+ if (normalized.page < 1) {
874
+ throw new Error("Page must be greater than 0");
875
+ }
876
+ if (normalized.limit < 1 || normalized.limit > 100) {
877
+ throw new Error("Limit must be between 1 and 100");
878
+ }
879
+ return normalized;
880
+ }
881
+ function buildQueryString(query) {
882
+ const params = new URLSearchParams();
883
+ if (query.page) params.append("page", query.page.toString());
884
+ if (query.limit) params.append("limit", query.limit.toString());
885
+ if (query.sort) params.append("sort", query.sort);
886
+ if (query.order) params.append("order", query.order);
887
+ if (query.search) params.append("search", query.search);
888
+ if (query.include?.length) {
889
+ params.append("include", query.include.join(","));
890
+ }
891
+ if (query.fields?.length) {
892
+ params.append("fields", query.fields.join(","));
893
+ }
894
+ if (query.filter) {
895
+ params.append("filter", JSON.stringify(query.filter));
896
+ }
897
+ return params.toString();
898
+ }
899
+ function measurePerformance(name, fn) {
900
+ const start = performance.now();
901
+ const result = fn();
902
+ const end = performance.now();
903
+ if (process.env.NODE_ENV === "development") {
904
+ console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
905
+ }
906
+ return { result, duration: end - start };
907
+ }
17
908
 
909
+ // src/content/index.ts
910
+ var ContentEngine = class {
911
+ constructor(config) {
912
+ this.config = config;
913
+ this.isServer = typeof window === "undefined";
914
+ this.localCache = new LocalCacheProxy();
915
+ this.memoryCache = new MemoryCache({
916
+ maxSize: 500,
917
+ ttl: 5 * 60 * 1e3
918
+ // 5 minutes
919
+ });
920
+ if (!this.isServer) {
921
+ this.browserCache = new BrowserCache(config.projectId);
922
+ }
923
+ this.rateLimiter = new RateLimiter({
924
+ maxRequests: config.debug ? 100 : 50,
925
+ timeWindow: 6e4
926
+ });
927
+ this.backoff = new ExponentialBackoff({
928
+ maxRetries: config.retries || 3,
929
+ baseDelay: 100,
930
+ maxDelay: 5e3,
931
+ jitter: true
932
+ });
933
+ this.requestBatcher = new RequestBatcher(20, 50);
934
+ this.circuitBreaker = new CircuitBreaker();
935
+ this.defaultRevalidate = config.revalidateTime || 60;
936
+ this.cacheStrategy = config.cacheStrategy || "memory";
937
+ if (!this.isServer) {
938
+ window.addEventListener("beforeunload", this.cleanup.bind(this));
939
+ }
940
+ }
941
+ /**
942
+ * Fetch a Single Page with full strategy pipeline
943
+ */
944
+ async getPage(slug, options = {}) {
945
+ const { result, duration } = measurePerformance(
946
+ `getPage("${slug}")`,
947
+ () => this._getPage(slug, options)
948
+ );
949
+ if (this.config.debug && duration > 100) {
950
+ console.warn(
951
+ `[NexusHub] \u26A0\uFE0F getPage("${slug}") took ${duration.toFixed(2)}ms`
952
+ );
953
+ }
954
+ return result;
955
+ }
956
+ async _getPage(slug, options = {}) {
957
+ validateSlug(slug);
958
+ const {
959
+ revalidate = this.defaultRevalidate,
960
+ tags = [],
961
+ forceRefresh = false,
962
+ includeMetadata = false
963
+ } = options;
964
+ const cacheKey = `page:${slug}`;
965
+ const cached = await this.checkCaches(
966
+ cacheKey,
967
+ forceRefresh,
968
+ includeMetadata
969
+ );
970
+ if (cached) {
971
+ if (this.config.debug) console.log(`[NexusHub] \u26A1 Cache hit: ${slug}`);
972
+ return includeMetadata ? cached : cached.data;
973
+ }
974
+ if (this.circuitBreaker.isOpen()) {
975
+ throw new Error(`[NexusHub] Circuit open. API is unavailable.`);
976
+ }
977
+ try {
978
+ await this.rateLimiter.checkLimit();
979
+ const data = await this.backoff.execute(
980
+ async () => {
981
+ return this.fetchPage(
982
+ slug,
983
+ cacheKey,
984
+ tags,
985
+ revalidate,
986
+ forceRefresh
987
+ );
988
+ },
989
+ (attempt, delay, error) => {
990
+ if (this.config.debug) {
991
+ console.log(
992
+ `[NexusHub] \u{1F504} Retry ${attempt} for '${slug}' after ${delay}ms: ${error.message}`
993
+ );
994
+ }
995
+ }
996
+ );
997
+ this.circuitBreaker.recordSuccess();
998
+ return includeMetadata ? data : data.data;
999
+ } catch (error) {
1000
+ this.circuitBreaker.recordFailure();
1001
+ const staleCache = this.memoryCache.get(cacheKey);
1002
+ if (staleCache && !forceRefresh) {
1003
+ console.warn(`[NexusHub] \u26A0\uFE0F Serving stale content for '${slug}'`);
1004
+ return includeMetadata ? staleCache : staleCache.data;
1005
+ }
1006
+ throw this.normalizeError(error, `Failed to fetch page '${slug}'`);
1007
+ }
1008
+ }
1009
+ /**
1010
+ * Fetch a Collection (Optimized)
1011
+ */
1012
+ async getCollection(collectionId, query = {}, options = {}) {
1013
+ const { result, duration } = measurePerformance(
1014
+ `getCollection("${collectionId}")`,
1015
+ () => this._getCollection(collectionId, query, options)
1016
+ );
1017
+ if (this.config.debug && duration > 100) {
1018
+ console.warn(
1019
+ `[NexusHub] \u26A0\uFE0F getCollection("${collectionId}") took ${duration.toFixed(2)}ms`
1020
+ );
1021
+ }
1022
+ return result;
1023
+ }
1024
+ async _getCollection(collectionId, query = {}, options = {}) {
1025
+ const normalizedQuery = normalizeQuery(query);
1026
+ const {
1027
+ revalidate = this.defaultRevalidate,
1028
+ tags = [],
1029
+ forceRefresh = false,
1030
+ includeMetadata = false
1031
+ } = options;
1032
+ const queryString = buildQueryString(normalizedQuery);
1033
+ const cacheKey = `collection:${collectionId}:${queryString}`;
1034
+ const cached = await this.checkCaches(
1035
+ cacheKey,
1036
+ forceRefresh,
1037
+ includeMetadata
1038
+ );
1039
+ if (cached) {
1040
+ if (this.config.debug) {
1041
+ console.log(
1042
+ `[NexusHub] \u26A1 Served collection '${collectionId}' from memory cache.`
1043
+ );
1044
+ }
1045
+ return includeMetadata ? cached : cached.data;
1046
+ }
1047
+ if (!forceRefresh && process.env.NODE_ENV === "development") {
1048
+ const localCollection = await this.localCache.getCollection(collectionId);
1049
+ if (localCollection) {
1050
+ const result = this.applyLocalQuery(
1051
+ localCollection,
1052
+ normalizedQuery
1053
+ );
1054
+ if (this.config.debug) {
1055
+ console.log(
1056
+ `[NexusHub] \u{1F4C1} Served collection '${collectionId}' from local cache.`
1057
+ );
1058
+ }
1059
+ this.memoryCache.set(cacheKey, result, {
1060
+ tags: [...tags, CacheTags.collection(collectionId)],
1061
+ revalidate
1062
+ });
1063
+ return result;
1064
+ }
1065
+ }
1066
+ if (this.circuitBreaker.isOpen()) throw new Error("Circuit open");
1067
+ try {
1068
+ await this.rateLimiter.checkLimit();
1069
+ const result = await this.backoff.execute(async () => {
1070
+ return this.fetchCollection(
1071
+ collectionId,
1072
+ normalizedQuery,
1073
+ cacheKey,
1074
+ tags,
1075
+ revalidate
1076
+ );
1077
+ });
1078
+ this.circuitBreaker.recordSuccess();
1079
+ return includeMetadata ? result : result.data;
1080
+ } catch (error) {
1081
+ this.circuitBreaker.recordFailure();
1082
+ const staleCache = this.memoryCache.get(cacheKey);
1083
+ if (staleCache && !forceRefresh) {
1084
+ console.warn(
1085
+ `[NexusHub] \u26A0\uFE0F Using stale cache for collection '${collectionId}'`
1086
+ );
1087
+ return includeMetadata ? staleCache : staleCache.data;
1088
+ }
1089
+ throw this.normalizeError(
1090
+ error,
1091
+ `Failed to fetch collection '${collectionId}'`
1092
+ );
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Fetch Global Settings with nested includes support
1097
+ */
1098
+ async getGlobals(options = {}) {
1099
+ const {
1100
+ include = [],
1101
+ revalidate = this.defaultRevalidate,
1102
+ forceRefresh = false
1103
+ } = options;
1104
+ const cacheKey = `globals:${include.join(",")}`;
1105
+ if (!forceRefresh && this.cacheStrategy === "memory") {
1106
+ const cached = this.memoryCache.get(cacheKey);
1107
+ if (cached && this.isCacheValid(cached.metadata)) {
1108
+ if (this.config.debug)
1109
+ console.log("[NexusHub] \u26A1 Served globals from memory cache.");
1110
+ return cached.data;
1111
+ }
1112
+ }
1113
+ if (!forceRefresh && !this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
1114
+ const cached = this.browserCache.get(cacheKey);
1115
+ if (cached) {
1116
+ return cached;
1117
+ }
1118
+ }
1119
+ if (!forceRefresh && process.env.NODE_ENV === "development") {
1120
+ const localGlobals = this.localCache.getGlobals();
1121
+ if (localGlobals) {
1122
+ this.memoryCache.set(cacheKey, localGlobals, {
1123
+ tags: [CacheTags.global],
1124
+ revalidate
1125
+ });
1126
+ return localGlobals;
1127
+ }
1128
+ }
1129
+ try {
1130
+ await this.rateLimiter.checkLimit();
1131
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/globals`;
1132
+ const params = include.length > 0 ? `?include=${include.join(",")}` : "";
1133
+ const res = await this.fetchWithTimeout(`${url}${params}`, {
1134
+ method: "GET",
1135
+ headers: this.getHeaders()
1136
+ });
1137
+ if (!res.ok) {
1138
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
1139
+ }
1140
+ const json = await res.json();
1141
+ const data = json.data || json;
1142
+ this.writeCache(cacheKey, data, {
1143
+ revalidate,
1144
+ tags: [CacheTags.project(this.config.projectId), CacheTags.global]
1145
+ });
1146
+ return data;
1147
+ } catch (error) {
1148
+ const staleCache = this.memoryCache.get(cacheKey);
1149
+ if (staleCache && !forceRefresh) {
1150
+ console.warn("[NexusHub] \u26A0\uFE0F Using stale cache for globals");
1151
+ return staleCache.data;
1152
+ }
1153
+ throw this.normalizeError(error, "Failed to fetch globals");
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Get a single item from a collection (Uses Request Batching)
1158
+ * If you call this 10 times in a loop, it sends 1 HTTP request.
1159
+ */
1160
+ async getItem(collectionId, itemId, options = {}) {
1161
+ const cacheKey = `item:${collectionId}:${itemId}`;
1162
+ if (this.cacheStrategy === "memory") {
1163
+ const cached = this.memoryCache.get(cacheKey);
1164
+ if (cached && this.isCacheValid(cached.metadata)) {
1165
+ if (this.config.debug) {
1166
+ console.log(`[NexusHub] \u26A1 Served item '${itemId}' from cache.`);
1167
+ }
1168
+ return cached.data;
1169
+ }
1170
+ }
1171
+ return this.requestBatcher.schedule(cacheKey, async () => {
1172
+ const params = new URLSearchParams();
1173
+ if (options.include?.length) {
1174
+ params.append("include", options.include.join(","));
1175
+ }
1176
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}/${itemId}?${params}`;
1177
+ const res = await this.fetchWithTimeout(url, {
1178
+ method: "GET",
1179
+ headers: this.getHeaders()
1180
+ });
1181
+ if (!res.ok) {
1182
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
1183
+ }
1184
+ const json = await res.json();
1185
+ const data = json.data || json;
1186
+ this.writeCache(cacheKey, data, {
1187
+ revalidate: options.revalidate || this.defaultRevalidate,
1188
+ tags: [
1189
+ CacheTags.project(this.config.projectId),
1190
+ CacheTags.collection(collectionId),
1191
+ `item_${itemId}`,
1192
+ ...options.tags || []
1193
+ ]
1194
+ });
1195
+ return data;
1196
+ });
1197
+ }
1198
+ /**
1199
+ * Search across collections
1200
+ */
1201
+ async search(query, options = {}) {
1202
+ const params = new URLSearchParams({
1203
+ q: query,
1204
+ limit: (options.limit || 20).toString()
1205
+ });
1206
+ if (options.collections?.length) {
1207
+ params.append("collections", options.collections.join(","));
1208
+ }
1209
+ if (options.fields?.length) {
1210
+ params.append("fields", options.fields.join(","));
1211
+ }
1212
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/search?${params}`;
1213
+ const res = await this.fetchWithTimeout(url, {
1214
+ method: "GET",
1215
+ headers: this.getHeaders()
1216
+ });
1217
+ if (!res.ok) {
1218
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
1219
+ }
1220
+ return await res.json();
1221
+ }
1222
+ /**
1223
+ * Prefetch content for better performance
1224
+ */
1225
+ async prefetch(urls) {
1226
+ if (typeof window !== "undefined" && "requestIdleCallback" in window) {
1227
+ requestIdleCallback(async () => {
1228
+ await Promise.allSettled(
1229
+ urls.map((url) => fetch(url, { priority: "low" }))
1230
+ );
1231
+ });
1232
+ }
1233
+ }
1234
+ /**
1235
+ * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
1236
+ */
1237
+ subscribeToUpdates(callback) {
1238
+ if (this.isServer || typeof EventSource === "undefined") {
1239
+ console.warn("[NexusHub] EventSource not supported in this environment");
1240
+ return () => {
1241
+ };
1242
+ }
1243
+ let eventSource = null;
1244
+ let retryCount = 0;
1245
+ let isClosed = false;
1246
+ const connect = () => {
1247
+ if (isClosed) return;
1248
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey}`;
1249
+ eventSource = new EventSource(url);
1250
+ eventSource.onopen = () => {
1251
+ retryCount = 0;
1252
+ if (this.config.debug)
1253
+ console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
1254
+ };
1255
+ eventSource.onmessage = (event) => {
1256
+ try {
1257
+ const data = JSON.parse(event.data);
1258
+ if (data.type === "content.updated" && data.slug) {
1259
+ this.invalidateCache([CacheTags.content(data.slug)]);
1260
+ }
1261
+ if (data.type === "collection.updated") {
1262
+ this.invalidateCache([CacheTags.collection(data.collectionId)]);
1263
+ }
1264
+ callback(data);
1265
+ } catch (e) {
1266
+ console.error("[NexusHub] SSE Parse Error", e);
1267
+ }
1268
+ };
1269
+ eventSource.onerror = () => {
1270
+ eventSource?.close();
1271
+ if (isClosed) return;
1272
+ const timeout = Math.min(1e3 * Math.pow(2, retryCount), 1e4);
1273
+ retryCount++;
1274
+ console.warn(
1275
+ `[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
1276
+ );
1277
+ setTimeout(connect, timeout);
1278
+ };
1279
+ };
1280
+ connect();
1281
+ return () => {
1282
+ isClosed = true;
1283
+ eventSource?.close();
1284
+ };
1285
+ }
1286
+ // --- CACHE MANAGEMENT ---
1287
+ /**
1288
+ * Check all caches in order of speed
1289
+ */
1290
+ async checkCaches(key, forceRefresh, includeMetadata) {
1291
+ if (forceRefresh) return null;
1292
+ if (this.cacheStrategy === "memory") {
1293
+ const cached = this.memoryCache.get(key);
1294
+ if (cached && this.isCacheValid(cached.metadata)) {
1295
+ return cached;
1296
+ }
1297
+ }
1298
+ if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
1299
+ const cached = this.browserCache.get(key);
1300
+ if (cached) {
1301
+ return {
1302
+ data: cached,
1303
+ metadata: {
1304
+ timestamp: 0,
1305
+ expiresAt: Infinity,
1306
+ tags: [],
1307
+ etag: void 0
1308
+ }
1309
+ };
1310
+ }
1311
+ }
1312
+ if (process.env.NODE_ENV === "development") {
1313
+ if (key.startsWith("page:")) {
1314
+ const slug = key.split(":")[1];
1315
+ const local = await this.localCache.getPage(slug);
1316
+ if (local) {
1317
+ return {
1318
+ data: local,
1319
+ metadata: {
1320
+ timestamp: 0,
1321
+ expiresAt: Infinity,
1322
+ tags: [],
1323
+ etag: void 0
1324
+ }
1325
+ };
1326
+ }
1327
+ }
1328
+ }
1329
+ return null;
1330
+ }
1331
+ writeCache(key, data, options) {
1332
+ if (this.cacheStrategy === "memory") {
1333
+ this.memoryCache.set(key, data, {
1334
+ tags: options.tags,
1335
+ revalidate: options.revalidate
1336
+ });
1337
+ }
1338
+ if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
1339
+ this.browserCache.set(key, data, options.revalidate * 1e3);
1340
+ }
1341
+ }
1342
+ // packages/client-sdk/src/content/index.ts
1343
+ /**
1344
+ * ⚡ THE PULSE: Next.js Cache Revalidation Receiver
1345
+ * Used in app/api/nexus-revalidate/route.ts to handle instant updates from the NexusHub backend.
1346
+ *
1347
+ * @param req The incoming Request object from Next.js
1348
+ * @param revalidateFn The revalidateTag function imported from 'next/cache'
1349
+ */
1350
+ async handleRevalidation(req, revalidateFn) {
1351
+ try {
1352
+ if (!req.body) {
1353
+ return { success: false, message: "Missing Pulse Payload" };
1354
+ }
1355
+ const body = await req.json();
1356
+ const { secret, tag } = body;
1357
+ if (!secret || secret !== this.config.projectId) {
1358
+ if (this.config.debug) {
1359
+ console.warn(`[NexusHub] \u26A0\uFE0F Blocked unauthorized Pulse request.`);
1360
+ }
1361
+ return { success: false, message: "Invalid Pulse Signature" };
1362
+ }
1363
+ const targetTag = tag || "content_all";
1364
+ revalidateFn(targetTag);
1365
+ this.invalidateCache([targetTag]);
1366
+ if (this.config.debug) {
1367
+ console.log(
1368
+ `[NexusHub] \u26A1 Pulse Received: Successfully revalidated tag [${targetTag}]`
1369
+ );
1370
+ }
1371
+ return { success: true, now: Date.now() };
1372
+ } catch (e) {
1373
+ if (this.config.debug) {
1374
+ console.error(`[NexusHub] \u274C Pulse Processing Error:`, e.message);
1375
+ }
1376
+ return {
1377
+ success: false,
1378
+ message: "Malformed Pulse Payload or Processing Error"
1379
+ };
1380
+ }
1381
+ }
1382
+ /**
1383
+ * Invalidate cache by tags
1384
+ */
1385
+ invalidateCache(tags) {
1386
+ this.memoryCache.invalidateByTags(tags);
1387
+ if (this.config.debug) {
1388
+ console.log(
1389
+ `[NexusHub] \u267B\uFE0F Invalidated cache for tags: ${tags.join(", ")}`
1390
+ );
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Clear all caches
1395
+ */
1396
+ clearCache() {
1397
+ this.memoryCache.clear();
1398
+ if (this.browserCache) {
1399
+ this.browserCache.clear();
1400
+ }
1401
+ if (this.config.debug) {
1402
+ console.log("[NexusHub] \u{1F9F9} Cleared all caches");
1403
+ }
1404
+ }
1405
+ /**
1406
+ * Get cache statistics
1407
+ */
1408
+ getCacheStats() {
1409
+ const stats = {
1410
+ memory: this.memoryCache.getStats(),
1411
+ local: { loaded: this.localCache.isLoaded() }
1412
+ };
1413
+ if (this.browserCache) {
1414
+ stats.browser = { size: 0 };
1415
+ }
1416
+ return stats;
1417
+ }
1418
+ // --- REQUEST METHODS ---
1419
+ async fetchPage(slug, cacheKey, tags, revalidate, forceRefresh) {
1420
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/page/${slug}`;
1421
+ if (this.config.debug) {
1422
+ console.log(`[NexusHub] \u{1F310} Fetching: ${url}`);
1423
+ }
1424
+ const res = await this.fetchWithTimeout(url, {
1425
+ method: "GET",
1426
+ headers: this.getHeaders(),
1427
+ tags,
1428
+ revalidate
1429
+ });
1430
+ if (!res.ok) {
1431
+ if (res.status === 404) {
1432
+ throw new Error(
1433
+ `Page '${slug}' not found. Check your Dashboard or Seed data.`
1434
+ );
1435
+ }
1436
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
1437
+ }
1438
+ const json = await res.json();
1439
+ const etag = res.headers.get("etag");
1440
+ const cacheEntry = {
1441
+ data: json.data,
1442
+ metadata: {
1443
+ timestamp: Date.now(),
1444
+ etag: etag || void 0,
1445
+ expiresAt: Date.now() + revalidate * 1e3,
1446
+ tags: [...tags, CacheTags.content(slug)]
1447
+ }
1448
+ };
1449
+ this.writeCache(cacheKey, cacheEntry.data, {
1450
+ revalidate,
1451
+ tags: [
1452
+ CacheTags.project(this.config.projectId),
1453
+ CacheTags.content(slug),
1454
+ ...tags
1455
+ ]
1456
+ });
1457
+ return cacheEntry;
1458
+ }
1459
+ async fetchCollection(collectionId, query, cacheKey, tags, revalidate) {
1460
+ const params = buildQueryString(query);
1461
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}?${params}`;
1462
+ if (this.config.debug) {
1463
+ console.log(`[NexusHub] \u{1F310} Fetching Collection: ${url}`);
1464
+ }
1465
+ const res = await this.fetchWithTimeout(url, {
1466
+ method: "GET",
1467
+ headers: this.getHeaders(),
1468
+ tags,
1469
+ revalidate
1470
+ });
1471
+ if (!res.ok) {
1472
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
1473
+ }
1474
+ const json = await res.json();
1475
+ const etag = res.headers.get("etag");
1476
+ const cacheEntry = {
1477
+ data: json,
1478
+ metadata: {
1479
+ timestamp: Date.now(),
1480
+ etag: etag || void 0,
1481
+ expiresAt: Date.now() + revalidate * 1e3,
1482
+ tags: [...tags, CacheTags.collection(collectionId)]
1483
+ }
1484
+ };
1485
+ this.writeCache(cacheKey, cacheEntry.data, {
1486
+ revalidate,
1487
+ tags: [
1488
+ CacheTags.project(this.config.projectId),
1489
+ CacheTags.collection(collectionId),
1490
+ ...tags
1491
+ ]
1492
+ });
1493
+ return cacheEntry;
1494
+ }
1495
+ // --- HELPER METHODS ---
1496
+ applyLocalQuery(items, query) {
1497
+ let filtered = [...items];
1498
+ if (query.search) {
1499
+ const searchLower = query.search.toLowerCase();
1500
+ filtered = filtered.filter(
1501
+ (item) => JSON.stringify(item).toLowerCase().includes(searchLower)
1502
+ );
1503
+ }
1504
+ if (query.filter) {
1505
+ filtered = filtered.filter((item) => {
1506
+ return Object.entries(query.filter).every(([key, value]) => {
1507
+ const itemValue = item[key];
1508
+ if (itemValue === void 0) return false;
1509
+ if (Array.isArray(value)) {
1510
+ return value.includes(itemValue);
1511
+ }
1512
+ return itemValue === value;
1513
+ });
1514
+ });
1515
+ }
1516
+ if (query.sort) {
1517
+ filtered.sort((a, b) => {
1518
+ const aVal = a[query.sort];
1519
+ const bVal = b[query.sort];
1520
+ const order = query.order === "asc" ? 1 : -1;
1521
+ if (aVal < bVal) return -1 * order;
1522
+ if (aVal > bVal) return 1 * order;
1523
+ return 0;
1524
+ });
1525
+ }
1526
+ const page = query.page || 1;
1527
+ const limit = query.limit || 10;
1528
+ const start = (page - 1) * limit;
1529
+ const end = start + limit;
1530
+ const total = filtered.length;
1531
+ return {
1532
+ items: filtered.slice(start, end),
1533
+ total,
1534
+ page,
1535
+ limit,
1536
+ totalPages: Math.ceil(total / limit),
1537
+ hasNext: end < total,
1538
+ hasPrev: start > 0
1539
+ };
1540
+ }
1541
+ async fetchWithTimeout(url, options = {}) {
1542
+ const {
1543
+ timeout = this.config.timeout || 1e4,
1544
+ tags = [],
1545
+ revalidate,
1546
+ ...fetchOptions
1547
+ } = options;
1548
+ const controller = new AbortController();
1549
+ const id = setTimeout(() => controller.abort(), timeout);
1550
+ const nextConfig = this.isServer ? { next: { tags, revalidate } } : {};
1551
+ try {
1552
+ const response = await fetch(url, {
1553
+ ...fetchOptions,
1554
+ ...nextConfig,
1555
+ // Inject Next.js tags
1556
+ signal: controller.signal
1557
+ });
1558
+ clearTimeout(id);
1559
+ return response;
1560
+ } catch (error) {
1561
+ clearTimeout(id);
1562
+ throw error;
1563
+ }
1564
+ }
1565
+ getHeaders() {
1566
+ const headers = {
1567
+ "Content-Type": "application/json",
1568
+ "X-Nexus-Client": "client-sdk/1.0.0",
1569
+ "X-Nexus-Project": this.config.projectId
1570
+ };
1571
+ if (this.config.apiKey) {
1572
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
1573
+ }
1574
+ return headers;
1575
+ }
1576
+ isCacheValid(metadata) {
1577
+ return Date.now() < metadata.expiresAt;
1578
+ }
1579
+ normalizeError(error, context) {
1580
+ if (error instanceof Error) {
1581
+ if (error.name === "AbortError") {
1582
+ return new Error(`${context}: Request timeout`);
1583
+ }
1584
+ return error;
1585
+ }
1586
+ return new Error(`${context}: ${String(error)}`);
1587
+ }
1588
+ /**
1589
+ * Cancel ongoing requests
1590
+ */
1591
+ cancelRequests() {
1592
+ if (this.abortController) {
1593
+ this.abortController.abort();
1594
+ this.abortController = new AbortController();
1595
+ }
1596
+ }
1597
+ /**
1598
+ * Cleanup resources
1599
+ */
1600
+ cleanup() {
1601
+ this.cancelRequests();
1602
+ if (!this.isServer) {
1603
+ window.removeEventListener("beforeunload", this.cleanup.bind(this));
1604
+ }
1605
+ }
1606
+ };
18
1607
 
1608
+ // src/analytics/fingerprint.ts
1609
+ var cachedEntropy = null;
1610
+ var getDeviceEntropy = async () => {
1611
+ if (cachedEntropy) return cachedEntropy;
1612
+ if (typeof window === "undefined") return {};
1613
+ const nav = window.navigator;
1614
+ cachedEntropy = {
1615
+ screen_resolution: `${window.screen.width}x${window.screen.height}`,
1616
+ color_depth: window.screen.colorDepth,
1617
+ pixel_ratio: window.devicePixelRatio || 1,
1618
+ hardware_concurrency: nav.hardwareConcurrency,
1619
+ device_memory: nav.deviceMemory,
1620
+ // Chrome/Edge only
1621
+ timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
1622
+ platform: nav.platform,
1623
+ language: nav.language,
1624
+ touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
1625
+ canvas_hash: await generateCanvasHash()
1626
+ };
1627
+ return cachedEntropy;
1628
+ };
1629
+ var generateCanvasHash = async () => {
1630
+ try {
1631
+ const canvas = document.createElement("canvas");
1632
+ const ctx = canvas.getContext("2d");
1633
+ if (!ctx) return "";
1634
+ canvas.width = 200;
1635
+ canvas.height = 50;
1636
+ ctx.textBaseline = "top";
1637
+ ctx.font = '16px "Arial"';
1638
+ ctx.textBaseline = "alphabetic";
1639
+ ctx.fillStyle = "#f60";
1640
+ ctx.fillRect(125, 1, 62, 20);
1641
+ ctx.fillStyle = "#069";
1642
+ ctx.fillText("NexusHub Rocks! <canvas> 1.0", 2, 15);
1643
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
1644
+ ctx.fillText("NexusHub Rocks! <canvas> 1.0", 4, 17);
1645
+ const dataUrl = canvas.toDataURL();
1646
+ let hash = 0;
1647
+ for (let i = 0; i < dataUrl.length; i++) {
1648
+ const char = dataUrl.charCodeAt(i);
1649
+ hash = (hash << 5) - hash + char;
1650
+ hash = hash & hash;
1651
+ }
1652
+ return hash.toString(16);
1653
+ } catch {
1654
+ return "";
1655
+ }
1656
+ };
1657
+ var getVisitorId = async () => {
1658
+ if (typeof window === "undefined") return "server_visitor";
1659
+ const STORAGE_KEY = "nexus_vid";
1660
+ let vid = localStorage.getItem(STORAGE_KEY);
1661
+ if (!vid) {
1662
+ const entropy = await getDeviceEntropy();
1663
+ const random = Math.random().toString(36).substring(2, 15);
1664
+ const timestamp = Date.now().toString(36);
1665
+ const fingerprint = [
1666
+ entropy.screen_resolution,
1667
+ entropy.hardware_concurrency,
1668
+ entropy.timezone_offset,
1669
+ entropy.platform,
1670
+ entropy.canvas_hash
1671
+ ].join("|");
1672
+ let hash = 0;
1673
+ for (let i = 0; i < fingerprint.length; i++) {
1674
+ hash = (hash << 5) - hash + fingerprint.charCodeAt(i);
1675
+ hash |= 0;
1676
+ }
1677
+ vid = `vis_${hash.toString(16)}_${timestamp}_${random}`;
1678
+ localStorage.setItem(STORAGE_KEY, vid);
1679
+ }
1680
+ return vid;
1681
+ };
19
1682
 
1683
+ // src/analytics/vitals.ts
1684
+ var import_web_vitals = require("web-vitals");
1685
+ var METRIC_KEY_MAP = {
1686
+ CLS: "cls",
1687
+ LCP: "lcp",
1688
+ INP: "inp",
1689
+ TTFB: "ttfb",
1690
+ FCP: "fcp"
1691
+ };
1692
+ var VitalsCollector = class {
1693
+ constructor() {
1694
+ this.metrics = {};
1695
+ this.hasUpdates = false;
1696
+ if (typeof window !== "undefined") {
1697
+ this.init();
1698
+ }
1699
+ }
1700
+ init() {
1701
+ const recordMetric = (metric) => {
1702
+ const key = METRIC_KEY_MAP[metric.name];
1703
+ this.metrics[key] = metric.value;
1704
+ this.hasUpdates = true;
1705
+ };
1706
+ (0, import_web_vitals.onCLS)(recordMetric);
1707
+ (0, import_web_vitals.onLCP)(recordMetric);
1708
+ (0, import_web_vitals.onTTFB)(recordMetric);
1709
+ (0, import_web_vitals.onINP)(recordMetric);
1710
+ (0, import_web_vitals.onFCP)(recordMetric);
1711
+ const navEntry = performance.getEntriesByType("navigation")[0];
1712
+ if (navEntry) {
1713
+ this.metrics.navigation_timing = {
1714
+ dns_lookup: navEntry.domainLookupEnd - navEntry.domainLookupStart,
1715
+ tcp_connect: navEntry.connectEnd - navEntry.connectStart,
1716
+ request_time: navEntry.responseEnd - navEntry.requestStart,
1717
+ dom_load: navEntry.domComplete - navEntry.domInteractive,
1718
+ // NEW: raw fields matching Rust NavigationTiming struct
1719
+ domain_lookup_start: navEntry.domainLookupStart,
1720
+ domain_lookup_end: navEntry.domainLookupEnd,
1721
+ connect_start: navEntry.connectStart,
1722
+ connect_end: navEntry.connectEnd,
1723
+ secure_connection_start: navEntry.secureConnectionStart > 0 ? navEntry.secureConnectionStart : void 0
1724
+ };
1725
+ this.hasUpdates = true;
1726
+ }
1727
+ const resourceEntries = performance.getEntriesByType(
1728
+ "resource"
1729
+ );
1730
+ if (resourceEntries.length > 0) {
1731
+ this.metrics.resources = resourceEntries.filter((r) => !r.name.includes("/api/collect")).sort((a, b) => b.duration - a.duration).slice(0, 20).map((r) => ({
1732
+ name: r.name,
1733
+ initiatorType: r.initiatorType,
1734
+ duration: Math.round(r.duration),
1735
+ transferSize: r.transferSize > 0 ? r.transferSize : void 0,
1736
+ encodedBodySize: r.encodedBodySize > 0 ? r.encodedBodySize : void 0,
1737
+ decodedBodySize: r.decodedBodySize > 0 ? r.decodedBodySize : void 0,
1738
+ startTime: Math.round(r.startTime),
1739
+ responseEnd: Math.round(r.responseEnd)
1740
+ }));
1741
+ this.hasUpdates = true;
1742
+ }
1743
+ const mem = performance.memory;
1744
+ if (mem) {
1745
+ this.metrics.memory_usage = {
1746
+ used_js_heap_size: mem.usedJSHeapSize,
1747
+ total_js_heap_size: mem.totalJSHeapSize,
1748
+ js_heap_size_limit: mem.jsHeapSizeLimit
1749
+ };
1750
+ this.hasUpdates = true;
1751
+ }
1752
+ }
1753
+ getMetricsSnapshot() {
1754
+ if (!this.hasUpdates) return null;
1755
+ this.hasUpdates = false;
1756
+ const snapshot = { ...this.metrics };
1757
+ this.metrics.resources = void 0;
1758
+ return snapshot;
1759
+ }
1760
+ };
1761
+ var vitalsCollector = new VitalsCollector();
1762
+ var initVitals = (_tracker) => {
1763
+ if (process.env.NODE_ENV === "development") {
1764
+ console.log("[NexusHub] Web Vitals monitoring active");
1765
+ }
1766
+ };
20
1767
 
1768
+ // src/analytics/storage.ts
1769
+ var DB_NAME = "NexusHub_Analytics";
1770
+ var STORE_NAME = "events_queue";
1771
+ var DB_VERSION = 2;
1772
+ var MAX_QUEUE_SIZE = 500;
1773
+ var EventStorage = class {
1774
+ constructor() {
1775
+ this.db = null;
1776
+ this.isReady = this.init();
1777
+ }
1778
+ init() {
1779
+ if (typeof window === "undefined" || !window.indexedDB) {
1780
+ return Promise.resolve();
1781
+ }
1782
+ return new Promise((resolve) => {
1783
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
1784
+ request.onerror = () => {
1785
+ console.warn(
1786
+ "[NexusHub] Failed to open IndexedDB. Falling back to memory."
1787
+ );
1788
+ resolve();
1789
+ };
1790
+ request.onsuccess = (event) => {
1791
+ this.db = event.target.result;
1792
+ resolve();
1793
+ };
1794
+ request.onupgradeneeded = (event) => {
1795
+ const db = event.target.result;
1796
+ if (db.objectStoreNames.contains(STORE_NAME)) {
1797
+ db.deleteObjectStore(STORE_NAME);
1798
+ }
1799
+ db.createObjectStore(STORE_NAME, {
1800
+ keyPath: "id",
1801
+ autoIncrement: true
1802
+ });
1803
+ };
1804
+ });
1805
+ }
1806
+ /**
1807
+ * Add an event to the persistent queue.
1808
+ * Evicts the oldest entry if the queue is already at MAX_QUEUE_SIZE.
1809
+ */
1810
+ async enqueue(payload) {
1811
+ await this.isReady;
1812
+ if (!this.db) return;
1813
+ const currentCount = await this.count();
1814
+ if (currentCount >= MAX_QUEUE_SIZE) {
1815
+ const oldest = await this.peek(1);
1816
+ if (oldest.length > 0 && oldest[0].id !== void 0) {
1817
+ await this.remove([oldest[0].id]);
1818
+ }
1819
+ }
1820
+ return new Promise((resolve, reject) => {
1821
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
1822
+ const store = transaction.objectStore(STORE_NAME);
1823
+ const request = store.add({
1824
+ payload,
1825
+ timestamp: Date.now(),
1826
+ retryCount: 0
1827
+ });
1828
+ request.onsuccess = () => resolve();
1829
+ request.onerror = () => reject(request.error);
1830
+ });
1831
+ }
1832
+ /**
1833
+ * Get a batch of the oldest events without removing them.
1834
+ */
1835
+ async peek(limit = 20) {
1836
+ await this.isReady;
1837
+ if (!this.db) return [];
1838
+ return new Promise((resolve) => {
1839
+ const transaction = this.db.transaction([STORE_NAME], "readonly");
1840
+ const store = transaction.objectStore(STORE_NAME);
1841
+ const request = store.getAll(null, limit);
1842
+ request.onsuccess = () => resolve(request.result);
1843
+ request.onerror = () => resolve([]);
1844
+ });
1845
+ }
1846
+ /**
1847
+ * Remove events by ID after a successful upload.
1848
+ */
1849
+ async remove(ids) {
1850
+ await this.isReady;
1851
+ if (!this.db || ids.length === 0) return;
1852
+ return new Promise((resolve, reject) => {
1853
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
1854
+ const store = transaction.objectStore(STORE_NAME);
1855
+ transaction.oncomplete = () => resolve();
1856
+ transaction.onerror = () => reject(transaction.error);
1857
+ transaction.onabort = () => reject(new Error("Delete transaction aborted"));
1858
+ ids.forEach((id) => {
1859
+ store.delete(id);
1860
+ });
1861
+ });
1862
+ }
1863
+ /**
1864
+ * Count pending events in the queue.
1865
+ */
1866
+ async count() {
1867
+ await this.isReady;
1868
+ if (!this.db) return 0;
1869
+ return new Promise((resolve) => {
1870
+ const transaction = this.db.transaction([STORE_NAME], "readonly");
1871
+ const store = transaction.objectStore(STORE_NAME);
1872
+ const request = store.count();
1873
+ request.onsuccess = () => resolve(request.result);
1874
+ request.onerror = () => resolve(0);
1875
+ });
1876
+ }
1877
+ };
1878
+ var eventStorage = new EventStorage();
21
1879
 
1880
+ // src/analytics/tracker.ts
1881
+ var SDK_VERSION = "0.1.0";
1882
+ var safeGetItem = (key) => {
1883
+ try {
1884
+ return localStorage.getItem(key);
1885
+ } catch {
1886
+ return null;
1887
+ }
1888
+ };
1889
+ var safeSetItem = (key, value) => {
1890
+ try {
1891
+ localStorage.setItem(key, value);
1892
+ } catch {
1893
+ }
1894
+ };
1895
+ var safeRemoveItem = (key) => {
1896
+ try {
1897
+ localStorage.removeItem(key);
1898
+ } catch {
1899
+ }
1900
+ };
1901
+ var generateUUID = () => {
1902
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1903
+ return crypto.randomUUID();
1904
+ }
1905
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1906
+ const r = Math.random() * 16 | 0;
1907
+ const v = c === "x" ? r : r & 3 | 8;
1908
+ return v.toString(16);
1909
+ });
1910
+ };
1911
+ var Tracker = class {
1912
+ constructor(config) {
1913
+ this.sessionId = "";
1914
+ this.visitorId = "";
1915
+ this.anonymousId = "";
1916
+ this.isFlushing = false;
1917
+ this.config = config;
1918
+ this.endpoint = `${this.config.analyticsUrl}/api/collect`;
1919
+ this.circuitBreaker = new CircuitBreaker();
1920
+ this.sessionStart = Date.now();
1921
+ if (typeof window !== "undefined") {
1922
+ this.initSession();
1923
+ this.startFlushing();
1924
+ }
1925
+ }
1926
+ async initSession() {
1927
+ this.visitorId = await getVisitorId();
1928
+ let anonId = safeGetItem("nexus_anon_id");
1929
+ if (!anonId) {
1930
+ anonId = `anon_${generateUUID().replace(/-/g, "")}`;
1931
+ safeSetItem("nexus_anon_id", anonId);
1932
+ }
1933
+ this.anonymousId = anonId;
1934
+ let sid = safeGetItem("nexus_sid");
1935
+ const lastActivity = safeGetItem("nexus_last_active");
1936
+ const now = Date.now();
1937
+ const SESSION_TIMEOUT = 30 * 60 * 1e3;
1938
+ const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
1939
+ if (isExpired) {
1940
+ const uuid = generateUUID().replace(/-/g, "").substring(0, 16);
1941
+ sid = `sess_${uuid}_${now}`;
1942
+ safeSetItem("nexus_sid", sid);
1943
+ this.sessionStart = now;
1944
+ }
1945
+ safeSetItem("nexus_last_active", now.toString());
1946
+ this.sessionId = sid;
1947
+ }
1948
+ async send(eventType, data = {}, eventName, ecommerce) {
1949
+ if (typeof window === "undefined") return;
1950
+ safeSetItem("nexus_last_active", Date.now().toString());
1951
+ const entropy = await getDeviceEntropy();
1952
+ const perfMetrics = vitalsCollector.getMetricsSnapshot();
1953
+ const utmParams = extractUtmParams(window.location.href);
1954
+ const payload = {
1955
+ projectId: this.config.projectId,
1956
+ sessionId: this.sessionId,
1957
+ visitorId: this.visitorId,
1958
+ anonymousId: this.anonymousId,
1959
+ messageId: generateUUID(),
1960
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
1961
+ version: SDK_VERSION,
1962
+ url: window.location.href,
1963
+ referrer: document.referrer,
1964
+ userAgent: window.navigator.userAgent,
1965
+ screenWidth: window.screen.width,
1966
+ screenHeight: window.screen.height,
1967
+ language: window.navigator.language,
1968
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1969
+ eventType,
1970
+ eventName,
1971
+ eventData: data,
1972
+ ecommerce: ecommerce ?? void 0,
1973
+ performance: perfMetrics || void 0,
1974
+ context: {
1975
+ device: {
1976
+ hardwareConcurrency: entropy.hardware_concurrency,
1977
+ deviceMemory: entropy.device_memory,
1978
+ pixelRatio: entropy.pixel_ratio,
1979
+ canvasFingerprint: entropy.canvas_hash,
1980
+ platform: entropy.platform
1981
+ },
1982
+ visitorIdLocal: this.visitorId,
1983
+ utm: utmParams
1984
+ },
1985
+ clientTimestamp: (/* @__PURE__ */ new Date()).toISOString()
1986
+ };
1987
+ await eventStorage.enqueue(payload);
1988
+ const count = await eventStorage.count();
1989
+ if (count >= 10 || eventType === "purchase" || eventType === "identify") {
1990
+ this.flushQueue();
1991
+ }
1992
+ }
1993
+ startFlushing() {
1994
+ this.flushInterval = setInterval(() => this.flushQueue(), 5e3);
1995
+ window.addEventListener("visibilitychange", () => {
1996
+ if (document.visibilityState === "hidden") this.flushQueue(true);
1997
+ });
1998
+ }
1999
+ async flushQueue(useBeacon = false) {
2000
+ if (this.isFlushing) return;
2001
+ if (this.circuitBreaker.isOpen()) return;
2002
+ this.isFlushing = true;
2003
+ try {
2004
+ const storedEvents = await eventStorage.peek(20);
2005
+ if (storedEvents.length === 0) {
2006
+ this.isFlushing = false;
2007
+ return;
2008
+ }
2009
+ const payloads = storedEvents.map((e) => e.payload);
2010
+ const promises = payloads.map(
2011
+ (event) => fetch(this.endpoint, {
2012
+ method: "POST",
2013
+ headers: {
2014
+ "Content-Type": "application/json",
2015
+ Authorization: `Bearer ${this.config.apiKey}`
2016
+ },
2017
+ body: JSON.stringify(event),
2018
+ keepalive: useBeacon
2019
+ })
2020
+ );
2021
+ const results = await Promise.allSettled(promises);
2022
+ const successIds = [];
2023
+ let failureCount = 0;
2024
+ results.forEach((res, index) => {
2025
+ if (res.status === "fulfilled" && res.value.ok) {
2026
+ successIds.push(storedEvents[index].id);
2027
+ } else {
2028
+ failureCount++;
2029
+ }
2030
+ });
2031
+ if (successIds.length > 0) {
2032
+ await eventStorage.remove(successIds);
2033
+ this.circuitBreaker.recordSuccess();
2034
+ }
2035
+ if (failureCount > 0) {
2036
+ this.circuitBreaker.recordFailure();
2037
+ }
2038
+ } catch (err) {
2039
+ console.error("[NexusHub] Network Error:", err);
2040
+ this.circuitBreaker.recordFailure();
2041
+ } finally {
2042
+ this.isFlushing = false;
2043
+ if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
2044
+ setTimeout(() => this.flushQueue(), 100);
2045
+ }
2046
+ }
2047
+ }
2048
+ getSession() {
2049
+ return this.sessionId;
2050
+ }
2051
+ getVisitorId() {
2052
+ return this.visitorId;
2053
+ }
2054
+ getAnonymousId() {
2055
+ return this.anonymousId;
2056
+ }
2057
+ getSessionDuration() {
2058
+ return Date.now() - this.sessionStart;
2059
+ }
2060
+ clearIdentity() {
2061
+ safeRemoveItem("nexus_anon_id");
2062
+ this.anonymousId = "";
2063
+ }
2064
+ stop() {
2065
+ if (this.flushInterval) {
2066
+ clearInterval(this.flushInterval);
2067
+ }
2068
+ this.flushQueue(true);
2069
+ }
2070
+ };
2071
+ function extractUtmParams(url) {
2072
+ try {
2073
+ const params = new URL(url).searchParams;
2074
+ const utmKeys = [
2075
+ "utm_source",
2076
+ "utm_medium",
2077
+ "utm_campaign",
2078
+ "utm_term",
2079
+ "utm_content"
2080
+ ];
2081
+ const result = {};
2082
+ utmKeys.forEach((key) => {
2083
+ const val = params.get(key);
2084
+ if (val) result[key] = val;
2085
+ });
2086
+ return result;
2087
+ } catch {
2088
+ return {};
2089
+ }
2090
+ }
22
2091
 
2092
+ // src/analytics/index.ts
2093
+ var safeRemoveItem2 = (key) => {
2094
+ try {
2095
+ localStorage.removeItem(key);
2096
+ } catch {
2097
+ }
2098
+ };
2099
+ var AnalyticsEngine = class {
2100
+ constructor(config) {
2101
+ this.cleanupFns = [];
2102
+ this.isInitialized = false;
2103
+ this.maxScrollDepth = 0;
2104
+ this.scrollThresholdsFired = /* @__PURE__ */ new Set();
2105
+ this.lastPath = typeof window !== "undefined" ? window.location.pathname : "";
2106
+ this.clickBuffer = [];
2107
+ this.tracker = new Tracker(config);
2108
+ }
2109
+ start() {
2110
+ if (this.isInitialized || typeof window === "undefined") return;
2111
+ this.isInitialized = true;
2112
+ this.pageView();
2113
+ initVitals(this.tracker);
2114
+ this.setupClickTracking();
2115
+ this.setupFormTracking();
2116
+ this.setupRouteTracking();
2117
+ this.setupShareTracking();
2118
+ this.setupScrollTracking();
2119
+ this.setupOutboundTracking();
2120
+ this.setupVideoTracking();
2121
+ this.setupErrorTracking();
2122
+ if (process.env.NODE_ENV === "development") {
2123
+ console.log("[NexusHub] \u{1F680} Analytics Engine Started");
2124
+ }
2125
+ }
2126
+ pageView(customReferrer) {
2127
+ if (typeof window === "undefined") return;
2128
+ const currentPath = window.location.pathname;
2129
+ this.maxScrollDepth = 0;
2130
+ this.scrollThresholdsFired.clear();
2131
+ this.tracker.send("page_view", {
2132
+ path: currentPath,
2133
+ search: window.location.search,
2134
+ title: document.title,
2135
+ timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
2136
+ referrer: customReferrer || (this.lastPath !== currentPath ? this.lastPath : document.referrer)
2137
+ });
2138
+ this.lastPath = currentPath;
2139
+ }
2140
+ setupShareTracking() {
2141
+ if (typeof window === "undefined") return;
2142
+ const copyHandler = () => {
2143
+ this.tracker.send("social_share", {
2144
+ method: "clipboard_copy",
2145
+ url: window.location.href
2146
+ });
2147
+ };
2148
+ window.addEventListener("copy", copyHandler, { passive: true });
2149
+ this.cleanupFns.push(() => window.removeEventListener("copy", copyHandler));
2150
+ if (typeof navigator !== "undefined" && navigator.share) {
2151
+ const originalShare = navigator.share.bind(navigator);
2152
+ navigator.share = (data) => {
2153
+ this.tracker.send("social_share", {
2154
+ method: "native_share_menu",
2155
+ url: data?.url || window.location.href,
2156
+ title: data?.title
2157
+ });
2158
+ return originalShare(data);
2159
+ };
2160
+ this.cleanupFns.push(() => {
2161
+ navigator.share = originalShare;
2162
+ });
2163
+ }
2164
+ }
2165
+ setupScrollTracking() {
2166
+ if (typeof window === "undefined") return;
2167
+ const THRESHOLDS = [25, 50, 75, 100];
2168
+ const scrollHandler = () => {
2169
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
2170
+ const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
2171
+ if (docHeight <= 0) return;
2172
+ const pct = Math.round(scrollTop / docHeight * 100);
2173
+ if (pct > this.maxScrollDepth) {
2174
+ this.maxScrollDepth = pct;
2175
+ }
2176
+ clearTimeout(this.scrollTimer);
2177
+ this.scrollTimer = setTimeout(() => {
2178
+ for (const threshold of THRESHOLDS) {
2179
+ if (this.maxScrollDepth >= threshold && !this.scrollThresholdsFired.has(threshold)) {
2180
+ this.scrollThresholdsFired.add(threshold);
2181
+ this.tracker.send("scroll", {
2182
+ depth: threshold / 100,
2183
+ depth_percent: threshold,
2184
+ path: window.location.pathname
2185
+ });
2186
+ }
2187
+ }
2188
+ }, 300);
2189
+ };
2190
+ window.addEventListener("scroll", scrollHandler, { passive: true });
2191
+ this.cleanupFns.push(() => {
2192
+ window.removeEventListener("scroll", scrollHandler);
2193
+ clearTimeout(this.scrollTimer);
2194
+ });
2195
+ }
2196
+ setupClickTracking() {
2197
+ if (typeof window === "undefined") return;
2198
+ const clickHandler = (e) => {
2199
+ const target = e.target;
2200
+ const link = target.closest("a");
2201
+ if (link) {
2202
+ this.tracker.send("click", {
2203
+ element_type: "link",
2204
+ href: link.href,
2205
+ text: link.innerText?.substring(0, 50),
2206
+ id: link.id,
2207
+ classes: link.className,
2208
+ dataset: { ...link.dataset },
2209
+ coordinates: { x: e.clientX, y: e.clientY }
2210
+ });
2211
+ }
2212
+ const button = target.closest("button");
2213
+ if (button) {
2214
+ this.tracker.send("click", {
2215
+ element_type: "button",
2216
+ text: button.innerText?.substring(0, 50),
2217
+ id: button.id,
2218
+ classes: button.className,
2219
+ coordinates: { x: e.clientX, y: e.clientY }
2220
+ });
2221
+ }
2222
+ const now = Date.now();
2223
+ const ZONE = 400;
2224
+ const WINDOW_MS = 1e3;
2225
+ const RAGE_THRESHOLD = 3;
2226
+ this.clickBuffer.push({ x: e.clientX, y: e.clientY, t: now });
2227
+ this.clickBuffer = this.clickBuffer.filter((c) => now - c.t < WINDOW_MS);
2228
+ const zone = {
2229
+ x: Math.floor(e.clientX / ZONE),
2230
+ y: Math.floor(e.clientY / ZONE)
2231
+ };
2232
+ const zoneClicks = this.clickBuffer.filter(
2233
+ (c) => Math.floor(c.x / ZONE) === zone.x && Math.floor(c.y / ZONE) === zone.y
2234
+ );
2235
+ if (zoneClicks.length >= RAGE_THRESHOLD) {
2236
+ this.tracker.send(
2237
+ "click",
2238
+ {
2239
+ element_type: target.tagName.toLowerCase(),
2240
+ event_name: "rage_click",
2241
+ coordinates: { x: e.clientX, y: e.clientY },
2242
+ click_count: zoneClicks.length,
2243
+ path: window.location.pathname
2244
+ },
2245
+ "rage_click"
2246
+ );
2247
+ this.clickBuffer = [];
2248
+ }
2249
+ const isInteractive = target.closest(
2250
+ "a, button, input, select, textarea, [onclick], [role='button']"
2251
+ );
2252
+ if (!isInteractive) {
2253
+ const pathBefore = window.location.pathname;
2254
+ setTimeout(() => {
2255
+ const pathAfter = window.location.pathname;
2256
+ if (pathBefore === pathAfter) {
2257
+ this.tracker.send(
2258
+ "click",
2259
+ {
2260
+ element_type: target.tagName.toLowerCase(),
2261
+ event_name: "dead_click",
2262
+ selector: getSelector(target),
2263
+ coordinates: { x: e.clientX, y: e.clientY },
2264
+ path: window.location.pathname
2265
+ },
2266
+ "dead_click"
2267
+ );
2268
+ }
2269
+ }, 300);
2270
+ }
2271
+ };
2272
+ window.addEventListener("click", clickHandler, { passive: true });
2273
+ this.cleanupFns.push(
2274
+ () => window.removeEventListener("click", clickHandler)
2275
+ );
2276
+ }
2277
+ setupFormTracking() {
2278
+ if (typeof document === "undefined") return;
2279
+ const submitHandler = (e) => {
2280
+ const form = e.target;
2281
+ if (form) {
2282
+ this.tracker.send("form_submit", {
2283
+ form_id: form.id || form.name || "unknown_form",
2284
+ action: form.action,
2285
+ method: form.method,
2286
+ field_count: form.elements.length
2287
+ });
2288
+ }
2289
+ };
2290
+ document.addEventListener("submit", submitHandler, { passive: true });
2291
+ this.cleanupFns.push(
2292
+ () => document.removeEventListener("submit", submitHandler)
2293
+ );
2294
+ }
2295
+ setupOutboundTracking() {
2296
+ if (typeof window === "undefined") return;
2297
+ const outboundHandler = (e) => {
2298
+ const link = e.target.closest("a");
2299
+ if (!link || !link.href) return;
2300
+ try {
2301
+ const linkHost = new URL(link.href).hostname;
2302
+ if (linkHost && linkHost !== window.location.hostname) {
2303
+ this.tracker.send(
2304
+ "click",
2305
+ {
2306
+ event_name: "outbound_click",
2307
+ href: link.href,
2308
+ text: link.innerText?.substring(0, 50),
2309
+ destination_host: linkHost,
2310
+ coordinates: { x: e.clientX, y: e.clientY }
2311
+ },
2312
+ "outbound_click"
2313
+ );
2314
+ }
2315
+ } catch {
2316
+ }
2317
+ };
2318
+ window.addEventListener("click", outboundHandler, { passive: true });
2319
+ this.cleanupFns.push(
2320
+ () => window.removeEventListener("click", outboundHandler)
2321
+ );
2322
+ }
2323
+ setupVideoTracking() {
2324
+ if (typeof document === "undefined") return;
2325
+ const attachVideoListeners = (video) => {
2326
+ if (video.__nexus_tracked) return;
2327
+ video.__nexus_tracked = true;
2328
+ const src = video.src || video.currentSrc || "unknown";
2329
+ let milestone50Fired = false;
2330
+ video.addEventListener("play", () => {
2331
+ this.tracker.send(
2332
+ "custom_event",
2333
+ { event_name: "video_play", src },
2334
+ "video_play"
2335
+ );
2336
+ });
2337
+ video.addEventListener("pause", () => {
2338
+ this.tracker.send(
2339
+ "custom_event",
2340
+ {
2341
+ event_name: "video_pause",
2342
+ src,
2343
+ position_seconds: Math.round(video.currentTime)
2344
+ },
2345
+ "video_pause"
2346
+ );
2347
+ });
2348
+ video.addEventListener("timeupdate", () => {
2349
+ if (!video.duration || video.duration === Infinity) return;
2350
+ const pct = video.currentTime / video.duration;
2351
+ if (pct >= 0.5 && !milestone50Fired) {
2352
+ milestone50Fired = true;
2353
+ this.tracker.send(
2354
+ "custom_event",
2355
+ {
2356
+ event_name: "video_50_percent",
2357
+ src
2358
+ },
2359
+ "video_50_percent"
2360
+ );
2361
+ }
2362
+ });
2363
+ video.addEventListener("ended", () => {
2364
+ this.tracker.send(
2365
+ "custom_event",
2366
+ {
2367
+ event_name: "video_complete",
2368
+ src,
2369
+ duration_seconds: Math.round(video.duration)
2370
+ },
2371
+ "video_complete"
2372
+ );
2373
+ });
2374
+ };
2375
+ document.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2376
+ const observer = new MutationObserver((mutations) => {
2377
+ mutations.forEach((m) => {
2378
+ m.addedNodes.forEach((node) => {
2379
+ if (node instanceof HTMLVideoElement) {
2380
+ attachVideoListeners(node);
2381
+ }
2382
+ if (node instanceof Element) {
2383
+ node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2384
+ }
2385
+ });
2386
+ });
2387
+ });
2388
+ observer.observe(document.body, { childList: true, subtree: true });
2389
+ this.cleanupFns.push(() => observer.disconnect());
2390
+ }
2391
+ setupErrorTracking() {
2392
+ if (typeof window === "undefined") return;
2393
+ const errorHandler = (e) => {
2394
+ this.tracker.send("error", {
2395
+ message: e.message,
2396
+ filename: e.filename,
2397
+ lineno: e.lineno,
2398
+ colno: e.colno,
2399
+ stack: e.error?.stack,
2400
+ url: window.location.href,
2401
+ type: "uncaught_error"
2402
+ });
2403
+ };
2404
+ const rejectionHandler = (e) => {
2405
+ const reason = e.reason instanceof Error ? e.reason.message : String(e.reason);
2406
+ const stack = e.reason instanceof Error ? e.reason.stack : void 0;
2407
+ this.tracker.send("error", {
2408
+ message: reason,
2409
+ stack,
2410
+ url: window.location.href,
2411
+ type: "unhandled_rejection"
2412
+ });
2413
+ };
2414
+ window.addEventListener("error", errorHandler);
2415
+ window.addEventListener("unhandledrejection", rejectionHandler);
2416
+ this.cleanupFns.push(() => {
2417
+ window.removeEventListener("error", errorHandler);
2418
+ window.removeEventListener("unhandledrejection", rejectionHandler);
2419
+ });
2420
+ }
2421
+ setupRouteTracking() {
2422
+ if (typeof window === "undefined" || typeof window.history === "undefined")
2423
+ return;
2424
+ const originalPushState = history.pushState.bind(history);
2425
+ const originalReplaceState = history.replaceState.bind(history);
2426
+ history.pushState = (...args) => {
2427
+ const prevPath = window.location.pathname;
2428
+ originalPushState(...args);
2429
+ if (!this.isInitializedByReactProvider()) {
2430
+ this.pageView(prevPath);
2431
+ }
2432
+ };
2433
+ history.replaceState = (...args) => {
2434
+ originalReplaceState(...args);
2435
+ };
2436
+ const popStateHandler = () => {
2437
+ if (!this.isInitializedByReactProvider()) {
2438
+ this.pageView();
2439
+ }
2440
+ };
2441
+ window.addEventListener("popstate", popStateHandler);
2442
+ this.cleanupFns.push(() => {
2443
+ history.pushState = originalPushState;
2444
+ history.replaceState = originalReplaceState;
2445
+ window.removeEventListener("popstate", popStateHandler);
2446
+ });
2447
+ }
2448
+ isInitializedByReactProvider() {
2449
+ return !!document.getElementById("__nexus_react_active");
2450
+ }
2451
+ async identify(userId, traits = {}) {
2452
+ return this.sendIdentityRequest("identify", { user_id: userId, traits });
2453
+ }
2454
+ async group(groupId, traits = {}) {
2455
+ return this.sendIdentityRequest("group", { group_id: groupId, traits });
2456
+ }
2457
+ async alias(newId) {
2458
+ return this.sendIdentityRequest("alias", {
2459
+ previous_id: this.tracker.getSession(),
2460
+ user_id: newId
2461
+ });
2462
+ }
2463
+ reset(performGdprScrub = false) {
2464
+ const config = this.tracker.config;
2465
+ this.tracker.stop();
2466
+ localStorage.removeItem("nexus_sid");
2467
+ localStorage.removeItem("nexus_vid");
2468
+ safeRemoveItem2("nexus_anon_id");
2469
+ this.tracker.clearIdentity();
2470
+ if (performGdprScrub) {
2471
+ const endpoint = `${config.analyticsUrl}/api/privacy/scrub`;
2472
+ const userId = this.tracker.getVisitorId();
2473
+ fetch(endpoint, {
2474
+ method: "DELETE",
2475
+ headers: {
2476
+ "Content-Type": "application/json",
2477
+ Authorization: `Bearer ${config.apiKey}`
2478
+ },
2479
+ body: JSON.stringify({
2480
+ project_id: config.projectId,
2481
+ user_id: userId
2482
+ })
2483
+ }).catch((err) => console.error("[NexusHub] Privacy scrub failed:", err));
2484
+ }
2485
+ window.location.reload();
2486
+ }
2487
+ track(eventName, properties = {}) {
2488
+ this.tracker.send(
2489
+ "custom_event",
2490
+ { event_name: eventName, ...properties },
2491
+ eventName
2492
+ );
2493
+ }
2494
+ trackPurchase(orderData) {
2495
+ const ecommerce = {
2496
+ orderId: orderData.orderId,
2497
+ total: orderData.total,
2498
+ revenue: orderData.revenue ?? orderData.total,
2499
+ currency: orderData.currency || "USD",
2500
+ products: orderData.products.map((p) => ({
2501
+ productId: p.id,
2502
+ sku: p.sku,
2503
+ name: p.name,
2504
+ price: p.price,
2505
+ quantity: p.quantity
2506
+ }))
2507
+ };
2508
+ this.tracker.send(
2509
+ "purchase",
2510
+ { order_id: orderData.orderId },
2511
+ "purchase",
2512
+ ecommerce
2513
+ );
2514
+ }
2515
+ trackError(error, context) {
2516
+ this.tracker.send("error", {
2517
+ message: error.message,
2518
+ stack: error.stack,
2519
+ context,
2520
+ url: typeof window !== "undefined" ? window.location.href : "",
2521
+ type: "manual"
2522
+ });
2523
+ }
2524
+ async sendIdentityRequest(type, data) {
2525
+ try {
2526
+ const config = this.tracker.config;
2527
+ const endpoint = `${config.analyticsUrl}/api/${type}`;
2528
+ const payload = {
2529
+ projectId: config.projectId,
2530
+ sessionId: this.tracker.getSession(),
2531
+ visitorId: this.tracker.getVisitorId(),
2532
+ anonymousId: this.tracker.getAnonymousId(),
2533
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2534
+ ...data
2535
+ };
2536
+ const response = await fetch(endpoint, {
2537
+ method: "POST",
2538
+ headers: {
2539
+ "Content-Type": "application/json",
2540
+ Authorization: `Bearer ${config.apiKey}`
2541
+ },
2542
+ body: JSON.stringify(payload)
2543
+ });
2544
+ return response.ok;
2545
+ } catch (error) {
2546
+ console.error(`[NexusHub] ${type} failed:`, error);
2547
+ return false;
2548
+ }
2549
+ }
2550
+ getSessionId() {
2551
+ return this.tracker.getSession();
2552
+ }
2553
+ stop(isFinalShutdown = false) {
2554
+ this.cleanupFns.forEach((fn) => fn());
2555
+ this.cleanupFns = [];
2556
+ if (isFinalShutdown) {
2557
+ this.tracker.send("session_end", {
2558
+ session_id: this.tracker.getSession(),
2559
+ duration: this.tracker.getSessionDuration(),
2560
+ max_scroll_depth: this.maxScrollDepth / 100
2561
+ });
2562
+ }
2563
+ this.tracker.stop();
2564
+ this.isInitialized = false;
2565
+ }
2566
+ };
2567
+ function getSelector(el, depth = 0) {
2568
+ if (!el || depth > 2) return "";
2569
+ const tag = el.tagName.toLowerCase();
2570
+ const id = el.id ? `#${el.id}` : "";
2571
+ const cls = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
2572
+ const self = `${tag}${id}${cls}`;
2573
+ const parent = el.parentElement && depth < 2 ? `${getSelector(el.parentElement, depth + 1)} > ` : "";
2574
+ return `${parent}${self}`;
2575
+ }
23
2576
 
24
- var _chunkFMMFQP5Ocjs = require('./chunk-FMMFQP5O.cjs');
2577
+ // src/client.ts
2578
+ var NexusClient = class {
2579
+ constructor(config) {
2580
+ const fullConfig = getFullConfig(config);
2581
+ this.config = {
2582
+ debug: config?.debug ?? false,
2583
+ cacheStrategy: config?.cacheStrategy ?? "memory",
2584
+ revalidateTime: config?.revalidateTime ?? 60,
2585
+ timeout: config?.timeout ?? 1e4,
2586
+ retries: config?.retries ?? 3,
2587
+ ...fullConfig
2588
+ };
2589
+ const errors = validateConfig(this.config);
2590
+ if (errors.length > 0) {
2591
+ console.warn("\u26A0\uFE0F NexusHub: Configuration issues:", errors.join(", "));
2592
+ if (!this.config.projectId) {
2593
+ console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Tracking will fail.");
2594
+ }
2595
+ }
2596
+ this.content = new ContentEngine(this.config);
2597
+ if (typeof window !== "undefined") {
2598
+ this.analytics = new AnalyticsEngine(this.config);
2599
+ this.analytics.start();
2600
+ }
2601
+ }
2602
+ /**
2603
+ * Helper alias for cleaner content fetching.
2604
+ */
2605
+ getPage(slug, options) {
2606
+ return this.content.getPage(slug, options);
2607
+ }
2608
+ /**
2609
+ * Returns a readonly snapshot of the current config.
2610
+ */
2611
+ getConfig() {
2612
+ return { ...this.config };
2613
+ }
2614
+ /**
2615
+ * Updates specific config fields at runtime.
2616
+ * Replaces the previous pattern of `(nexus as any).config.projectId = x`
2617
+ * which bypassed TypeScript and mutated internal state unsafely.
2618
+ */
2619
+ updateConfig(updates) {
2620
+ this.config = { ...this.config, ...updates };
2621
+ }
2622
+ };
2623
+ var nexus = new NexusClient();
2624
+ var createNexusClient = (config) => new NexusClient(config);
25
2625
 
26
2626
  // src/index.ts
2627
+ init_config();
27
2628
  var VERSION = "0.0.1";
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
- exports.AnalyticsEngine = _chunkFMMFQP5Ocjs.AnalyticsEngine; exports.AuthProvider = _chunkFMMFQP5Ocjs.AuthProvider; exports.BrowserCache = _chunkFMMFQP5Ocjs.BrowserCache; exports.CacheTags = _chunkFMMFQP5Ocjs.CacheTags; exports.ContentEngine = _chunkFMMFQP5Ocjs.ContentEngine; exports.DEFAULT_ANALYTICS_URL = _chunkFMMFQP5Ocjs.DEFAULT_ANALYTICS_URL; exports.DEFAULT_API_URL = _chunkFMMFQP5Ocjs.DEFAULT_API_URL; exports.LOCAL_NEST_URL = _chunkFMMFQP5Ocjs.LOCAL_NEST_URL; exports.LOCAL_RUST_URL = _chunkFMMFQP5Ocjs.LOCAL_RUST_URL; exports.LocalCache = _chunkFMMFQP5Ocjs.LocalCacheProxy; exports.MemoryCache = _chunkFMMFQP5Ocjs.MemoryCache; exports.NexusClient = _chunkFMMFQP5Ocjs.NexusClient; exports.NexusProvider = _chunkFMMFQP5Ocjs.NexusProvider; exports.VERSION = VERSION; exports.createNexusClient = _chunkFMMFQP5Ocjs.createNexusClient; exports.getEnvConfig = _chunkFMMFQP5Ocjs.getEnvConfig; exports.getFullConfig = _chunkFMMFQP5Ocjs.getFullConfig; exports.mergeConfigs = _chunkFMMFQP5Ocjs.mergeConfigs; exports.nexus = _chunkFMMFQP5Ocjs.nexus; exports.useNexus = _chunkFMMFQP5Ocjs.useNexus; exports.useNexusAuth = _chunkFMMFQP5Ocjs.useNexusAuth; exports.validateConfig = _chunkFMMFQP5Ocjs.validateConfig;
2629
+ // Annotate the CommonJS export names for ESM import in node:
2630
+ 0 && (module.exports = {
2631
+ AnalyticsEngine,
2632
+ BrowserCache,
2633
+ CacheTags,
2634
+ ContentEngine,
2635
+ DEFAULT_ANALYTICS_URL,
2636
+ DEFAULT_API_URL,
2637
+ LOCAL_NEST_URL,
2638
+ LOCAL_RUST_URL,
2639
+ LocalCache,
2640
+ MemoryCache,
2641
+ NexusClient,
2642
+ VERSION,
2643
+ createNexusClient,
2644
+ getEnvConfig,
2645
+ getFullConfig,
2646
+ mergeConfigs,
2647
+ nexus,
2648
+ validateConfig
2649
+ });