webhanger-front 1.0.0

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.
Files changed (3) hide show
  1. package/browser.js +140 -0
  2. package/index.js +165 -0
  3. package/package.json +11 -0
package/browser.js ADDED
@@ -0,0 +1,140 @@
1
+ // WebHanger Frontend SDK — IIFE browser build
2
+ // Use via <script src="browser.js"> or from a CDN
3
+
4
+ (function (global) {
5
+ const VERSION = "1.0.0";
6
+ const IDB_NAME = "webhanger_cache";
7
+ const IDB_STORE = "components";
8
+ const LS_PREFIX = "wh_";
9
+ const SIZE_THRESHOLD = 50 * 1024;
10
+
11
+ function xorDecode(str, key) {
12
+ let out = "";
13
+ for (let i = 0; i < str.length; i++) {
14
+ out += String.fromCharCode(str.charCodeAt(i) ^ key.charCodeAt(i % key.length));
15
+ }
16
+ return out;
17
+ }
18
+
19
+ function decrypt(b64, projectId, salt) {
20
+ if (!b64) return "";
21
+ return xorDecode(atob(b64), projectId + salt);
22
+ }
23
+
24
+ function openIDB() {
25
+ return new Promise((resolve, reject) => {
26
+ const req = indexedDB.open(IDB_NAME, 1);
27
+ req.onupgradeneeded = (e) => e.target.result.createObjectStore(IDB_STORE);
28
+ req.onsuccess = (e) => resolve(e.target.result);
29
+ req.onerror = () => reject(req.error);
30
+ });
31
+ }
32
+
33
+ async function idbGet(key) {
34
+ const db = await openIDB();
35
+ return new Promise((resolve, reject) => {
36
+ const tx = db.transaction(IDB_STORE, "readonly");
37
+ const req = tx.objectStore(IDB_STORE).get(key);
38
+ req.onsuccess = () => resolve(req.result);
39
+ req.onerror = () => reject(req.error);
40
+ });
41
+ }
42
+
43
+ async function idbSet(key, value) {
44
+ const db = await openIDB();
45
+ return new Promise((resolve, reject) => {
46
+ const tx = db.transaction(IDB_STORE, "readwrite");
47
+ const req = tx.objectStore(IDB_STORE).put(value, key);
48
+ req.onsuccess = () => resolve();
49
+ req.onerror = () => reject(req.error);
50
+ });
51
+ }
52
+
53
+ async function cacheGet(key) {
54
+ try {
55
+ const ls = localStorage.getItem(LS_PREFIX + key);
56
+ if (ls) return ls;
57
+ } catch (_) {}
58
+ return await idbGet(key);
59
+ }
60
+
61
+ async function cacheSet(key, value) {
62
+ try {
63
+ if (value.length < SIZE_THRESHOLD) {
64
+ localStorage.setItem(LS_PREFIX + key, value);
65
+ return;
66
+ }
67
+ } catch (_) {}
68
+ await idbSet(key, value);
69
+ }
70
+
71
+ async function fetchComponent(cdnUrl, token, expires) {
72
+ const url = expires
73
+ ? `${cdnUrl}?token=${token}&expires=${expires}`
74
+ : `${cdnUrl}?token=${token}`;
75
+ const res = await fetch(url);
76
+ if (!res.ok) throw new Error(`Failed to fetch component: ${res.status}`);
77
+ return await res.text();
78
+ }
79
+
80
+ function injectComponent(encryptedPayload, projectId, targetSelector) {
81
+ const target = document.querySelector(targetSelector || "[data-wh]");
82
+ if (!target) { console.warn("[WebHanger] No mount target found."); return; }
83
+
84
+ let payload;
85
+ try { payload = JSON.parse(encryptedPayload); }
86
+ catch (_) { console.error("[WebHanger] Invalid component payload."); return; }
87
+
88
+ const css = decrypt(payload.c, projectId, "::css");
89
+ if (css) {
90
+ const style = document.createElement("style");
91
+ style.textContent = css;
92
+ document.head.appendChild(style);
93
+ }
94
+
95
+ const html = decrypt(payload.h, projectId, "::html");
96
+ if (html) target.innerHTML = html;
97
+
98
+ const js = decrypt(payload.j, projectId, "::js");
99
+ if (js) {
100
+ const script = document.createElement("script");
101
+ script.textContent = js;
102
+ document.head.appendChild(script);
103
+ document.head.removeChild(script);
104
+ }
105
+ }
106
+
107
+ async function load(cdnUrl, projectId, token, expires, selector = "[data-wh]") {
108
+ if (!cdnUrl || !projectId || !token || expires === undefined || expires === null) {
109
+ console.error("[WebHanger] Missing required params: cdnUrl, projectId, token, expires");
110
+ return;
111
+ }
112
+ if (expires !== 0 && Math.floor(Date.now() / 1000) > expires) {
113
+ console.warn("[WebHanger] Token expired.");
114
+ return;
115
+ }
116
+ const cacheKey = `${cdnUrl}@${expires}`;
117
+ try {
118
+ let payload = await cacheGet(cacheKey);
119
+ if (!payload) {
120
+ payload = await fetchComponent(cdnUrl, token, expires);
121
+ await cacheSet(cacheKey, payload);
122
+ }
123
+ injectComponent(payload, projectId, selector);
124
+ } catch (err) {
125
+ console.error("[WebHanger] Load failed:", err.message);
126
+ }
127
+ }
128
+
129
+ async function clearCache() {
130
+ Object.keys(localStorage)
131
+ .filter(k => k.startsWith(LS_PREFIX))
132
+ .forEach(k => localStorage.removeItem(k));
133
+ const db = await openIDB();
134
+ const tx = db.transaction(IDB_STORE, "readwrite");
135
+ tx.objectStore(IDB_STORE).clear();
136
+ }
137
+
138
+ global.WebHangerFront = { load, clearCache, version: VERSION };
139
+
140
+ })(window);
package/index.js ADDED
@@ -0,0 +1,165 @@
1
+ // WebHanger Frontend SDK — ESM
2
+ // Use with bundlers (Vite, Webpack, Rollup) or native ESM
3
+
4
+ const VERSION = "1.0.0";
5
+ const IDB_NAME = "webhanger_cache";
6
+ const IDB_STORE = "components";
7
+ const LS_PREFIX = "wh_";
8
+ const SIZE_THRESHOLD = 50 * 1024;
9
+
10
+ // ─── Decrypt ─────────────────────────────────────────────────────────────────
11
+
12
+ function xorDecode(str, key) {
13
+ let out = "";
14
+ for (let i = 0; i < str.length; i++) {
15
+ out += String.fromCharCode(str.charCodeAt(i) ^ key.charCodeAt(i % key.length));
16
+ }
17
+ return out;
18
+ }
19
+
20
+ function decrypt(b64, projectId, salt) {
21
+ if (!b64) return "";
22
+ return xorDecode(atob(b64), projectId + salt);
23
+ }
24
+
25
+ // ─── IndexedDB ────────────────────────────────────────────────────────────────
26
+
27
+ function openIDB() {
28
+ return new Promise((resolve, reject) => {
29
+ const req = indexedDB.open(IDB_NAME, 1);
30
+ req.onupgradeneeded = (e) => e.target.result.createObjectStore(IDB_STORE);
31
+ req.onsuccess = (e) => resolve(e.target.result);
32
+ req.onerror = () => reject(req.error);
33
+ });
34
+ }
35
+
36
+ async function idbGet(key) {
37
+ const db = await openIDB();
38
+ return new Promise((resolve, reject) => {
39
+ const tx = db.transaction(IDB_STORE, "readonly");
40
+ const req = tx.objectStore(IDB_STORE).get(key);
41
+ req.onsuccess = () => resolve(req.result);
42
+ req.onerror = () => reject(req.error);
43
+ });
44
+ }
45
+
46
+ async function idbSet(key, value) {
47
+ const db = await openIDB();
48
+ return new Promise((resolve, reject) => {
49
+ const tx = db.transaction(IDB_STORE, "readwrite");
50
+ const req = tx.objectStore(IDB_STORE).put(value, key);
51
+ req.onsuccess = () => resolve();
52
+ req.onerror = () => reject(req.error);
53
+ });
54
+ }
55
+
56
+ // ─── Cache ────────────────────────────────────────────────────────────────────
57
+
58
+ async function cacheGet(key) {
59
+ try {
60
+ const ls = localStorage.getItem(LS_PREFIX + key);
61
+ if (ls) return ls;
62
+ } catch (_) {}
63
+ return await idbGet(key);
64
+ }
65
+
66
+ async function cacheSet(key, value) {
67
+ try {
68
+ if (value.length < SIZE_THRESHOLD) {
69
+ localStorage.setItem(LS_PREFIX + key, value);
70
+ return;
71
+ }
72
+ } catch (_) {}
73
+ await idbSet(key, value);
74
+ }
75
+
76
+ // ─── Fetch ────────────────────────────────────────────────────────────────────
77
+
78
+ async function fetchComponent(cdnUrl, token, expires) {
79
+ const url = expires
80
+ ? `${cdnUrl}?token=${token}&expires=${expires}`
81
+ : `${cdnUrl}?token=${token}`;
82
+ const res = await fetch(url);
83
+ if (!res.ok) throw new Error(`Failed to fetch component: ${res.status}`);
84
+ return await res.text();
85
+ }
86
+
87
+ // ─── Inject ───────────────────────────────────────────────────────────────────
88
+
89
+ function injectComponent(encryptedPayload, projectId, targetSelector) {
90
+ const target = document.querySelector(targetSelector || "[data-wh]");
91
+ if (!target) { console.warn("[WebHanger] No mount target found."); return; }
92
+
93
+ let payload;
94
+ try { payload = JSON.parse(encryptedPayload); }
95
+ catch (_) { console.error("[WebHanger] Invalid component payload."); return; }
96
+
97
+ const css = decrypt(payload.c, projectId, "::css");
98
+ if (css) {
99
+ const style = document.createElement("style");
100
+ style.textContent = css;
101
+ document.head.appendChild(style);
102
+ }
103
+
104
+ const html = decrypt(payload.h, projectId, "::html");
105
+ if (html) target.innerHTML = html;
106
+
107
+ const js = decrypt(payload.j, projectId, "::js");
108
+ if (js) {
109
+ const script = document.createElement("script");
110
+ script.textContent = js;
111
+ document.head.appendChild(script);
112
+ document.head.removeChild(script);
113
+ }
114
+ }
115
+
116
+ // ─── Public API ───────────────────────────────────────────────────────────────
117
+
118
+ /**
119
+ * Load a WebHanger component into the DOM.
120
+ *
121
+ * @param {string} cdnUrl - Full CDN URL of the component
122
+ * @param {string} projectId - Your WebHanger project ID (decrypt key)
123
+ * @param {string} token - HMAC signed token
124
+ * @param {number} expires - Unix timestamp expiry (0 = never)
125
+ * @param {string} [selector] - CSS selector for mount target (default: [data-wh])
126
+ */
127
+ export async function load(cdnUrl, projectId, token, expires, selector = "[data-wh]") {
128
+ if (!cdnUrl || !projectId || !token || expires === undefined || expires === null) {
129
+ console.error("[WebHanger] Missing required params: cdnUrl, projectId, token, expires");
130
+ return;
131
+ }
132
+
133
+ if (expires !== 0 && Math.floor(Date.now() / 1000) > expires) {
134
+ console.warn("[WebHanger] Token expired.");
135
+ return;
136
+ }
137
+
138
+ const cacheKey = `${cdnUrl}@${expires}`;
139
+
140
+ try {
141
+ let payload = await cacheGet(cacheKey);
142
+ if (!payload) {
143
+ payload = await fetchComponent(cdnUrl, token, expires);
144
+ await cacheSet(cacheKey, payload);
145
+ }
146
+ injectComponent(payload, projectId, selector);
147
+ } catch (err) {
148
+ console.error("[WebHanger] Load failed:", err.message);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Clear cached components from localStorage + IndexedDB.
154
+ */
155
+ export async function clearCache() {
156
+ Object.keys(localStorage)
157
+ .filter(k => k.startsWith(LS_PREFIX))
158
+ .forEach(k => localStorage.removeItem(k));
159
+
160
+ const db = await openIDB();
161
+ const tx = db.transaction(IDB_STORE, "readwrite");
162
+ tx.objectStore(IDB_STORE).clear();
163
+ }
164
+
165
+ export const version = VERSION;
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "webhanger-front",
3
+ "version": "1.0.0",
4
+ "description": "WebHanger browser SDK — load encrypted UI components from CDN",
5
+ "main": "index.js",
6
+ "module": "index.js",
7
+ "browser": "browser.js",
8
+ "type": "module",
9
+ "keywords": ["webhanger", "cdn", "components", "frontend", "sdk"],
10
+ "license": "ISC"
11
+ }