zubbl-sdk 1.1.16 → 1.1.19

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.
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('crypto')) :
3
3
  typeof define === 'function' && define.amd ? define(['exports', 'crypto'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ZubblSdk = {}, global.crypto));
5
- })(this, (function (exports, crypto) { 'use strict';
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ZubblSdk = {}));
5
+ })(this, (function (exports) { 'use strict';
6
6
 
7
7
  // src/core/config.ts
8
8
  // -----------------------------------------------------------
@@ -15,21 +15,9 @@
15
15
  const isWorker = typeof self !== "undefined" &&
16
16
  typeof self.importScripts === "function" &&
17
17
  !isNode;
18
- let config = {
19
- apiKey: null,
20
- tenantId: null,
21
- appId: null,
22
- baseUrl: "https://api.zubbl.com/api",
23
- injectWorkerHeaders: false,
24
- workerSecret: null,
25
- };
26
- // -----------------------------------------------------------
27
- // Browser safety check
28
- // -----------------------------------------------------------
29
- function assertBrowserSafety(config) {
18
+ function assertBrowserSafety(cfg) {
30
19
  if (isBrowser || isWorker) {
31
- const key = config.apiKey ?? "";
32
- // Simple rule: if the key looks like a secret (sk_ or very long)
20
+ const key = cfg.apiKey ?? "";
33
21
  const looksSecret = key.startsWith("sk_") || key.length > 40;
34
22
  if (looksSecret) {
35
23
  throw new Error("[Zubbl SDK] Secret or server key detected in browser environment. " +
@@ -37,290 +25,40 @@
37
25
  }
38
26
  }
39
27
  }
40
- // -----------------------------------------------------------
41
- // Public API
42
- // -----------------------------------------------------------
43
- function init$1(partial) {
44
- config = { ...config, ...partial };
45
- assertBrowserSafety(config); // 🔒 check that key is safe in browser
46
- }
47
- function getConfig() {
48
- return config;
49
- }
50
- // -----------------------------------------------------------
51
- // Header builder (used by HTTP layer)
52
- // -----------------------------------------------------------
53
- function buildHeaders(extra = {}) {
54
- if (!config.apiKey || !config.tenantId || !config.appId) {
55
- throw new Error("[Zubbl SDK] Not initialized – call init() first.");
56
- }
57
- return {
58
- Authorization: `Bearer ${config.apiKey}`,
59
- "X-Tenant-Id": config.tenantId,
60
- "X-App-Id": config.appId,
61
- "Content-Type": "application/json",
62
- ...extra,
28
+ /**
29
+ * Creates an isolated configuration store.
30
+ * Each ZubblClient instance owns its own store.
31
+ */
32
+ function createConfigStore(initial) {
33
+ let current = {
34
+ apiKey: null,
35
+ tenantId: null,
36
+ appId: null,
37
+ baseUrl: "https://api.zubbl.com/api",
38
+ injectWorkerHeaders: false,
39
+ workerSecret: null,
40
+ ...initial,
63
41
  };
64
- }
65
-
66
- // src/core/context.ts
67
- // -----------------------------------------------------------
68
- // Adaptive Context Engine — Geo + Stability Enrichment + Persistence
69
- // -----------------------------------------------------------
70
- const MAX_HISTORY = 100;
71
- const CACHE_KEY = "zubbl-stability";
72
- let state = loadStability() ?? {
73
- routes: [],
74
- geo: "unknown",
75
- stability: 1.0,
76
- lastGeoChange: Date.now(),
77
- };
78
- // -----------------------------------------------------------
79
- // 💾 Persistence Helpers
80
- // -----------------------------------------------------------
81
- function loadStability() {
82
- if (!isBrowser || typeof localStorage === "undefined")
83
- return null;
84
- try {
85
- const raw = localStorage.getItem(CACHE_KEY);
86
- if (!raw)
87
- return null;
88
- const parsed = JSON.parse(raw);
89
- if (!parsed.geo || !parsed.stability)
90
- return null;
91
- return { ...parsed, routes: [] };
42
+ function init(partial) {
43
+ current = { ...current, ...partial };
44
+ assertBrowserSafety(current);
92
45
  }
93
- catch {
94
- return null;
46
+ function get() {
47
+ return current;
95
48
  }
96
- }
97
- function saveStability() {
98
- if (!isBrowser || typeof localStorage === "undefined")
99
- return;
100
- try {
101
- const payload = {
102
- geo: state.geo,
103
- stability: state.stability,
104
- lastGeoChange: state.lastGeoChange,
105
- };
106
- localStorage.setItem(CACHE_KEY, JSON.stringify(payload));
107
- }
108
- catch {
109
- /* ignore */
110
- }
111
- }
112
- // -----------------------------------------------------------
113
- // 🌍 Geo Detection (Node, Browser, Worker)
114
- // -----------------------------------------------------------
115
- function detectGeo() {
116
- try {
117
- if (isBrowser && typeof window !== "undefined" && window.navigator?.language) {
118
- return window.navigator.language.split("-")[1] || "unknown";
119
- }
120
- if (isWorker && typeof self !== "undefined" && self.navigator?.language) {
121
- return self.navigator.language.split("-")[1] || "unknown";
49
+ function buildHeaders(extra = {}) {
50
+ if (!current.apiKey || !current.tenantId || !current.appId) {
51
+ throw new Error("[Zubbl SDK] Not initialized call init() first.");
122
52
  }
123
- if (isNode && typeof process !== "undefined") {
124
- const envGeo = process.env.ZUBBL_GEO || process.env.GEO;
125
- if (envGeo && /^[A-Z]{2}$/i.test(envGeo))
126
- return envGeo.toUpperCase();
127
- return "GB";
128
- }
129
- return "unknown";
130
- }
131
- catch (err) {
132
- console.warn("[Zubbl SDK][Geo Detect] Failed:", err);
133
- return "unknown";
134
- }
135
- }
136
- // -----------------------------------------------------------
137
- // 🧭 Update Geo & Stability (with persistence)
138
- // -----------------------------------------------------------
139
- function updateGeo(geoHint) {
140
- const newGeo = geoHint || detectGeo();
141
- if (newGeo !== state.geo) {
142
- const now = Date.now();
143
- const elapsed = (now - state.lastGeoChange) / 1000;
144
- state.stability = Math.max(0.5, Math.min(1.0, elapsed / 3600));
145
- state.geo = newGeo;
146
- state.lastGeoChange = now;
147
- saveStability();
148
- }
149
- else {
150
- state.stability = Math.min(1.0, state.stability + 0.01);
151
- saveStability();
152
- }
153
- }
154
- // -----------------------------------------------------------
155
- // 🧮 Record Context Samples
156
- // -----------------------------------------------------------
157
- function recordContextSample(path, method, status, latency) {
158
- state.routes.push({ path, method, ts: Date.now(), status, latency });
159
- if (state.routes.length > MAX_HISTORY)
160
- state.routes.shift();
161
- updateGeo(); // auto-refresh geo info
162
- }
163
- // -----------------------------------------------------------
164
- // 📦 Get Adaptive Headers
165
- // -----------------------------------------------------------
166
- function getAdaptiveHeaders() {
167
- const currentGeo = detectGeo();
168
- updateGeo(currentGeo);
169
- const summary = btoa(JSON.stringify({
170
- env: isNode ? "node" : isBrowser ? "browser" : "worker",
171
- routeCount: state.routes.length,
172
- lastGeo: state.geo,
173
- avgLatency: state.routes.reduce((a, b) => a + b.latency, 0) /
174
- Math.max(1, state.routes.length),
175
- }));
176
- return {
177
- "X-Zubbl-Context-Summary": summary,
178
- "X-Zubbl-Geo-Stability": `${state.geo}/${state.stability.toFixed(2)}`,
179
- };
180
- }
181
- // -----------------------------------------------------------
182
- // 🌍 Initialize Geo Context Immediately
183
- // -----------------------------------------------------------
184
- updateGeo(process.env.ZUBBL_GEO);
185
-
186
- // -----------------------------------------------------------
187
- // 🔐 Secure Signing & Nonce Generator
188
- // -----------------------------------------------------------
189
- // -----------------------------------------------------------
190
- // Generate UUIDv4 Nonce
191
- // -----------------------------------------------------------
192
- function generateNonce() {
193
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
194
- const r = (Math.random() * 16) | 0;
195
- const v = c === "x" ? r : (r & 0x3) | 0x8;
196
- return v.toString(16);
197
- });
198
- }
199
- // -----------------------------------------------------------
200
- // Compute SHA256 HMAC signature
201
- // -----------------------------------------------------------
202
- async function signPayload(payload, secret) {
203
- if (isNode) {
204
- // ✅ Node.js native HMAC
205
- return crypto.createHmac("sha256", secret).update(payload).digest("hex");
206
- }
207
- // ✅ Browser / Worker using WebCrypto
208
- if (typeof window !== "undefined" && window.crypto?.subtle) {
209
- const key = await window.crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
210
- const sig = await window.crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
211
- return Array.from(new Uint8Array(sig))
212
- .map(b => b.toString(16).padStart(2, "0"))
213
- .join("");
214
- }
215
- throw new Error("No crypto implementation available in this environment");
216
- }
217
- // -----------------------------------------------------------
218
- // Attach signing headers to a request
219
- // -----------------------------------------------------------
220
- async function attachSigningHeaders(method, url, body, secretKey) {
221
- const nonce = generateNonce();
222
- // Compute canonical string (deterministic)
223
- const bodyHash = body
224
- ? crypto.createHash("sha256").update(body).digest("hex")
225
- : "";
226
- const canonical = `${method.toUpperCase()}\n${url}\n${nonce}\n${bodyHash}`;
227
- // ⚙️ Always include nonce even if secret is missing
228
- if (!secretKey) {
229
- return { "X-Zubbl-Nonce": nonce };
230
- }
231
- const signature = await signPayload(canonical, secretKey);
232
- return {
233
- "X-Zubbl-Nonce": nonce,
234
- "X-Zubbl-Signature": signature,
235
- };
236
- }
237
-
238
- // src/core/http.ts
239
- // -----------------------------------------------------------
240
- // Isomorphic HTTP transport for Node, Browser, and Workers
241
- // Includes adaptive context, telemetry, signing & safety timeout
242
- // -----------------------------------------------------------
243
- async function httpRequest(url, options = {}) {
244
- // 🔧 Default timeout lowered for SDK-12 hardening
245
- const { timeoutMs = 250, ...fetchOpts } = options;
246
- // ---------------------------------------------------------
247
- // Polyfill fetch for Node
248
- // ---------------------------------------------------------
249
- let fetchImpl;
250
- if (isNode) {
251
- const { fetch } = await import('undici');
252
- // @ts-expect-error Node native fetch lacks duplex definition in undici types
253
- fetchImpl = fetch;
254
- }
255
- else if (isBrowser || isWorker) {
256
- fetchImpl = fetch;
257
- }
258
- else {
259
- console.warn("[Zubbl SDK][HTTP] Unsupported runtime environment");
260
- return null;
261
- }
262
- const controller = new AbortController();
263
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
264
- const start = performance.now();
265
- try {
266
- const config = getConfig();
267
- const bodyText = fetchOpts.body && typeof fetchOpts.body === "string"
268
- ? fetchOpts.body
269
- : fetchOpts.body
270
- ? JSON.stringify(fetchOpts.body)
271
- : "";
272
- // ---------------------------------------------------------
273
- // 🔐 Signing headers (adds X-Zubbl-Nonce and X-Zubbl-Signature)
274
- // ---------------------------------------------------------
275
- const signingHeaders = await attachSigningHeaders(fetchOpts.method ?? "GET", url, bodyText, config.workerSecret ?? null);
276
- // ---------------------------------------------------------
277
- // 🧠 Adaptive context headers
278
- // ---------------------------------------------------------
279
- const adaptiveHeaders = getAdaptiveHeaders();
280
- // ✅ Merge headers cleanly — final order matters
281
- fetchOpts.headers = {
282
- ...(fetchOpts.headers || {}),
283
- ...adaptiveHeaders,
284
- ...signingHeaders,
53
+ return {
54
+ Authorization: `Bearer ${current.apiKey}`,
55
+ "X-Tenant-Id": current.tenantId,
56
+ "X-App-Id": current.appId,
57
+ "Content-Type": "application/json",
58
+ ...extra,
285
59
  };
286
- // ---------------------------------------------------------
287
- // Perform request
288
- // ---------------------------------------------------------
289
- const response = await fetchImpl(url, { ...fetchOpts, signal: controller.signal });
290
- const latencyMs = performance.now() - start;
291
- const status = response?.status ?? 0;
292
- // -----------------------------------------------------
293
- // Telemetry hook — record runtime discovery + metrics
294
- // -----------------------------------------------------
295
- await Promise.resolve().then(function () { return telemetry; }).then(({ recordRequestTelemetry, logRuntimeDiscovery }) => {
296
- logRuntimeDiscovery(url);
297
- recordRequestTelemetry(url, fetchOpts.method ?? "GET", status, latencyMs);
298
- });
299
- // -----------------------------------------------------
300
- // Adaptive context sample recording
301
- // -----------------------------------------------------
302
- recordContextSample(url, fetchOpts.method ?? "GET", status, latencyMs);
303
- // -----------------------------------------------------
304
- // Error handling / Soft fail behaviour
305
- // -----------------------------------------------------
306
- if (!response.ok) {
307
- const text = await response.text().catch(() => "");
308
- console.warn(`[Zubbl SDK][HTTP] Non-OK response ${status}: ${text}`);
309
- return response; // still return so host can inspect
310
- }
311
- return response;
312
- }
313
- catch (err) {
314
- const latencyMs = performance.now() - start;
315
- const reason = err?.name === "AbortError"
316
- ? `timeout (${timeoutMs}ms)`
317
- : err?.message || "unknown network error";
318
- console.warn(`[Zubbl SDK][HTTP] Request failed: ${reason}`, { url, latencyMs });
319
- return null; // 🧩 Never throw to host app
320
- }
321
- finally {
322
- clearTimeout(timeout);
323
60
  }
61
+ return { init, get, buildHeaders };
324
62
  }
325
63
 
326
64
  // src/core/storage.ts
@@ -521,202 +259,408 @@
521
259
  };
522
260
  }
523
261
 
524
- // ---------------------------------------------------------
525
- // Zubbl SDK — Unified Bootstrap (Adaptive + GEO Aware)
526
- // ---------------------------------------------------------
527
- // ---------------------------------------------------------
528
- // Initialization
529
- // ---------------------------------------------------------
530
- async function init({ apiKey, tenantId, appId, baseUrl = "https://api.zubbl.com/api", injectWorkerHeaders = false, workerSecret = null, }) {
531
- // Initialize core configuration
532
- init$1({ apiKey, tenantId, appId, baseUrl, injectWorkerHeaders, workerSecret });
533
- // Prime GEO context cache
262
+ // src/core/context.ts
263
+ // -----------------------------------------------------------
264
+ // Adaptive Context Engine — Geo + Stability Enrichment + Persistence
265
+ // -----------------------------------------------------------
266
+ const MAX_HISTORY = 100;
267
+ const CACHE_KEY = "zubbl-stability";
268
+ let state = loadStability() ?? {
269
+ routes: [],
270
+ geo: "unknown",
271
+ stability: 1.0,
272
+ lastGeoChange: Date.now(),
273
+ };
274
+ // -----------------------------------------------------------
275
+ // 💾 Persistence Helpers
276
+ // -----------------------------------------------------------
277
+ function loadStability() {
278
+ if (!isBrowser || typeof localStorage === "undefined")
279
+ return null;
534
280
  try {
535
- const geo = await getGeoInfo();
536
- console.log(`[Zubbl SDK] 🌍 GEO detected: ${geo.country} (ASN ${geo.asn || "?"})`);
281
+ const raw = localStorage.getItem(CACHE_KEY);
282
+ if (!raw)
283
+ return null;
284
+ const parsed = JSON.parse(raw);
285
+ if (!parsed.geo || !parsed.stability)
286
+ return null;
287
+ return { ...parsed, routes: [] };
537
288
  }
538
- catch (err) {
539
- console.warn("[Zubbl SDK] GEO init failed:", err?.message);
289
+ catch {
290
+ return null;
540
291
  }
541
292
  }
542
- // ---------------------------------------------------------
543
- // Identify User
544
- // ---------------------------------------------------------
545
- async function identifyUser({ email, name }) {
546
- const config = getConfig();
547
- if (!config.apiKey || !config.tenantId || !config.appId) {
548
- throw new Error("Zubbl SDK not initialized");
293
+ function saveStability() {
294
+ if (!isBrowser || typeof localStorage === "undefined")
295
+ return;
296
+ try {
297
+ const payload = {
298
+ geo: state.geo,
299
+ stability: state.stability,
300
+ lastGeoChange: state.lastGeoChange,
301
+ };
302
+ localStorage.setItem(CACHE_KEY, JSON.stringify(payload));
549
303
  }
550
- const adaptiveHeaders = getAdaptiveHeaders();
551
- const geoHeaders = await getGeoHeaders();
552
- const headers = buildHeaders({
553
- "Content-Type": "application/json",
554
- ...adaptiveHeaders,
555
- ...geoHeaders,
556
- });
557
- const body = JSON.stringify({ email, name });
558
- const resp = await httpRequest(`${config.baseUrl}/sdk/identify`, {
559
- method: "POST",
560
- headers,
561
- body,
562
- });
563
- const data = await resp.json().catch(() => ({}));
564
- if (!data.external_user_id) {
565
- console.warn("[Zubbl SDK] identifyUser: missing external_user_id");
304
+ catch {
305
+ /* ignore */
566
306
  }
567
- return data;
568
307
  }
569
- // ---------------------------------------------------------
570
- // Fetch Effective Tiles
571
- // ---------------------------------------------------------
572
- async function getTiles({ external_user_id, app_id = null, }) {
573
- const config = getConfig();
574
- if (!config.apiKey || !config.tenantId || !config.appId) {
575
- throw new Error("Zubbl SDK not initialized");
308
+ // -----------------------------------------------------------
309
+ // 🌍 Geo Detection (Node, Browser, Worker)
310
+ // -----------------------------------------------------------
311
+ function detectGeo() {
312
+ try {
313
+ if (isBrowser && typeof window !== "undefined" && window.navigator?.language) {
314
+ return window.navigator.language.split("-")[1] || "unknown";
315
+ }
316
+ if (isWorker && typeof self !== "undefined" && self.navigator?.language) {
317
+ return self.navigator.language.split("-")[1] || "unknown";
318
+ }
319
+ if (isNode && typeof process !== "undefined") {
320
+ const envGeo = process.env.ZUBBL_GEO || process.env.GEO;
321
+ if (envGeo && /^[A-Z]{2}$/i.test(envGeo))
322
+ return envGeo.toUpperCase();
323
+ return "GB";
324
+ }
325
+ return "unknown";
326
+ }
327
+ catch (err) {
328
+ console.warn("[Zubbl SDK][Geo Detect] Failed:", err);
329
+ return "unknown";
576
330
  }
577
- if (!external_user_id)
578
- throw new Error("external_user_id is required");
579
- const adaptiveHeaders = getAdaptiveHeaders();
580
- const geoHeaders = await getGeoHeaders();
581
- const headers = buildHeaders({
582
- "X-External-User-Id": external_user_id,
583
- ...adaptiveHeaders,
584
- ...geoHeaders,
585
- });
586
- const params = new URLSearchParams({ external_user_id });
587
- params.set("app_id", app_id || config.appId);
588
- const resp = await httpRequest(`${config.baseUrl.replace(/\/$/, "")}/tiles/effective?${params.toString()}`, { method: "GET", headers });
589
- return resp.json();
590
331
  }
591
- // ---------------------------------------------------------
592
- // Policy Enforcement
593
- // ---------------------------------------------------------
594
- async function enforce({ external_user_id, app_id = null, }) {
595
- const config = getConfig();
596
- if (!config.apiKey || !config.tenantId || !config.appId) {
597
- throw new Error("Zubbl SDK not initialized");
332
+ // -----------------------------------------------------------
333
+ // 🧭 Update Geo & Stability (with persistence)
334
+ // -----------------------------------------------------------
335
+ function updateGeo(geoHint) {
336
+ const newGeo = geoHint || detectGeo();
337
+ if (newGeo !== state.geo) {
338
+ const now = Date.now();
339
+ const elapsed = (now - state.lastGeoChange) / 1000;
340
+ state.stability = Math.max(0.5, Math.min(1.0, elapsed / 3600));
341
+ state.geo = newGeo;
342
+ state.lastGeoChange = now;
343
+ saveStability();
344
+ }
345
+ else {
346
+ state.stability = Math.min(1.0, state.stability + 0.01);
347
+ saveStability();
598
348
  }
599
- const adaptiveHeaders = getAdaptiveHeaders();
600
- const geoHeaders = await getGeoHeaders();
601
- const headers = buildHeaders({
602
- "X-External-User-Id": external_user_id,
603
- ...adaptiveHeaders,
604
- ...geoHeaders,
605
- });
606
- const resp = await httpRequest(`${config.baseUrl.replace(/\/$/, "")}/sdk/test-enforcement`, { method: "POST", headers });
607
- const json = await resp.json().catch(() => ({}));
608
- return { decision: "allow", status: resp.status, data: json };
609
349
  }
610
- // ---------------------------------------------------------
611
- // Exposed Helpers
612
- // ---------------------------------------------------------
613
- const storage = createStorage();
614
-
615
350
  // -----------------------------------------------------------
616
- // Zubbl Telemetry Core (Endpoint Discovery)
351
+ // 🧮 Record Context Samples
617
352
  // -----------------------------------------------------------
618
- // Local telemetry buffer
619
- let sampleRatio = 0.1; // 1 in 10 by default
620
- let buffer = [];
621
- let debounceTimer = null;
622
- function shouldSample() {
623
- return Math.random() < sampleRatio;
353
+ function recordContextSample(path, method, status, latency) {
354
+ state.routes.push({ path, method, ts: Date.now(), status, latency });
355
+ if (state.routes.length > MAX_HISTORY)
356
+ state.routes.shift();
357
+ updateGeo(); // auto-refresh geo info
624
358
  }
625
359
  // -----------------------------------------------------------
626
- // Emit telemetry events
360
+ // 📦 Get Adaptive Headers
627
361
  // -----------------------------------------------------------
628
- function emitEndpoint(payload) {
629
- if (!shouldSample())
630
- return;
631
- const enriched = {
632
- ...payload,
633
- ts: Date.now(),
634
- source: isBrowser ? "browser" : isNode ? "node" : "worker",
362
+ function getAdaptiveHeaders() {
363
+ const currentGeo = detectGeo();
364
+ updateGeo(currentGeo);
365
+ const summary = btoa(JSON.stringify({
366
+ env: isNode ? "node" : isBrowser ? "browser" : "worker",
367
+ routeCount: state.routes.length,
368
+ lastGeo: state.geo,
369
+ avgLatency: state.routes.reduce((a, b) => a + b.latency, 0) /
370
+ Math.max(1, state.routes.length),
371
+ }));
372
+ return {
373
+ "X-Zubbl-Context-Summary": summary,
374
+ "X-Zubbl-Geo-Stability": `${state.geo}/${state.stability.toFixed(2)}`,
635
375
  };
636
- buffer.push(enriched);
637
- if (!debounceTimer) {
638
- debounceTimer = setTimeout(flushBuffer, 200);
639
- }
640
376
  }
641
377
  // -----------------------------------------------------------
642
- // Flush logic (batch send)
378
+ // 🌍 Initialize Geo Context Immediately
643
379
  // -----------------------------------------------------------
644
- async function flushBuffer() {
645
- if (buffer.length === 0) {
646
- debounceTimer = null;
647
- return;
380
+ updateGeo(process.env.ZUBBL_GEO);
381
+
382
+ // -----------------------------------------------------------
383
+ // 🔐 Secure Signing & Nonce Generator
384
+ // -----------------------------------------------------------
385
+ // -----------------------------------------------------------
386
+ // Generate UUIDv4 Nonce
387
+ // -----------------------------------------------------------
388
+ function generateNonce() {
389
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
390
+ const r = (Math.random() * 16) | 0;
391
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
392
+ return v.toString(16);
393
+ });
394
+ }
395
+ // -----------------------------------------------------------
396
+ // Attach signing headers to a request
397
+ // -----------------------------------------------------------
398
+ async function attachSigningHeaders(method, url, body, secretKey) {
399
+ const nonce = generateNonce();
400
+ // Compute canonical string (deterministic)
401
+ const bodyHash = "";
402
+ `${method.toUpperCase()}\n${url}\n${nonce}\n${bodyHash}`;
403
+ // ⚙️ Always include nonce even if secret is missing
404
+ {
405
+ return { "X-Zubbl-Nonce": nonce };
648
406
  }
649
- const batch = [...buffer];
650
- buffer = [];
651
- debounceTimer = null;
652
- const config = getConfig();
653
- const url = `${config.baseUrl.replace(/\/$/, "")}/sdk/log`;
654
- const headers = {
655
- "Content-Type": "application/json",
656
- Authorization: `Bearer ${config.apiKey}`,
657
- "X-Tenant-Id": config.tenantId ?? "",
658
- "X-App-Id": config.appId ?? "",
659
- };
407
+ }
408
+
409
+ // src/core/http.ts
410
+ // -----------------------------------------------------------
411
+ // HTTP utility (instance-safe)
412
+ // -----------------------------------------------------------
413
+ /**
414
+ * Performs a safe HTTP request with adaptive + signing headers.
415
+ * NOTE: no global getConfig() dependency — caller must provide fully resolved URL + headers.
416
+ */
417
+ async function httpRequest(url, options = {}) {
418
+ // 🔧 Default timeout lowered for SDK-12 hardening
419
+ const controller = new AbortController();
420
+ const timeout = setTimeout(() => controller.abort(), 8000);
660
421
  try {
661
- const resp = await fetch(url, {
662
- method: "POST",
663
- headers,
664
- body: JSON.stringify(batch),
665
- });
666
- const text = await resp.text();
667
- console.log("[Zubbl SDK][Telemetry] Flushed", batch.length, "events →", resp.status, text);
422
+ const finalOptions = {
423
+ ...options,
424
+ headers: {
425
+ ...options.headers,
426
+ ...getAdaptiveHeaders(),
427
+ },
428
+ signal: controller.signal,
429
+ };
430
+ attachSigningHeaders(finalOptions.headers);
431
+ // Record adaptive context sample
432
+ recordContextSample();
433
+ const response = await fetch(url, finalOptions);
434
+ if (!response.ok) {
435
+ console.warn(`[Zubbl SDK][HTTP] ${response.status} on ${url}`);
436
+ }
437
+ return response;
668
438
  }
669
439
  catch (err) {
670
- console.warn("[Zubbl SDK][Telemetry] Failed to send batch:", err.message);
440
+ console.error("[Zubbl SDK][HTTP] Request failed:", err?.message);
441
+ throw err;
442
+ }
443
+ finally {
444
+ clearTimeout(timeout);
445
+ }
446
+ }
447
+
448
+ // src/features/identifyUser.ts
449
+ // -----------------------------------------------------------
450
+ // Zubbl SDK — Instance-safe identifyUser feature
451
+ // -----------------------------------------------------------
452
+ const USER_CACHE_KEY = "external_user_id";
453
+ const USER_TTL = 24 * 60 * 60; // 24h in seconds
454
+ /**
455
+ * Identify or auto-create a user by email/name.
456
+ * Instance-safe: requires store (from createConfigStore()) passed in.
457
+ * Idempotent: returns cached UUID if available.
458
+ */
459
+ async function identifyUserFeature(store, { email, name }) {
460
+ if (!email)
461
+ throw new Error("email is required");
462
+ const config = store.get();
463
+ const storage = createStorage();
464
+ // ⚡ Step 1 — Check cache
465
+ const cached = storage.get(USER_CACHE_KEY);
466
+ if (cached?.id) {
467
+ return { external_user_id: cached.id, cached: true };
468
+ }
469
+ // ⚙️ Step 2 — Prepare headers and request backend
470
+ const adaptiveHeaders = getAdaptiveHeaders();
471
+ const geoHeaders = await getGeoHeaders();
472
+ const headers = store.buildHeaders({
473
+ "Content-Type": "application/json",
474
+ ...adaptiveHeaders,
475
+ ...geoHeaders,
476
+ });
477
+ const resp = await httpRequest(`${config.baseUrl}/sdk/identify`, {
478
+ method: "POST",
479
+ headers,
480
+ body: JSON.stringify({ email, name }),
481
+ });
482
+ const data = await resp.json().catch(() => ({}));
483
+ const { external_user_id } = data;
484
+ if (!external_user_id) {
485
+ throw new Error("Backend did not return external_user_id");
671
486
  }
487
+ // 🗃️ Step 3 — Cache for 24 h
488
+ storage.set(USER_CACHE_KEY, { id: external_user_id }, USER_TTL);
489
+ return { external_user_id, cached: false };
672
490
  }
491
+
492
+ // src/features/getTiles.ts
673
493
  // -----------------------------------------------------------
674
- // Helper for HTTP wrapping
494
+ // Zubbl SDK Instance-safe getTiles feature
675
495
  // -----------------------------------------------------------
676
- async function recordRequestTelemetry(url, method, status, latency, extra = {}) {
496
+ const TILE_CACHE_KEY = "tiles_cache";
497
+ const TILE_TTL_DEFAULT = 10 * 60; // 10 min in seconds
498
+ /**
499
+ * Fetch effective tiles with caching, ETag, and background refresh.
500
+ * Instance-safe: requires store (from createConfigStore()) passed in.
501
+ */
502
+ async function getTilesFeature(store, { external_user_id, app_id = null, ttlSeconds = TILE_TTL_DEFAULT, }) {
503
+ if (!external_user_id)
504
+ throw new Error("external_user_id is required");
505
+ const config = store.get();
506
+ const storage = createStorage();
507
+ const cached = storage.get(TILE_CACHE_KEY);
508
+ const isFresh = cached && cached.expiresAt > Date.now();
509
+ // ⚡ Return cached data immediately if valid
510
+ if (isFresh) {
511
+ backgroundRefresh(store, external_user_id, app_id, ttlSeconds);
512
+ return { data: cached.data, fromCache: true };
513
+ }
677
514
  try {
678
- const path = new URL(url).pathname;
679
- emitEndpoint({
680
- path,
681
- method,
682
- status,
683
- latency,
684
- ...extra,
515
+ const adaptiveHeaders = getAdaptiveHeaders();
516
+ const geoHeaders = await getGeoHeaders();
517
+ const headers = store.buildHeaders({
518
+ "X-External-User-Id": external_user_id,
519
+ ...adaptiveHeaders,
520
+ ...geoHeaders,
685
521
  });
522
+ if (cached?.etag)
523
+ headers["If-None-Match"] = cached.etag;
524
+ const params = new URLSearchParams({ external_user_id });
525
+ params.set("app_id", app_id || config.appId);
526
+ const url = `${config.baseUrl.replace(/\/$/, "")}/tiles/effective?${params.toString()}`;
527
+ const resp = await httpRequest(url, { headers });
528
+ if (resp.status === 304 && cached?.data) {
529
+ cached.expiresAt = Date.now() + ttlSeconds * 1000;
530
+ storage.set(TILE_CACHE_KEY, cached, ttlSeconds);
531
+ return { data: cached.data, fromCache: true, status: 304 };
532
+ }
533
+ const data = await resp.json();
534
+ const etag = resp.headers.get("ETag") || undefined;
535
+ const entry = {
536
+ data,
537
+ etag,
538
+ expiresAt: Date.now() + ttlSeconds * 1000,
539
+ };
540
+ storage.set(TILE_CACHE_KEY, entry, ttlSeconds);
541
+ return { data, fromCache: false, status: resp.status };
686
542
  }
687
543
  catch (err) {
688
- console.warn("[Zubbl SDK][Telemetry] Failed to record telemetry", err);
544
+ if (cached?.data) {
545
+ return { data: cached.data, fromCache: true, error: String(err) };
546
+ }
547
+ throw err;
689
548
  }
690
549
  }
691
- // -----------------------------------------------------------
692
- // Runtime Endpoint Discovery
693
- // -----------------------------------------------------------
694
- let hasLoggedDiscovery = false;
695
- function logRuntimeDiscovery(url) {
696
- if (hasLoggedDiscovery)
697
- return; // only once per runtime
698
- hasLoggedDiscovery = true;
699
- const runtime = isNode ? "Node" : isBrowser ? "Browser" : isWorker ? "Worker" : "Unknown";
700
- const time = new Date().toISOString();
701
- console.log(`[Zubbl SDK][Discovery] Runtime: ${runtime}`);
702
- console.log(`[Zubbl SDK][Discovery] Timestamp: ${time}`);
703
- console.log(`[Zubbl SDK][Discovery] Initial endpoint: ${url}`);
550
+ /**
551
+ * Background refresh (non-blocking).
552
+ */
553
+ async function backgroundRefresh(store, external_user_id, app_id, ttlSeconds) {
554
+ try {
555
+ await getTilesFeature(store, { external_user_id, app_id, ttlSeconds });
556
+ }
557
+ catch {
558
+ // Silently ignore background refresh errors
559
+ }
560
+ }
561
+
562
+ // src/client.ts
563
+ // -----------------------------------------------------------
564
+ // Zubbl SDK — Final Instance-based Client (Feature Delegation)
565
+ // -----------------------------------------------------------
566
+ /**
567
+ * Creates a self-contained, instance-based Zubbl client.
568
+ * Each instance owns its own config store and initialization state.
569
+ */
570
+ function createZubblClient(cfg) {
571
+ const store = createConfigStore();
572
+ const storage = createStorage();
573
+ let initialized = false;
574
+ // ---------------------------------------------------------
575
+ // Initialization
576
+ // ---------------------------------------------------------
577
+ async function init() {
578
+ if (initialized)
579
+ return;
580
+ // Initialize per-client config
581
+ store.init(cfg);
582
+ // Optional: GEO bootstrap
583
+ try {
584
+ const geo = await getGeoInfo();
585
+ console.log(`[Zubbl SDK] 🌍 GEO detected: ${geo.country} (ASN ${geo.asn || "?"})`);
586
+ }
587
+ catch (err) {
588
+ console.warn("[Zubbl SDK] GEO init failed:", err?.message);
589
+ }
590
+ initialized = true;
591
+ }
592
+ function assertInitialized() {
593
+ if (!initialized) {
594
+ throw new Error("Zubbl client not initialized — call client.init() first");
595
+ }
596
+ }
597
+ // ---------------------------------------------------------
598
+ // User + Tile Operations (delegated to feature modules)
599
+ // ---------------------------------------------------------
600
+ async function identifyUser({ email, name }) {
601
+ assertInitialized();
602
+ return identifyUserFeature(store, { email, name });
603
+ }
604
+ async function getTiles({ external_user_id, app_id = null, ttlSeconds, }) {
605
+ assertInitialized();
606
+ return getTilesFeature(store, { external_user_id, app_id, ttlSeconds });
607
+ }
608
+ // ---------------------------------------------------------
609
+ // Enforcement API (kept inline)
610
+ // ---------------------------------------------------------
611
+ async function enforce({ external_user_id, app_id = null, }) {
612
+ assertInitialized();
613
+ const config = store.get();
614
+ const headers = store.buildHeaders({
615
+ "X-External-User-Id": external_user_id,
616
+ });
617
+ const resp = await fetch(`${config.baseUrl?.replace(/\/$/, "")}/sdk/test-enforcement`, {
618
+ method: "POST",
619
+ headers,
620
+ });
621
+ const json = await resp.json().catch(() => ({}));
622
+ return { decision: "allow", status: resp.status, data: json };
623
+ }
624
+ // ---------------------------------------------------------
625
+ // Public API
626
+ // ---------------------------------------------------------
627
+ return {
628
+ init,
629
+ identifyUser,
630
+ getTiles,
631
+ enforce,
632
+ storage,
633
+ };
704
634
  }
705
635
 
706
- var telemetry = /*#__PURE__*/Object.freeze({
707
- __proto__: null,
708
- emitEndpoint: emitEndpoint,
709
- logRuntimeDiscovery: logRuntimeDiscovery,
710
- recordRequestTelemetry: recordRequestTelemetry
711
- });
636
+ // src/index.ts Backward-compatible singleton API
637
+ let defaultClient = null;
638
+ async function init(config) {
639
+ if (!defaultClient)
640
+ defaultClient = createZubblClient(config);
641
+ return defaultClient.init();
642
+ }
643
+ async function identifyUser(...args) {
644
+ if (!defaultClient)
645
+ throw new Error("Zubbl SDK not initialized — call init() first");
646
+ return defaultClient.identifyUser(...args);
647
+ }
648
+ async function getTiles(...args) {
649
+ if (!defaultClient)
650
+ throw new Error("Zubbl SDK not initialized — call init() first");
651
+ return defaultClient.getTiles(...args);
652
+ }
653
+ async function enforce(...args) {
654
+ if (!defaultClient)
655
+ throw new Error("Zubbl SDK not initialized — call init() first");
656
+ return defaultClient.enforce(...args);
657
+ }
712
658
 
659
+ exports.createZubblClient = createZubblClient;
713
660
  exports.enforce = enforce;
714
- exports.getConfig = getConfig;
715
661
  exports.getTiles = getTiles;
716
- exports.httpRequest = httpRequest;
717
662
  exports.identifyUser = identifyUser;
718
663
  exports.init = init;
719
- exports.storage = storage;
720
664
 
721
665
  }));
722
666
  //# sourceMappingURL=zubbl-sdk.umd.js.map