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