zubbl-sdk 1.1.15 → 1.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/core/config.d.ts +14 -0
- package/dist/types/core/context.d.ts +4 -0
- package/dist/types/core/geo.d.ts +13 -0
- package/dist/types/core/http.d.ts +4 -0
- package/dist/types/core/signing.d.ts +3 -0
- package/dist/types/core/storage.d.ts +6 -0
- package/dist/types/core/telemetry.d.ts +4 -0
- package/dist/types/features/getTiles.d.ts +23 -0
- package/dist/types/features/identifyUser.d.ts +11 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/zubbl-sdk.cjs.js +690 -224
- package/dist/zubbl-sdk.cjs.js.map +1 -0
- package/dist/zubbl-sdk.esm.js +688 -222
- package/dist/zubbl-sdk.esm.js.map +1 -0
- package/dist/zubbl-sdk.umd.js +698 -232
- package/dist/zubbl-sdk.umd.js.map +1 -0
- package/package.json +7 -3
package/dist/zubbl-sdk.umd.js
CHANGED
|
@@ -1,256 +1,722 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
})(this, (function (exports,
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('crypto')) :
|
|
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';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
7
|
+
// src/core/config.ts
|
|
8
|
+
// -----------------------------------------------------------
|
|
9
|
+
// Universal environment detection + SDK configuration
|
|
10
|
+
// -----------------------------------------------------------
|
|
11
|
+
const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
12
|
+
const isNode = typeof process !== "undefined" &&
|
|
13
|
+
!!process.versions?.node &&
|
|
14
|
+
!isBrowser;
|
|
15
|
+
const isWorker = typeof self !== "undefined" &&
|
|
16
|
+
typeof self.importScripts === "function" &&
|
|
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) {
|
|
30
|
+
if (isBrowser || isWorker) {
|
|
31
|
+
const key = config.apiKey ?? "";
|
|
32
|
+
// Simple rule: if the key looks like a secret (sk_ or very long)
|
|
33
|
+
const looksSecret = key.startsWith("sk_") || key.length > 40;
|
|
34
|
+
if (looksSecret) {
|
|
35
|
+
throw new Error("[Zubbl SDK] Secret or server key detected in browser environment. " +
|
|
36
|
+
"Use a public SDK key instead (prefix pk_).");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
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,
|
|
63
|
+
};
|
|
38
64
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function flushEndpointBuffer() {
|
|
42
|
-
if (endpointBuffer.length === 0) {
|
|
43
|
-
debounceTimer = null;
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const batch = [...endpointBuffer];
|
|
47
|
-
endpointBuffer = [];
|
|
48
|
-
debounceTimer = null;
|
|
49
|
-
|
|
50
|
-
const telemetryUrl =
|
|
51
|
-
process.env.ZUBBL_TELEMETRY_URL || `${config.baseUrl.replace(/\/$/, "")}/sdk/log`;
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(),
|
|
58
77
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
95
|
+
}
|
|
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";
|
|
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);
|
|
87
185
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
tenant_id: context.tenant_id || config.tenantId,
|
|
100
|
-
app_id: context.app_id || config.appId,
|
|
101
|
-
external_user_id: context.external_user_id,
|
|
102
|
-
method: init.method || "GET",
|
|
103
|
-
path: url.pathname, // fixed to "path"
|
|
104
|
-
status: response?.status ?? 0,
|
|
105
|
-
latency_ms,
|
|
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);
|
|
106
197
|
});
|
|
107
|
-
} catch (e) {
|
|
108
|
-
console.warn("[Zubbl SDK] Failed to record endpoint telemetry", e);
|
|
109
|
-
}
|
|
110
198
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
apiKey,
|
|
129
|
-
tenantId,
|
|
130
|
-
appId,
|
|
131
|
-
baseUrl: baseUrl || "https://api.zubbl.com/api",
|
|
132
|
-
injectWorkerHeaders,
|
|
133
|
-
workerSecret,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Identify a user by email (and optional name).
|
|
139
|
-
* Backend will return or generate a valid UUID for external_user_id.
|
|
140
|
-
*/
|
|
141
|
-
async function identifyUser({ email, name }) {
|
|
142
|
-
if (!config.apiKey || !config.tenantId || !config.appId) {
|
|
143
|
-
throw new Error("Zubbl SDK not initialized");
|
|
144
|
-
}
|
|
145
|
-
if (!email) throw new Error("email is required");
|
|
146
|
-
|
|
147
|
-
const headers = {
|
|
148
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
149
|
-
"X-Tenant-Id": config.tenantId,
|
|
150
|
-
"X-App-Id": config.appId,
|
|
151
|
-
"Content-Type": "application/json",
|
|
152
|
-
};
|
|
153
|
-
if (config.injectWorkerHeaders && config.workerSecret) {
|
|
154
|
-
headers["X-Zubbl-Worker-Secret"] = config.workerSecret;
|
|
155
|
-
headers["X-Zubbl-Internal-Call"] = "true";
|
|
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");
|
|
156
216
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
};
|
|
167
236
|
}
|
|
168
237
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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,
|
|
285
|
+
};
|
|
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
|
+
}
|
|
173
324
|
}
|
|
174
325
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
326
|
+
// src/core/storage.ts
|
|
327
|
+
// ------------------------------------------------------
|
|
328
|
+
// Node (in-memory) implementation
|
|
329
|
+
// ------------------------------------------------------
|
|
330
|
+
class NodeStorage {
|
|
331
|
+
constructor() {
|
|
332
|
+
this.store = new Map();
|
|
333
|
+
}
|
|
334
|
+
get(key) {
|
|
335
|
+
const entry = this.store.get(key);
|
|
336
|
+
if (!entry)
|
|
337
|
+
return null;
|
|
338
|
+
if (entry.expiresAt && entry.expiresAt < Date.now()) {
|
|
339
|
+
this.store.delete(key);
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
return entry.value;
|
|
343
|
+
}
|
|
344
|
+
set(key, value, ttlSeconds) {
|
|
345
|
+
const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined;
|
|
346
|
+
this.store.set(key, { value, expiresAt });
|
|
347
|
+
}
|
|
348
|
+
remove(key) {
|
|
349
|
+
this.store.delete(key);
|
|
350
|
+
}
|
|
192
351
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
352
|
+
// ------------------------------------------------------
|
|
353
|
+
// Browser (localStorage) implementation
|
|
354
|
+
// ------------------------------------------------------
|
|
355
|
+
class BrowserStorage {
|
|
356
|
+
constructor() {
|
|
357
|
+
this.prefix = "zubbl_sdk_";
|
|
358
|
+
}
|
|
359
|
+
get(key) {
|
|
360
|
+
try {
|
|
361
|
+
const raw = localStorage.getItem(this.prefix + key);
|
|
362
|
+
if (!raw)
|
|
363
|
+
return null;
|
|
364
|
+
const { value, expiresAt } = JSON.parse(raw);
|
|
365
|
+
if (expiresAt && expiresAt < Date.now()) {
|
|
366
|
+
localStorage.removeItem(this.prefix + key);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
set(key, value, ttlSeconds) {
|
|
376
|
+
try {
|
|
377
|
+
const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined;
|
|
378
|
+
localStorage.setItem(this.prefix + key, JSON.stringify({ value, expiresAt }));
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
// localStorage quota exceeded → silently ignore
|
|
382
|
+
console.warn("[Zubbl SDK][Storage] Failed to persist item:", err);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
remove(key) {
|
|
386
|
+
localStorage.removeItem(this.prefix + key);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// ------------------------------------------------------
|
|
390
|
+
// Factory
|
|
391
|
+
// ------------------------------------------------------
|
|
392
|
+
function createStorage() {
|
|
393
|
+
if (isNode)
|
|
394
|
+
return new NodeStorage();
|
|
395
|
+
if (isBrowser)
|
|
396
|
+
return new BrowserStorage();
|
|
397
|
+
// Fallback: safe no-op storage for tests / fake environments
|
|
398
|
+
return {
|
|
399
|
+
get: () => null,
|
|
400
|
+
set: () => { },
|
|
401
|
+
remove: () => { },
|
|
402
|
+
};
|
|
203
403
|
}
|
|
204
404
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
405
|
+
// -----------------------------------------------------------
|
|
406
|
+
// 🌍 Zubbl Geo Engine — Multi-Provider Fallback + 15min Cache
|
|
407
|
+
// -----------------------------------------------------------
|
|
408
|
+
// Fully typed, aligns with Zubbl GEO Enforcement Policy
|
|
409
|
+
// -----------------------------------------------------------
|
|
410
|
+
const GEO_CACHE_KEY = "zubbl-geo-cache";
|
|
411
|
+
const GEO_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|
412
|
+
// -----------------------------------------------------------
|
|
413
|
+
// 🗃 Cache Helpers
|
|
414
|
+
// -----------------------------------------------------------
|
|
415
|
+
function loadGeoCache() {
|
|
416
|
+
try {
|
|
417
|
+
if (!isBrowser || typeof localStorage === "undefined")
|
|
418
|
+
return null;
|
|
419
|
+
const raw = localStorage.getItem(GEO_CACHE_KEY);
|
|
420
|
+
if (!raw)
|
|
421
|
+
return null;
|
|
422
|
+
const parsed = JSON.parse(raw);
|
|
423
|
+
if (!parsed.data?.ip || Date.now() - parsed.timestamp > GEO_TTL_MS)
|
|
424
|
+
return null;
|
|
425
|
+
return parsed.data;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function saveGeoCache(data) {
|
|
432
|
+
try {
|
|
433
|
+
if (!isBrowser || typeof localStorage === "undefined")
|
|
434
|
+
return;
|
|
435
|
+
localStorage.setItem(GEO_CACHE_KEY, JSON.stringify({ data, timestamp: Date.now() }));
|
|
436
|
+
}
|
|
437
|
+
catch { }
|
|
438
|
+
}
|
|
439
|
+
// -----------------------------------------------------------
|
|
440
|
+
// 🌐 Provider Wrappers
|
|
441
|
+
// -----------------------------------------------------------
|
|
442
|
+
async function fetchJSON(url, timeoutMs = 3000) {
|
|
443
|
+
const controller = new AbortController();
|
|
444
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
445
|
+
try {
|
|
446
|
+
const resp = await fetch(url, { signal: controller.signal });
|
|
447
|
+
clearTimeout(timeout);
|
|
448
|
+
if (!resp.ok)
|
|
449
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
450
|
+
return await resp.json();
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
clearTimeout(timeout);
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function providerIpInfo() {
|
|
458
|
+
const d = await fetchJSON("https://ipinfo.io/json?token=public");
|
|
459
|
+
return {
|
|
460
|
+
ip: d.ip || null,
|
|
461
|
+
country: d.country || "unknown",
|
|
462
|
+
asn: d.org?.replace(/^AS/, "") || null,
|
|
463
|
+
privacy: { vpn: false, hosting: false },
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async function providerIpApi() {
|
|
467
|
+
const d = await fetchJSON("https://ipapi.co/json/");
|
|
468
|
+
return {
|
|
469
|
+
ip: d.ip || null,
|
|
470
|
+
country: d.country_code || "unknown",
|
|
471
|
+
asn: d.asn?.replace(/^AS/, "") || null,
|
|
472
|
+
privacy: { vpn: false, hosting: false },
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function providerGeoJs() {
|
|
476
|
+
const d = await fetchJSON("https://get.geojs.io/v1/ip/geo.json");
|
|
477
|
+
return {
|
|
478
|
+
ip: d.ip || null,
|
|
479
|
+
country: d.country_code || "unknown",
|
|
480
|
+
asn: null,
|
|
481
|
+
privacy: { vpn: false, hosting: false },
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
// -----------------------------------------------------------
|
|
485
|
+
// 🧠 Fallback Resolver
|
|
486
|
+
// -----------------------------------------------------------
|
|
487
|
+
async function getGeoInfo() {
|
|
488
|
+
const cached = loadGeoCache();
|
|
489
|
+
if (cached)
|
|
490
|
+
return cached;
|
|
491
|
+
const providers = [providerIpInfo, providerIpApi, providerGeoJs];
|
|
492
|
+
for (const fn of providers) {
|
|
493
|
+
try {
|
|
494
|
+
const result = await fn();
|
|
495
|
+
if (result?.country && result.country !== "unknown") {
|
|
496
|
+
saveGeoCache(result);
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
console.warn("[Zubbl SDK][Geo] Provider failed:", fn.name, "-", err.message);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const fallback = {
|
|
505
|
+
ip: null,
|
|
506
|
+
country: "unknown",
|
|
507
|
+
asn: null,
|
|
508
|
+
privacy: { vpn: false, hosting: false },
|
|
509
|
+
};
|
|
510
|
+
saveGeoCache(fallback);
|
|
511
|
+
return fallback;
|
|
512
|
+
}
|
|
513
|
+
async function getGeoHeaders() {
|
|
514
|
+
const geo = await getGeoInfo();
|
|
515
|
+
return {
|
|
516
|
+
"X-Zubbl-Geo-Country": geo.country || "unknown",
|
|
517
|
+
"X-Zubbl-ASN": geo.asn || "unknown",
|
|
518
|
+
"X-Zubbl-IP": geo.ip || "",
|
|
519
|
+
"X-Zubbl-Privacy-VPN": String(geo.privacy?.vpn ?? false),
|
|
520
|
+
"X-Zubbl-Privacy-Hosting": String(geo.privacy?.hosting ?? false),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
214
523
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
534
|
+
try {
|
|
535
|
+
const geo = await getGeoInfo();
|
|
536
|
+
console.log(`[Zubbl SDK] 🌍 GEO detected: ${geo.country} (ASN ${geo.asn || "?"})`);
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
console.warn("[Zubbl SDK] GEO init failed:", err?.message);
|
|
540
|
+
}
|
|
218
541
|
}
|
|
219
|
-
|
|
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");
|
|
549
|
+
}
|
|
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");
|
|
566
|
+
}
|
|
567
|
+
return data;
|
|
568
|
+
}
|
|
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");
|
|
576
|
+
}
|
|
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
|
+
}
|
|
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");
|
|
598
|
+
}
|
|
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
|
+
}
|
|
610
|
+
// ---------------------------------------------------------
|
|
611
|
+
// Exposed Helpers
|
|
612
|
+
// ---------------------------------------------------------
|
|
613
|
+
const storage = createStorage();
|
|
220
614
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
615
|
+
// -----------------------------------------------------------
|
|
616
|
+
// Zubbl Telemetry Core (Endpoint Discovery)
|
|
617
|
+
// -----------------------------------------------------------
|
|
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;
|
|
624
|
+
}
|
|
625
|
+
// -----------------------------------------------------------
|
|
626
|
+
// Emit telemetry events
|
|
627
|
+
// -----------------------------------------------------------
|
|
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",
|
|
635
|
+
};
|
|
636
|
+
buffer.push(enriched);
|
|
637
|
+
if (!debounceTimer) {
|
|
638
|
+
debounceTimer = setTimeout(flushBuffer, 200);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// -----------------------------------------------------------
|
|
642
|
+
// Flush logic (batch send)
|
|
643
|
+
// -----------------------------------------------------------
|
|
644
|
+
async function flushBuffer() {
|
|
645
|
+
if (buffer.length === 0) {
|
|
646
|
+
debounceTimer = null;
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
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
|
+
};
|
|
660
|
+
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);
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
console.warn("[Zubbl SDK][Telemetry] Failed to send batch:", err.message);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// -----------------------------------------------------------
|
|
674
|
+
// Helper for HTTP wrapping
|
|
675
|
+
// -----------------------------------------------------------
|
|
676
|
+
async function recordRequestTelemetry(url, method, status, latency, extra = {}) {
|
|
677
|
+
try {
|
|
678
|
+
const path = new URL(url).pathname;
|
|
679
|
+
emitEndpoint({
|
|
680
|
+
path,
|
|
681
|
+
method,
|
|
682
|
+
status,
|
|
683
|
+
latency,
|
|
684
|
+
...extra,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
console.warn("[Zubbl SDK][Telemetry] Failed to record telemetry", err);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
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}`);
|
|
231
704
|
}
|
|
232
705
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return { decision: "allow", status: resp.status, data: resp.data };
|
|
240
|
-
} catch (e) {
|
|
241
|
-
if (e.response && e.response.status === 429) {
|
|
242
|
-
return { decision: "block", status: 429, data: e.response.data };
|
|
243
|
-
}
|
|
244
|
-
throw e;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
706
|
+
var telemetry = /*#__PURE__*/Object.freeze({
|
|
707
|
+
__proto__: null,
|
|
708
|
+
emitEndpoint: emitEndpoint,
|
|
709
|
+
logRuntimeDiscovery: logRuntimeDiscovery,
|
|
710
|
+
recordRequestTelemetry: recordRequestTelemetry
|
|
711
|
+
});
|
|
247
712
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
713
|
+
exports.enforce = enforce;
|
|
714
|
+
exports.getConfig = getConfig;
|
|
715
|
+
exports.getTiles = getTiles;
|
|
716
|
+
exports.httpRequest = httpRequest;
|
|
717
|
+
exports.identifyUser = identifyUser;
|
|
718
|
+
exports.init = init;
|
|
719
|
+
exports.storage = storage;
|
|
255
720
|
|
|
256
721
|
}));
|
|
722
|
+
//# sourceMappingURL=zubbl-sdk.umd.js.map
|