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