zubbl-sdk 1.1.18 → 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: [] };
92
- }
93
- catch {
94
- return null;
42
+ function init(partial) {
43
+ current = { ...current, ...partial };
44
+ assertBrowserSafety(current);
95
45
  }
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 */
46
+ function get() {
47
+ return current;
110
48
  }
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";
49
+ function buildHeaders(extra = {}) {
50
+ if (!current.apiKey || !current.tenantId || !current.appId) {
51
+ throw new Error("[Zubbl SDK] Not initialized – call init() first.");
119
52
  }
120
- if (isWorker && typeof self !== "undefined" && self.navigator?.language) {
121
- return self.navigator.language.split("-")[1] || "unknown";
122
- }
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,14 +259,327 @@
521
259
  };
522
260
  }
523
261
 
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;
280
+ try {
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: [] };
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ }
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));
303
+ }
304
+ catch {
305
+ /* ignore */
306
+ }
307
+ }
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";
330
+ }
331
+ }
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();
348
+ }
349
+ }
350
+ // -----------------------------------------------------------
351
+ // 🧮 Record Context Samples
352
+ // -----------------------------------------------------------
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
358
+ }
359
+ // -----------------------------------------------------------
360
+ // 📦 Get Adaptive Headers
361
+ // -----------------------------------------------------------
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)}`,
375
+ };
376
+ }
377
+ // -----------------------------------------------------------
378
+ // 🌍 Initialize Geo Context Immediately
379
+ // -----------------------------------------------------------
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 };
406
+ }
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);
421
+ try {
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;
438
+ }
439
+ catch (err) {
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");
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 };
490
+ }
491
+
492
+ // src/features/getTiles.ts
493
+ // -----------------------------------------------------------
494
+ // Zubbl SDK — Instance-safe getTiles feature
495
+ // -----------------------------------------------------------
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
+ }
514
+ try {
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,
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 };
542
+ }
543
+ catch (err) {
544
+ if (cached?.data) {
545
+ return { data: cached.data, fromCache: true, error: String(err) };
546
+ }
547
+ throw err;
548
+ }
549
+ }
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
+
524
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
+ */
525
570
  function createZubblClient(cfg) {
526
- let initialized = false;
571
+ const store = createConfigStore();
527
572
  const storage = createStorage();
573
+ let initialized = false;
574
+ // ---------------------------------------------------------
575
+ // Initialization
576
+ // ---------------------------------------------------------
528
577
  async function init() {
529
578
  if (initialized)
530
579
  return;
531
- init$1(cfg);
580
+ // Initialize per-client config
581
+ store.init(cfg);
582
+ // Optional: GEO bootstrap
532
583
  try {
533
584
  const geo = await getGeoInfo();
534
585
  console.log(`[Zubbl SDK] 🌍 GEO detected: ${geo.country} (ASN ${geo.asn || "?"})`);
@@ -543,55 +594,36 @@
543
594
  throw new Error("Zubbl client not initialized — call client.init() first");
544
595
  }
545
596
  }
597
+ // ---------------------------------------------------------
598
+ // User + Tile Operations (delegated to feature modules)
599
+ // ---------------------------------------------------------
546
600
  async function identifyUser({ email, name }) {
547
601
  assertInitialized();
548
- const adaptiveHeaders = getAdaptiveHeaders();
549
- const geoHeaders = await getGeoHeaders();
550
- const headers = buildHeaders({
551
- "Content-Type": "application/json",
552
- ...adaptiveHeaders,
553
- ...geoHeaders,
554
- });
555
- const body = JSON.stringify({ email, name });
556
- const resp = await httpRequest(`${cfg.baseUrl}/sdk/identify`, {
557
- method: "POST",
558
- headers,
559
- body,
560
- });
561
- return resp.json().catch(() => ({}));
602
+ return identifyUserFeature(store, { email, name });
562
603
  }
563
- async function getTiles({ external_user_id, app_id = null }) {
604
+ async function getTiles({ external_user_id, app_id = null, ttlSeconds, }) {
564
605
  assertInitialized();
565
- if (!external_user_id)
566
- throw new Error("external_user_id is required");
567
- const adaptiveHeaders = getAdaptiveHeaders();
568
- const geoHeaders = await getGeoHeaders();
569
- const headers = buildHeaders({
570
- "X-External-User-Id": external_user_id,
571
- ...adaptiveHeaders,
572
- ...geoHeaders,
573
- });
574
- const params = new URLSearchParams({ external_user_id });
575
- params.set("app_id", app_id || cfg.appId);
576
- const resp = await httpRequest(`${cfg.baseUrl?.replace(/\/$/, "")}/tiles/effective?${params.toString()}`, { method: "GET", headers });
577
- return resp.json();
606
+ return getTilesFeature(store, { external_user_id, app_id, ttlSeconds });
578
607
  }
579
- async function enforce({ external_user_id, app_id = null }) {
608
+ // ---------------------------------------------------------
609
+ // Enforcement API (kept inline)
610
+ // ---------------------------------------------------------
611
+ async function enforce({ external_user_id, app_id = null, }) {
580
612
  assertInitialized();
581
- const adaptiveHeaders = getAdaptiveHeaders();
582
- const geoHeaders = await getGeoHeaders();
583
- const headers = buildHeaders({
613
+ const config = store.get();
614
+ const headers = store.buildHeaders({
584
615
  "X-External-User-Id": external_user_id,
585
- ...adaptiveHeaders,
586
- ...geoHeaders,
587
616
  });
588
- const resp = await httpRequest(`${cfg.baseUrl?.replace(/\/$/, "")}/sdk/test-enforcement`, {
617
+ const resp = await fetch(`${config.baseUrl?.replace(/\/$/, "")}/sdk/test-enforcement`, {
589
618
  method: "POST",
590
619
  headers,
591
620
  });
592
621
  const json = await resp.json().catch(() => ({}));
593
622
  return { decision: "allow", status: resp.status, data: json };
594
623
  }
624
+ // ---------------------------------------------------------
625
+ // Public API
626
+ // ---------------------------------------------------------
595
627
  return {
596
628
  init,
597
629
  identifyUser,
@@ -624,104 +656,6 @@
624
656
  return defaultClient.enforce(...args);
625
657
  }
626
658
 
627
- // -----------------------------------------------------------
628
- // Zubbl Telemetry Core (Endpoint Discovery)
629
- // -----------------------------------------------------------
630
- // Local telemetry buffer
631
- let sampleRatio = 0.1; // 1 in 10 by default
632
- let buffer = [];
633
- let debounceTimer = null;
634
- function shouldSample() {
635
- return Math.random() < sampleRatio;
636
- }
637
- // -----------------------------------------------------------
638
- // Emit telemetry events
639
- // -----------------------------------------------------------
640
- function emitEndpoint(payload) {
641
- if (!shouldSample())
642
- return;
643
- const enriched = {
644
- ...payload,
645
- ts: Date.now(),
646
- source: isBrowser ? "browser" : isNode ? "node" : "worker",
647
- };
648
- buffer.push(enriched);
649
- if (!debounceTimer) {
650
- debounceTimer = setTimeout(flushBuffer, 200);
651
- }
652
- }
653
- // -----------------------------------------------------------
654
- // Flush logic (batch send)
655
- // -----------------------------------------------------------
656
- async function flushBuffer() {
657
- if (buffer.length === 0) {
658
- debounceTimer = null;
659
- return;
660
- }
661
- const batch = [...buffer];
662
- buffer = [];
663
- debounceTimer = null;
664
- const config = getConfig();
665
- const url = `${config.baseUrl.replace(/\/$/, "")}/sdk/log`;
666
- const headers = {
667
- "Content-Type": "application/json",
668
- Authorization: `Bearer ${config.apiKey}`,
669
- "X-Tenant-Id": config.tenantId ?? "",
670
- "X-App-Id": config.appId ?? "",
671
- };
672
- try {
673
- const resp = await fetch(url, {
674
- method: "POST",
675
- headers,
676
- body: JSON.stringify(batch),
677
- });
678
- const text = await resp.text();
679
- console.log("[Zubbl SDK][Telemetry] Flushed", batch.length, "events →", resp.status, text);
680
- }
681
- catch (err) {
682
- console.warn("[Zubbl SDK][Telemetry] Failed to send batch:", err.message);
683
- }
684
- }
685
- // -----------------------------------------------------------
686
- // Helper for HTTP wrapping
687
- // -----------------------------------------------------------
688
- async function recordRequestTelemetry(url, method, status, latency, extra = {}) {
689
- try {
690
- const path = new URL(url).pathname;
691
- emitEndpoint({
692
- path,
693
- method,
694
- status,
695
- latency,
696
- ...extra,
697
- });
698
- }
699
- catch (err) {
700
- console.warn("[Zubbl SDK][Telemetry] Failed to record telemetry", err);
701
- }
702
- }
703
- // -----------------------------------------------------------
704
- // Runtime Endpoint Discovery
705
- // -----------------------------------------------------------
706
- let hasLoggedDiscovery = false;
707
- function logRuntimeDiscovery(url) {
708
- if (hasLoggedDiscovery)
709
- return; // only once per runtime
710
- hasLoggedDiscovery = true;
711
- const runtime = isNode ? "Node" : isBrowser ? "Browser" : isWorker ? "Worker" : "Unknown";
712
- const time = new Date().toISOString();
713
- console.log(`[Zubbl SDK][Discovery] Runtime: ${runtime}`);
714
- console.log(`[Zubbl SDK][Discovery] Timestamp: ${time}`);
715
- console.log(`[Zubbl SDK][Discovery] Initial endpoint: ${url}`);
716
- }
717
-
718
- var telemetry = /*#__PURE__*/Object.freeze({
719
- __proto__: null,
720
- emitEndpoint: emitEndpoint,
721
- logRuntimeDiscovery: logRuntimeDiscovery,
722
- recordRequestTelemetry: recordRequestTelemetry
723
- });
724
-
725
659
  exports.createZubblClient = createZubblClient;
726
660
  exports.enforce = enforce;
727
661
  exports.getTiles = getTiles;