webhanger 1.0.6 → 1.0.9
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/README.md +462 -321
- package/bin/cli.js +266 -71
- package/helper/authConfig.js +68 -0
- package/helper/bundler.js +3 -2
- package/helper/dbHandler.js +11 -4
- package/helper/oauthHandler.js +149 -0
- package/helper/personalization.js +138 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,34 +1,32 @@
|
|
|
1
1
|
# WebHanger
|
|
2
2
|
|
|
3
|
-
> **Component-as-a-Service (CaaS)** — Bundle once.
|
|
3
|
+
> **Component-as-a-Service (CaaS)** — Bundle once. AES-256 encrypt. Deploy to edge CDN. Load anywhere with zero code.
|
|
4
4
|
|
|
5
|
-
WebHanger is a secure, edge-delivered component distribution platform.
|
|
5
|
+
WebHanger is a secure, edge-delivered component distribution platform. Deploy encrypted UI components to a CDN and load them into any website or framework with a single tag — no tokens in HTML, no exposed secrets, no configuration.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
## Packages
|
|
10
11
|
|
|
11
12
|
| Package | Install | Description |
|
|
12
13
|
|---|---|---|
|
|
13
14
|
| `webhanger` | `npm install -g webhanger` | CLI + Node.js library |
|
|
14
|
-
| `webhanger-front` | `npm install webhanger-front` | Browser SDK |
|
|
15
|
+
| `webhanger-front` | `npm install webhanger-front` | Browser + ESM SDK |
|
|
16
|
+
| `webhanger-admin` | `npm install webhanger-admin` | Admin SDK + dashboard |
|
|
17
|
+
| `webhanger-auth` | `npm install webhanger-auth` | OAuth authentication SDK |
|
|
15
18
|
|
|
16
19
|
---
|
|
17
20
|
|
|
18
21
|
## Quick Start
|
|
19
22
|
|
|
20
23
|
```bash
|
|
21
|
-
# 1. Install CLI
|
|
22
24
|
npm install -g webhanger
|
|
23
|
-
|
|
24
|
-
# 2. Setup your project (provisions S3 + CloudFront automatically)
|
|
25
25
|
wh init
|
|
26
|
-
|
|
27
|
-
# 3. Deploy all components, build site, zip for upload — one command
|
|
28
|
-
wh ship ./components ./docs 1.0.0 ./dist
|
|
26
|
+
wh ship ./components ./site 1.0.0 ./dist
|
|
29
27
|
```
|
|
30
28
|
|
|
31
|
-
Load in any
|
|
29
|
+
Load in any HTML:
|
|
32
30
|
|
|
33
31
|
```html
|
|
34
32
|
<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
|
|
@@ -36,185 +34,109 @@ Load in any website — zero JS required:
|
|
|
36
34
|
|
|
37
35
|
<wh-component name="navbar"></wh-component>
|
|
38
36
|
<wh-component name="hero"></wh-component>
|
|
39
|
-
<wh-component name="footer"></wh-component>
|
|
37
|
+
<wh-component name="footer" sandbox></wh-component>
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## CLI Reference
|
|
45
|
-
|
|
46
|
-
### `wh init`
|
|
40
|
+
Load in Next.js / React / Vite:
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
```js
|
|
43
|
+
import { load } from "webhanger-front";
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
const manifest = await fetch("/wh-manifest.json").then(r => r.json());
|
|
46
|
+
const c = manifest.components["navbar"];
|
|
47
|
+
await load(c.urls || c.url, manifest.pid, c.token, c.expires, "#nav-mount");
|
|
52
48
|
```
|
|
53
49
|
|
|
54
|
-
Prompts:
|
|
55
|
-
- Project name
|
|
56
|
-
- Storage provider: `s3` | `r2` | `minio` | `local`
|
|
57
|
-
- Database provider: `firebase` | `supabase` | `mongodb`
|
|
58
|
-
- Credentials
|
|
59
|
-
|
|
60
|
-
For **S3**: automatically creates the bucket, configures CORS + versioning, spins up a CloudFront distribution. No AWS Console needed.
|
|
61
|
-
|
|
62
|
-
Optional: setup Cloudflare Edge Worker for token validation + geo routing at the edge.
|
|
63
|
-
|
|
64
|
-
Generates `webhanger.config.json`.
|
|
65
|
-
|
|
66
50
|
---
|
|
67
51
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Deploy + build + zip in one shot.
|
|
52
|
+
## CLI Reference
|
|
71
53
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```
|
|
54
|
+
### `wh init`
|
|
55
|
+
Interactive setup. Provisions S3 bucket + CloudFront automatically. Supports Firebase, Supabase, MongoDB. Optional Cloudflare Edge Worker setup.
|
|
75
56
|
|
|
57
|
+
### `wh ship` ⭐
|
|
58
|
+
Deploy + build + zip in one shot.
|
|
76
59
|
```bash
|
|
77
|
-
wh ship ./components ./
|
|
60
|
+
wh ship ./components ./site 1.0.0 ./dist
|
|
78
61
|
```
|
|
79
|
-
|
|
80
|
-
What it does:
|
|
81
|
-
1. Deploys all components (bundle → AES encrypt → upload → sign → register)
|
|
62
|
+
1. Deploys all components (bundle → AES-256 encrypt → upload → HMAC sign → register)
|
|
82
63
|
2. Resolves dependency graph
|
|
83
|
-
3. Writes `wh-manifest.json`
|
|
84
|
-
4. Production builds the site
|
|
85
|
-
5. Zips
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
🚀 [1/4] Deploying components...
|
|
89
|
-
navbar@1.0.0... ✅
|
|
90
|
-
hero@1.0.0... ✅
|
|
91
|
-
|
|
92
|
-
🔍 [2/4] Resolving dependency graph...
|
|
93
|
-
|
|
94
|
-
🏗️ [3/4] Building ./docs → ./dist...
|
|
95
|
-
index.html 4.2 kB
|
|
96
|
-
|
|
97
|
-
📦 [4/4] Zipping ./dist...
|
|
98
|
-
|
|
99
|
-
✅ Ship complete!
|
|
100
|
-
Deploy zip: ./deploy.zip (12.4 kB)
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Upload `deploy.zip` to Netlify, S3, cPanel, or any static host.
|
|
104
|
-
|
|
105
|
-
---
|
|
64
|
+
3. Writes `wh-manifest.json`
|
|
65
|
+
4. Production builds the site
|
|
66
|
+
5. Zips for upload
|
|
106
67
|
|
|
107
68
|
### `wh deploy`
|
|
108
|
-
|
|
109
|
-
Deploy a single component.
|
|
110
|
-
|
|
111
69
|
```bash
|
|
112
|
-
wh deploy <component-dir> <name> <version>
|
|
113
70
|
wh deploy ./components/navbar navbar 1.0.0
|
|
114
71
|
```
|
|
115
72
|
|
|
116
|
-
Prompts for token (auto-generate or custom) and expiry (never or seconds).
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
73
|
### `wh graph-deploy`
|
|
121
|
-
|
|
122
|
-
Deploy all components in a directory and resolve the dependency graph.
|
|
123
|
-
|
|
124
74
|
```bash
|
|
125
|
-
wh graph-deploy <components-dir> [version] [out-dir]
|
|
126
75
|
wh graph-deploy ./components 1.0.0 ./output
|
|
127
76
|
```
|
|
128
77
|
|
|
129
|
-
|
|
78
|
+
### `wh atomize`
|
|
79
|
+
Split a single HTML page into CDN-powered components.
|
|
80
|
+
```bash
|
|
81
|
+
wh atomize ./docs/index.html ./atomized 1.0.0
|
|
82
|
+
```
|
|
130
83
|
|
|
131
84
|
### `wh build`
|
|
132
|
-
|
|
133
|
-
Production build — minifies HTML, inlines local CSS/JS.
|
|
134
|
-
|
|
85
|
+
Production build — minifies HTML, extracts CSS/JS to hashed files.
|
|
135
86
|
```bash
|
|
136
|
-
wh build
|
|
137
|
-
wh build ./docs ./dist
|
|
87
|
+
wh build ./site ./dist
|
|
138
88
|
```
|
|
139
89
|
|
|
140
|
-
---
|
|
141
|
-
|
|
142
90
|
### `wh zip`
|
|
143
|
-
|
|
144
|
-
Zip a directory for deployment.
|
|
145
|
-
|
|
146
91
|
```bash
|
|
147
|
-
wh zip <src-dir> [out-file]
|
|
148
92
|
wh zip ./dist ./deploy.zip
|
|
149
93
|
```
|
|
150
94
|
|
|
151
|
-
---
|
|
152
|
-
|
|
153
95
|
### `wh analyze`
|
|
154
|
-
|
|
155
|
-
Detect framework, styling approach, and CDN dependencies automatically.
|
|
156
|
-
|
|
157
96
|
```bash
|
|
158
97
|
wh analyze ./components/navbar
|
|
159
98
|
```
|
|
160
99
|
|
|
161
|
-
```
|
|
162
|
-
🔍 Component Analysis
|
|
163
|
-
|
|
164
|
-
Framework : vanilla
|
|
165
|
-
Styling : tailwind
|
|
166
|
-
Imports : gsap, axios
|
|
167
|
-
CDN Assets:
|
|
168
|
-
[script] https://cdn.tailwindcss.com
|
|
169
|
-
[script] https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
100
|
### `wh convert`
|
|
175
|
-
|
|
176
|
-
Convert a vanilla HTML/CSS/JS component to any framework.
|
|
177
|
-
|
|
178
101
|
```bash
|
|
179
|
-
wh convert <dir> <name> <target> [out-dir]
|
|
180
102
|
wh convert ./components/navbar navbar react ./output
|
|
103
|
+
# targets: react | vue | svelte | next | angular | astro
|
|
181
104
|
```
|
|
182
105
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
106
|
+
### `wh breakdown`
|
|
107
|
+
Extract embedded CSS/JS from a single HTML file.
|
|
108
|
+
```bash
|
|
109
|
+
wh breakdown ./components/navbar
|
|
110
|
+
```
|
|
186
111
|
|
|
187
112
|
### `wh access`
|
|
188
|
-
|
|
189
|
-
Role-based access control for teams.
|
|
190
|
-
|
|
191
113
|
```bash
|
|
192
|
-
wh access grant
|
|
193
|
-
wh access revoke <key>
|
|
194
|
-
wh access list
|
|
114
|
+
wh access grant
|
|
115
|
+
wh access revoke <key>
|
|
116
|
+
wh access list
|
|
195
117
|
```
|
|
196
118
|
|
|
197
|
-
Roles:
|
|
198
|
-
|
|
199
|
-
| Role | deploy | read | delete | manage_access |
|
|
200
|
-
|---|---|---|---|---|
|
|
201
|
-
| owner | ✅ | ✅ | ✅ | ✅ |
|
|
202
|
-
| admin | ✅ | ✅ | ✅ | ✅ |
|
|
203
|
-
| deployer | ✅ | ✅ | ❌ | ❌ |
|
|
204
|
-
| viewer | ❌ | ✅ | ❌ | ❌ |
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
119
|
### `wh edge-init`
|
|
209
|
-
|
|
210
|
-
Setup Cloudflare Edge Worker for production-grade delivery.
|
|
211
|
-
|
|
212
120
|
```bash
|
|
213
121
|
wh edge-init
|
|
214
|
-
# then:
|
|
215
122
|
cd edge && wrangler deploy
|
|
216
123
|
```
|
|
217
124
|
|
|
125
|
+
### `wh auth init`
|
|
126
|
+
Interactive OAuth setup — Google, GitHub, Facebook.
|
|
127
|
+
```bash
|
|
128
|
+
wh auth init
|
|
129
|
+
```
|
|
130
|
+
Prompts for providers, base URL, client IDs/secrets. Generates `wh-auth.config.json` with callback URLs.
|
|
131
|
+
|
|
132
|
+
### `wh auth serve`
|
|
133
|
+
Start the OAuth callback server.
|
|
134
|
+
```bash
|
|
135
|
+
wh auth serve
|
|
136
|
+
wh auth serve 3001 # custom port
|
|
137
|
+
```
|
|
138
|
+
Handles OAuth redirects, token exchange, JWT issuance.
|
|
139
|
+
|
|
218
140
|
---
|
|
219
141
|
|
|
220
142
|
## Component Structure
|
|
@@ -222,64 +144,70 @@ cd edge && wrangler deploy
|
|
|
222
144
|
```
|
|
223
145
|
components/
|
|
224
146
|
navbar/
|
|
225
|
-
index.html
|
|
226
|
-
style.css
|
|
227
|
-
script.js
|
|
228
|
-
webhanger.component.json
|
|
147
|
+
index.html
|
|
148
|
+
style.css
|
|
149
|
+
script.js
|
|
150
|
+
webhanger.component.json
|
|
229
151
|
```
|
|
230
152
|
|
|
231
153
|
### `webhanger.component.json`
|
|
232
|
-
|
|
233
154
|
```json
|
|
234
155
|
{
|
|
235
156
|
"assets": [
|
|
236
|
-
{ "type": "script", "url": "https://cdn.tailwindcss.com" }
|
|
237
|
-
{ "type": "script", "url": "https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js" }
|
|
157
|
+
{ "type": "script", "url": "https://cdn.tailwindcss.com" }
|
|
238
158
|
],
|
|
239
|
-
"dependencies": ["
|
|
159
|
+
"dependencies": ["chart@1.0.0"],
|
|
160
|
+
"props": {
|
|
161
|
+
"brand": { "type": "string", "default": "MyApp" },
|
|
162
|
+
"ctaText": { "type": "string", "default": "Get Started" },
|
|
163
|
+
"ctaHref": { "type": "string", "default": "/signup" }
|
|
164
|
+
}
|
|
240
165
|
}
|
|
241
166
|
```
|
|
242
167
|
|
|
243
|
-
CDN assets are auto-detected by `wh analyze` and merged automatically on deploy.
|
|
244
|
-
|
|
245
168
|
---
|
|
246
169
|
|
|
247
|
-
##
|
|
170
|
+
## Component Props System
|
|
248
171
|
|
|
249
|
-
|
|
172
|
+
Pass dynamic data into components via HTML attributes — no redeployment needed.
|
|
250
173
|
|
|
174
|
+
### In your component HTML, use `{{wh.propName}}` placeholders:
|
|
175
|
+
```html
|
|
176
|
+
<!-- index.html -->
|
|
177
|
+
<nav>
|
|
178
|
+
<span class="brand">{{wh.brand}}</span>
|
|
179
|
+
<a href="{{wh.ctaHref}}" class="cta">{{wh.ctaText}}</a>
|
|
180
|
+
</nav>
|
|
251
181
|
```
|
|
252
|
-
dashboard@1.0.0
|
|
253
|
-
├── navbar@1.0.0
|
|
254
|
-
└── statsbar@1.0.0
|
|
255
|
-
└── chart@1.0.0
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
- Depth-first resolution
|
|
259
|
-
- Circular dependency detection with full chain error
|
|
260
|
-
- Deps load before parent component
|
|
261
|
-
- Stored in Firestore per project
|
|
262
182
|
|
|
263
|
-
|
|
264
|
-
|
|
183
|
+
### Pass props via attributes:
|
|
184
|
+
```html
|
|
185
|
+
<wh-component
|
|
186
|
+
name="navbar"
|
|
187
|
+
wh-brand="Acme Corp"
|
|
188
|
+
wh-cta-text="Sign Up Free"
|
|
189
|
+
wh-cta-href="/register">
|
|
190
|
+
</wh-component>
|
|
265
191
|
```
|
|
266
192
|
|
|
267
|
-
|
|
268
|
-
|
|
193
|
+
### Or programmatically:
|
|
269
194
|
```js
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
195
|
+
await load(url, pid, token, 0, "#mount", null, [], {
|
|
196
|
+
props: {
|
|
197
|
+
brand: "Acme Corp",
|
|
198
|
+
ctaText: "Sign Up Free",
|
|
199
|
+
ctaHref: "/register"
|
|
200
|
+
}
|
|
201
|
+
});
|
|
273
202
|
```
|
|
274
203
|
|
|
204
|
+
Props are resolved **after decryption** — the encrypted payload on CDN contains `{{wh.brand}}` literally. The SDK substitutes values in memory before DOM injection. Defaults from `webhanger.component.json` are used when no prop is passed.
|
|
205
|
+
|
|
275
206
|
---
|
|
276
207
|
|
|
277
208
|
## Security
|
|
278
209
|
|
|
279
210
|
### AES-256-GCM Encryption
|
|
280
|
-
|
|
281
|
-
Every component chunk (HTML/CSS/JS) is encrypted before upload.
|
|
282
|
-
|
|
283
211
|
```
|
|
284
212
|
Key = SHA-256(projectId + salt)
|
|
285
213
|
Payload = iv:tag:ciphertext (base64)
|
|
@@ -287,151 +215,349 @@ Salts = "::html" | "::css" | "::js"
|
|
|
287
215
|
```
|
|
288
216
|
|
|
289
217
|
### HMAC-SHA256 Signed URLs
|
|
290
|
-
|
|
291
218
|
```
|
|
292
|
-
token = HMAC-SHA256(projectId:
|
|
219
|
+
token = HMAC-SHA256(projectId:path:expires, secretKey)
|
|
293
220
|
```
|
|
294
221
|
|
|
295
222
|
### Integrity Check
|
|
296
|
-
|
|
297
|
-
SHA-256 hash of raw content stored in payload. Verified after decryption — detects tampering.
|
|
223
|
+
SHA-256 hash verified after decryption — detects tampering.
|
|
298
224
|
|
|
299
225
|
### Domain Restriction
|
|
300
|
-
|
|
301
226
|
```js
|
|
302
|
-
|
|
303
|
-
allowedDomains: ["mysite.com"
|
|
227
|
+
load(url, pid, token, 0, "[data-wh]", null, [], {
|
|
228
|
+
allowedDomains: ["mysite.com"]
|
|
304
229
|
});
|
|
305
230
|
```
|
|
306
231
|
|
|
307
|
-
###
|
|
232
|
+
### Manifest-based Delivery
|
|
233
|
+
Tokens, projectId, CDN URLs never in HTML. Fetched at runtime from `wh-manifest.json`.
|
|
308
234
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Dependency Graph
|
|
312
238
|
|
|
313
|
-
|
|
314
|
-
|
|
239
|
+
```
|
|
240
|
+
dashboard@1.0.0
|
|
241
|
+
├── navbar@1.0.0
|
|
242
|
+
└── statsbar@1.0.0
|
|
243
|
+
└── chart@1.0.0
|
|
315
244
|
```
|
|
316
245
|
|
|
317
|
-
|
|
246
|
+
```js
|
|
247
|
+
import { resolveGraph } from "webhanger";
|
|
248
|
+
const graph = await resolveGraph(config.db, projectId, "dashboard", "1.0.0");
|
|
249
|
+
// Returns: [chart, navbar, statsbar, dashboard]
|
|
250
|
+
```
|
|
318
251
|
|
|
319
|
-
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Multi-CDN Failover
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"cdn": {
|
|
259
|
+
"url": "https://primary.cloudfront.net",
|
|
260
|
+
"fallbacks": ["https://fallback.r2.dev"]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
320
264
|
|
|
321
265
|
---
|
|
322
266
|
|
|
323
267
|
## Edge Worker (Cloudflare Workers)
|
|
324
268
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
-
|
|
328
|
-
- Version resolution (`latest` → `1.2.0` from KV)
|
|
329
|
-
- Geo-based routing (India → ap-south-1, Europe → eu-west-1)
|
|
269
|
+
- HMAC token validation at edge
|
|
270
|
+
- Version resolution (`latest` → `1.2.0`)
|
|
271
|
+
- Geo-based routing
|
|
330
272
|
- Rate limiting (100 req/min per IP)
|
|
331
273
|
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## OAuth Authentication (webhanger-auth)
|
|
277
|
+
|
|
278
|
+
Drop-in OAuth for any website. Supports Google, GitHub, Facebook. Zero backend code required beyond `wh auth serve`.
|
|
279
|
+
|
|
280
|
+
### Setup
|
|
281
|
+
|
|
332
282
|
```bash
|
|
333
|
-
|
|
334
|
-
|
|
283
|
+
npm install -g webhanger # CLI
|
|
284
|
+
npm install webhanger-auth # browser SDK
|
|
285
|
+
|
|
286
|
+
wh auth init # configure providers
|
|
287
|
+
wh auth serve # start OAuth server (default port 3001)
|
|
335
288
|
```
|
|
336
289
|
|
|
337
|
-
|
|
290
|
+
### Zero-code HTML
|
|
338
291
|
|
|
339
|
-
|
|
292
|
+
```html
|
|
293
|
+
<script src="https://unpkg.com/webhanger-auth/browser.min.js"></script>
|
|
340
294
|
|
|
341
|
-
|
|
295
|
+
<!-- Renders a styled OAuth button, handles the full flow -->
|
|
296
|
+
<wh-auth
|
|
297
|
+
provider="google"
|
|
298
|
+
on-success="/dashboard?name={{name}}&email={{email}}"
|
|
299
|
+
on-error="/login?error={{message}}"
|
|
300
|
+
theme="dark">
|
|
301
|
+
</wh-auth>
|
|
302
|
+
|
|
303
|
+
<wh-auth provider="github" on-success="/dashboard"></wh-auth>
|
|
304
|
+
<wh-auth provider="facebook" on-success="/dashboard"></wh-auth>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Available `on-success` template variables: `{{name}}` `{{email}}` `{{avatar}}` `{{provider}}` `{{id}}`
|
|
308
|
+
|
|
309
|
+
### Programmatic
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
// Login
|
|
313
|
+
WHAuth.login("google", {
|
|
314
|
+
onSuccess: (user) => {
|
|
315
|
+
console.log(user.email, user.name, user.avatar, user.provider);
|
|
316
|
+
redirect("/dashboard");
|
|
317
|
+
},
|
|
318
|
+
onError: (err) => showError(err.message)
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Check session
|
|
322
|
+
WHAuth.isLoggedIn(); // true/false
|
|
323
|
+
WHAuth.getUser(); // { email, name, avatar, provider, id }
|
|
324
|
+
WHAuth.getToken(); // JWT string
|
|
325
|
+
|
|
326
|
+
// Logout
|
|
327
|
+
WHAuth.logout();
|
|
328
|
+
|
|
329
|
+
// Events
|
|
330
|
+
WHAuth.on("success", ({ user, token }) => analytics.track("login", user));
|
|
331
|
+
WHAuth.on("error", ({ message }) => errorTracker.capture(message));
|
|
332
|
+
WHAuth.on("logout", () => clearSession());
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### `wh-auth.config.json` (generated by `wh auth init`)
|
|
342
336
|
|
|
343
337
|
```json
|
|
344
338
|
{
|
|
345
|
-
"
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
339
|
+
"baseUrl": "https://myapp.com",
|
|
340
|
+
"callbackPath": "/auth/callback",
|
|
341
|
+
"port": 3001,
|
|
342
|
+
"providers": {
|
|
343
|
+
"google": {
|
|
344
|
+
"clientId": "...",
|
|
345
|
+
"clientSecret": "...",
|
|
346
|
+
"callbackUrl": "https://myapp.com/auth/callback/google"
|
|
347
|
+
},
|
|
348
|
+
"github": {
|
|
349
|
+
"clientId": "...",
|
|
350
|
+
"clientSecret": "...",
|
|
351
|
+
"callbackUrl": "https://myapp.com/auth/callback/github"
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
"session": {
|
|
355
|
+
"secret": "auto-generated",
|
|
356
|
+
"expiresIn": "7d"
|
|
351
357
|
}
|
|
352
358
|
}
|
|
353
359
|
```
|
|
354
360
|
|
|
355
|
-
|
|
361
|
+
### Verify JWT server-side
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
import { verifyAuthToken } from "webhanger-auth";
|
|
365
|
+
|
|
366
|
+
const user = await verifyAuthToken(req.headers.authorization?.split(" ")[1]);
|
|
367
|
+
// { email, name, avatar, provider, id, exp, iat }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Auth flow
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
User clicks <wh-auth provider="google">
|
|
374
|
+
└── popup opens → /auth/google
|
|
375
|
+
└── redirect to Google OAuth consent
|
|
376
|
+
└── Google → /auth/callback/google?code=xxx
|
|
377
|
+
└── exchange code → fetch profile
|
|
378
|
+
└── issue JWT
|
|
379
|
+
└── postMessage to opener
|
|
380
|
+
└── WHAuth.on("success") fires
|
|
381
|
+
└── redirect to on-success URL
|
|
382
|
+
```
|
|
356
383
|
|
|
357
384
|
---
|
|
358
385
|
|
|
359
|
-
|
|
386
|
+
A local web UI + SDK for managing deployed components.
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
npm install webhanger-admin
|
|
390
|
+
|
|
391
|
+
# Start dashboard
|
|
392
|
+
npx wh-admin ./webhanger.config.json 5000
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Open `http://localhost:5000`
|
|
360
396
|
|
|
361
|
-
|
|
397
|
+
Or use programmatically:
|
|
362
398
|
|
|
399
|
+
```js
|
|
400
|
+
import { WebHangerAdmin } from "webhanger-admin";
|
|
401
|
+
|
|
402
|
+
const admin = new WebHangerAdmin("./webhanger.config.json");
|
|
403
|
+
|
|
404
|
+
const components = await admin.listComponents();
|
|
405
|
+
const manifest = await admin.generateManifest();
|
|
406
|
+
await admin.saveManifest("./public/wh-manifest.json");
|
|
407
|
+
|
|
408
|
+
const { apiKey } = await admin.grantAccess("deployer", "CI/CD");
|
|
409
|
+
await admin.resignComponent("navbar", "1.0.0", 86400);
|
|
410
|
+
await admin.deleteComponent("navbar", "1.0.0");
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
See `webhanger-admin/README.md` for the full API reference.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Browser SDK
|
|
418
|
+
|
|
419
|
+
### Zero-code Custom Element
|
|
363
420
|
```html
|
|
364
421
|
<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
|
|
365
422
|
<script>WebHangerFront.initialize("./wh-manifest.json");</script>
|
|
366
423
|
|
|
367
|
-
<wh-component name="navbar"></wh-component>
|
|
368
|
-
<wh-component name="hero"></wh-component>
|
|
424
|
+
<wh-component name="navbar" wh-brand="MyApp" wh-cta-text="Start Free"></wh-component>
|
|
369
425
|
<wh-component name="footer" sandbox></wh-component>
|
|
370
426
|
```
|
|
371
427
|
|
|
372
|
-
###
|
|
373
|
-
|
|
428
|
+
### ESM (Next.js / React / Vite)
|
|
374
429
|
```js
|
|
375
|
-
|
|
376
|
-
cdnUrl, // string or array (multi-CDN)
|
|
377
|
-
projectId, // decrypt key
|
|
378
|
-
token, // HMAC token
|
|
379
|
-
expires, // unix timestamp, 0 = never
|
|
380
|
-
selector, // CSS selector, default "[data-wh]"
|
|
381
|
-
onSignal, // optional signal callback
|
|
382
|
-
deps, // optional pre-resolved deps array
|
|
383
|
-
options // optional options object
|
|
384
|
-
);
|
|
430
|
+
import { load, use, on, registerSW, clearCache, metrics, gpu } from "webhanger-front";
|
|
385
431
|
```
|
|
386
432
|
|
|
387
|
-
###
|
|
388
|
-
|
|
433
|
+
### Manual load with props
|
|
389
434
|
```js
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
await load(
|
|
436
|
+
cdnUrl,
|
|
437
|
+
projectId,
|
|
438
|
+
token,
|
|
439
|
+
expires,
|
|
440
|
+
selector,
|
|
441
|
+
onSignal,
|
|
442
|
+
deps,
|
|
443
|
+
{
|
|
444
|
+
props: { brand: "MyApp", ctaText: "Get Started" },
|
|
445
|
+
sandbox: true,
|
|
446
|
+
allowedDomains: ["mysite.com"],
|
|
447
|
+
beforeMount: () => showSpinner(),
|
|
448
|
+
afterMount: () => hideSpinner(),
|
|
449
|
+
onError: (err) => showFallback(err)
|
|
450
|
+
}
|
|
451
|
+
);
|
|
397
452
|
```
|
|
398
453
|
|
|
399
454
|
### Signal callback
|
|
400
|
-
|
|
401
455
|
```js
|
|
402
|
-
|
|
456
|
+
load(url, pid, token, 0, "[data-wh]", ({ stage, time, source }) => {
|
|
403
457
|
// stages: start → fetching → assets → deps → injecting → done | error
|
|
404
|
-
console.log(stage, detail.time, detail.source);
|
|
405
458
|
});
|
|
406
459
|
```
|
|
407
460
|
|
|
408
461
|
### Plugin system
|
|
409
|
-
|
|
410
462
|
```js
|
|
411
|
-
|
|
412
|
-
install({ on
|
|
463
|
+
use({
|
|
464
|
+
install({ on }) {
|
|
413
465
|
on("load", ({ time, source }) => analytics.track("load", { time, source }));
|
|
414
466
|
on("error", ({ message }) => errorTracker.capture(message));
|
|
415
467
|
on("metric", ({ name, value }) => dashboard.update(name, value));
|
|
468
|
+
on("gpu", ({ supported }) => console.log("WebGPU:", supported));
|
|
469
|
+
on("sw", ({ scope }) => console.log("SW:", scope));
|
|
416
470
|
}
|
|
417
471
|
});
|
|
418
472
|
```
|
|
419
473
|
|
|
420
474
|
### Observability
|
|
475
|
+
```js
|
|
476
|
+
on("load", ({ time, source }) => console.log(time, source));
|
|
477
|
+
console.log(metrics); // { loads, cacheHits, errors, totalTime }
|
|
478
|
+
```
|
|
421
479
|
|
|
480
|
+
### WebGPU
|
|
422
481
|
```js
|
|
423
|
-
|
|
424
|
-
|
|
482
|
+
console.log(gpu.supported);
|
|
483
|
+
on("gpu", ({ supported }) => console.log("GPU:", supported));
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Offline + Service Worker
|
|
487
|
+
```js
|
|
488
|
+
await registerSW("./webhanger.sw.js");
|
|
425
489
|
|
|
426
|
-
|
|
427
|
-
|
|
490
|
+
await setOfflinePage(
|
|
491
|
+
"<h1>Offline</h1><p>Back soon.</p>",
|
|
492
|
+
"body { background: #030712; color: white; }"
|
|
493
|
+
);
|
|
428
494
|
```
|
|
429
495
|
|
|
430
|
-
|
|
496
|
+
Offline behavior:
|
|
497
|
+
- Online first visit → loads from CDN, caches everything
|
|
498
|
+
- Online repeat visit → loads from SW cache instantly, badge shows
|
|
499
|
+
- Offline with cache → full page works
|
|
500
|
+
- Offline no cache → custom offline page with "⬡ Served by WebHanger" badge
|
|
501
|
+
|
|
502
|
+
### Smart Cache Invalidation
|
|
503
|
+
|
|
504
|
+
Only re-fetches from CDN when components actually changed. Checks admin server on every page load — if nothing changed, loads instantly from cache.
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
// Instead of initialize(), use smartInitialize()
|
|
508
|
+
WebHangerFront.smartInitialize(
|
|
509
|
+
"./wh-manifest.json",
|
|
510
|
+
"http://localhost:5000" // wh-admin server URL
|
|
511
|
+
);
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Flow:
|
|
515
|
+
```
|
|
516
|
+
Page loads
|
|
517
|
+
└── GET /api/last-updated from admin server (2s timeout)
|
|
518
|
+
├── same as localStorage "wh_last_updated"
|
|
519
|
+
│ └── load all components from cache (instant, 0ms)
|
|
520
|
+
└── different (components updated since last visit)
|
|
521
|
+
└── clear component cache
|
|
522
|
+
└── reload from CDN
|
|
523
|
+
└── update localStorage timestamp
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Events:
|
|
527
|
+
```js
|
|
528
|
+
WebHangerFront.on("cache-invalidated", ({ serverTs, cachedTs }) => {
|
|
529
|
+
console.log("Components updated — reloading from CDN");
|
|
530
|
+
});
|
|
531
|
+
WebHangerFront.on("cache-hit", ({ serverTs }) => {
|
|
532
|
+
console.log("Up to date — loaded from cache");
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
If admin server is unreachable (offline, not running), falls back to normal cache behavior silently — zero errors.
|
|
537
|
+
|
|
538
|
+
Test it:
|
|
539
|
+
```bash
|
|
540
|
+
# 1. Deploy a component
|
|
541
|
+
node smart-cache-test/deploy.js 1.0.0
|
|
542
|
+
|
|
543
|
+
# 2. Start admin server
|
|
544
|
+
node admin/server.js ./webhanger.config.json 5000
|
|
545
|
+
|
|
546
|
+
# 3. Serve the test page
|
|
547
|
+
npx serve smart-cache-test/site
|
|
548
|
+
|
|
549
|
+
# 4. Open http://localhost:3000 — loads from CDN, caches timestamp
|
|
550
|
+
# 5. Refresh — loads from cache instantly (banner: "✓ Up to date")
|
|
551
|
+
# 6. Redeploy with new version
|
|
552
|
+
node smart-cache-test/deploy.js 2.0.0
|
|
553
|
+
# 7. Refresh — cache invalidates, loads fresh (banner: "🔄 Components updated")
|
|
554
|
+
```
|
|
431
555
|
|
|
556
|
+
See `smart-cache-test/` for the full working demo.
|
|
557
|
+
|
|
558
|
+
### Hard flush
|
|
432
559
|
```js
|
|
433
|
-
await
|
|
434
|
-
// Clears: localStorage, IndexedDB, SW caches, sessionStorage, unregisters SW
|
|
560
|
+
await clearCache();
|
|
435
561
|
```
|
|
436
562
|
|
|
437
563
|
---
|
|
@@ -442,9 +568,23 @@ await WebHangerFront.clearCache();
|
|
|
442
568
|
|---|---|
|
|
443
569
|
| `localStorage` | Components < 50KB |
|
|
444
570
|
| `IndexedDB` | Components ≥ 50KB |
|
|
445
|
-
| Service Worker | Offline
|
|
571
|
+
| Service Worker | Offline + navigation cache |
|
|
572
|
+
| `wh_last_updated` | Smart cache invalidation timestamp |
|
|
573
|
+
|
|
574
|
+
**Stale-while-revalidate** — returns cached instantly, refreshes in background.
|
|
575
|
+
|
|
576
|
+
**Smart cache invalidation** — `smartInitialize()` checks admin server on every load. Only re-fetches from CDN when components actually changed. Zero CDN requests on cache hits.
|
|
446
577
|
|
|
447
|
-
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Access Control
|
|
581
|
+
|
|
582
|
+
| Role | deploy | read | delete | manage_access |
|
|
583
|
+
|---|---|---|---|---|
|
|
584
|
+
| owner | ✅ | ✅ | ✅ | ✅ |
|
|
585
|
+
| admin | ✅ | ✅ | ✅ | ✅ |
|
|
586
|
+
| deployer | ✅ | ✅ | ❌ | ❌ |
|
|
587
|
+
| viewer | ❌ | ✅ | ❌ | ❌ |
|
|
448
588
|
|
|
449
589
|
---
|
|
450
590
|
|
|
@@ -454,44 +594,82 @@ await WebHangerFront.clearCache();
|
|
|
454
594
|
import { WebHanger } from "webhanger";
|
|
455
595
|
const wh = new WebHanger();
|
|
456
596
|
|
|
457
|
-
// Deploy
|
|
458
597
|
const result = await wh.deploy("./components/navbar", "navbar", "1.0.0", {
|
|
459
|
-
expiresInSeconds: 86400,
|
|
460
|
-
token: "custom-token", // optional
|
|
598
|
+
expiresInSeconds: 86400,
|
|
461
599
|
dependencies: ["chart@1.0.0"]
|
|
462
600
|
});
|
|
463
|
-
// { cdnUrl, cdnUrls, token, expires, dependencies }
|
|
464
601
|
|
|
465
|
-
// Resolve
|
|
466
602
|
const comp = await wh.resolve("navbar", "1.0.0");
|
|
467
|
-
|
|
468
|
-
// Rotate token without redeploying
|
|
469
603
|
await wh.resign("navbar", "1.0.0", { expiresInSeconds: 3600 });
|
|
470
|
-
|
|
471
|
-
// Delete from storage
|
|
472
604
|
await wh.remove("navbar", "1.0.0");
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
---
|
|
473
608
|
|
|
474
|
-
|
|
475
|
-
|
|
609
|
+
## Next.js Integration
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
npm install webhanger-front
|
|
476
613
|
```
|
|
477
614
|
|
|
478
|
-
|
|
615
|
+
```tsx
|
|
616
|
+
"use client";
|
|
617
|
+
import { useEffect, useRef } from "react";
|
|
618
|
+
import { load } from "webhanger-front";
|
|
479
619
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
620
|
+
export default function WebHangerComponent({ name, props = {} }: { name: string; props?: Record<string, string> }) {
|
|
621
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
622
|
+
|
|
623
|
+
useEffect(() => {
|
|
624
|
+
fetch("/wh-manifest.json")
|
|
625
|
+
.then(r => r.json())
|
|
626
|
+
.then(m => {
|
|
627
|
+
const c = m.components[name];
|
|
628
|
+
if (!c || !ref.current) return;
|
|
629
|
+
ref.current.id = `wh-${name}`;
|
|
630
|
+
load(c.urls || c.url, m.pid, c.token, c.expires, `#wh-${name}`, null, [], { props });
|
|
631
|
+
});
|
|
632
|
+
}, [name]);
|
|
633
|
+
|
|
634
|
+
return <div ref={ref} />;
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
// app/page.tsx
|
|
640
|
+
<WebHangerComponent name="navbar" props={{ brand: "MyApp", ctaText: "Sign Up" }} />
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Examples & Tests
|
|
646
|
+
|
|
647
|
+
| Folder | What it tests |
|
|
648
|
+
|---|---|
|
|
649
|
+
| `examples/` | Full component deploy + browser SDK demo |
|
|
650
|
+
| `showcase/` | All 4 packages together |
|
|
651
|
+
| `auth-test/` | OAuth login flow (Google, GitHub) |
|
|
652
|
+
| `smart-cache-test/` | Smart cache invalidation demo |
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
# Examples
|
|
656
|
+
node examples/deploy.js && npx serve examples/site
|
|
657
|
+
|
|
658
|
+
# Showcase (all packages)
|
|
659
|
+
node showcase/deploy.js && npx serve showcase/site
|
|
660
|
+
|
|
661
|
+
# Auth test
|
|
662
|
+
wh auth init && wh auth serve &
|
|
663
|
+
npx serve . # open /auth-test/login.html
|
|
664
|
+
|
|
665
|
+
# Smart cache test
|
|
666
|
+
node smart-cache-test/deploy.js 1.0.0
|
|
667
|
+
node admin/server.js ./webhanger.config.json 5000 &
|
|
668
|
+
npx serve smart-cache-test/site
|
|
493
669
|
```
|
|
494
670
|
|
|
671
|
+
See `examples/EXAMPLE.md` for the full step-by-step guide.
|
|
672
|
+
|
|
495
673
|
---
|
|
496
674
|
|
|
497
675
|
## Storage Providers
|
|
@@ -500,47 +678,16 @@ import {
|
|
|
500
678
|
|---|---|
|
|
501
679
|
| `s3` | AWS S3 — auto-provisions bucket + CloudFront |
|
|
502
680
|
| `r2` | Cloudflare R2 — zero egress fees |
|
|
503
|
-
| `minio` | Self-hosted MinIO
|
|
504
|
-
| `local` | Local disk — dev
|
|
681
|
+
| `minio` | Self-hosted MinIO |
|
|
682
|
+
| `local` | Local disk — dev only |
|
|
505
683
|
|
|
506
684
|
## Database Providers
|
|
507
685
|
|
|
508
686
|
| Provider | Notes |
|
|
509
687
|
|---|---|
|
|
510
|
-
| `firebase` | Firebase Firestore — free tier
|
|
511
|
-
| `supabase` | Supabase Postgres
|
|
512
|
-
| `mongodb` | MongoDB Atlas
|
|
513
|
-
|
|
514
|
-
---
|
|
515
|
-
|
|
516
|
-
## `webhanger.config.json`
|
|
517
|
-
|
|
518
|
-
```json
|
|
519
|
-
{
|
|
520
|
-
"project": "my-app",
|
|
521
|
-
"projectId": "wh_1234567890",
|
|
522
|
-
"secretKey": "64-char-hex-secret",
|
|
523
|
-
"webHangerVersion": "1.0.0",
|
|
524
|
-
"storage": {
|
|
525
|
-
"provider": "s3",
|
|
526
|
-
"accessKey": "...",
|
|
527
|
-
"secretKey": "...",
|
|
528
|
-
"bucket": "my-bucket",
|
|
529
|
-
"region": "ap-south-1",
|
|
530
|
-
"distributionId": "EXXXXX"
|
|
531
|
-
},
|
|
532
|
-
"cdn": {
|
|
533
|
-
"url": "https://primary.cloudfront.net",
|
|
534
|
-
"fallbacks": ["https://fallback.r2.dev"]
|
|
535
|
-
},
|
|
536
|
-
"db": {
|
|
537
|
-
"provider": "firebase",
|
|
538
|
-
"serviceAccountPath": "./firebase-service-account.json"
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
> Keep this file private. Never commit it. Add to `.gitignore`.
|
|
688
|
+
| `firebase` | Firebase Firestore — free tier |
|
|
689
|
+
| `supabase` | Supabase Postgres |
|
|
690
|
+
| `mongodb` | MongoDB Atlas |
|
|
544
691
|
|
|
545
692
|
---
|
|
546
693
|
|
|
@@ -548,47 +695,41 @@ import {
|
|
|
548
695
|
|
|
549
696
|
```
|
|
550
697
|
Developer
|
|
551
|
-
└── wh ship ./components ./
|
|
698
|
+
└── wh ship ./components ./site
|
|
552
699
|
├── wh analyze → detect Tailwind, GSAP, deps
|
|
553
|
-
├── wh
|
|
554
|
-
├──
|
|
700
|
+
├── wh breakdown → extract CSS/JS from single HTML
|
|
701
|
+
├── bundle → html + css + js → single payload
|
|
702
|
+
├── props schema → stored in payload for runtime resolution
|
|
703
|
+
├── AES-256-GCM → encrypt each chunk
|
|
555
704
|
├── SHA-256 hash → integrity fingerprint
|
|
556
705
|
├── S3 upload → store encrypted payload
|
|
557
706
|
├── HMAC sign → project-scoped signed URL
|
|
558
|
-
├──
|
|
559
|
-
├── wh build → minify HTML,
|
|
707
|
+
├── DB register → metadata + dep graph
|
|
708
|
+
├── wh build → minify HTML, extract CSS/JS
|
|
560
709
|
└── wh zip → deploy.zip ready for upload
|
|
561
710
|
|
|
562
711
|
Browser
|
|
563
|
-
└── <wh-component name="navbar">
|
|
712
|
+
└── <wh-component name="navbar" wh-brand="MyApp">
|
|
564
713
|
├── fetch wh-manifest.json
|
|
565
714
|
├── check token expiry
|
|
566
715
|
├── stale-while-revalidate cache
|
|
567
|
-
├── fetch from CloudFront
|
|
568
|
-
|
|
569
|
-
├──
|
|
570
|
-
├──
|
|
571
|
-
├── resolve + load dependency graph
|
|
716
|
+
├── fetch from CloudFront / Edge Worker
|
|
717
|
+
├── multi-CDN failover
|
|
718
|
+
├── load CDN assets
|
|
719
|
+
├── resolve dependency graph
|
|
572
720
|
├── AES-256-GCM decrypt in memory
|
|
573
721
|
├── SHA-256 integrity verify
|
|
722
|
+
├── resolve props ({{wh.brand}} → "MyApp")
|
|
574
723
|
├── domain restriction check
|
|
575
724
|
├── inject CSS → HTML → JS (or Shadow DOM)
|
|
576
725
|
├── fire lifecycle hooks
|
|
577
|
-
|
|
726
|
+
├── emit metrics + plugin events
|
|
727
|
+
├── WebGPU detection
|
|
728
|
+
└── Service Worker caches for offline
|
|
578
729
|
```
|
|
579
730
|
|
|
580
731
|
---
|
|
581
732
|
|
|
582
|
-
## Real-World Use Cases
|
|
583
|
-
|
|
584
|
-
- **Enterprise micro-frontends** — shared UI across 50+ apps, update once
|
|
585
|
-
- **Education platforms** — push UI updates to all school sites instantly
|
|
586
|
-
- **Low-bandwidth regions** — cache once, serve offline via Service Worker
|
|
587
|
-
- **Security platforms** — inject warnings/banners dynamically across sites
|
|
588
|
-
- **White-label SaaS** — per-tenant component customization
|
|
589
|
-
|
|
590
|
-
---
|
|
591
|
-
|
|
592
733
|
## License
|
|
593
734
|
|
|
594
735
|
ISC
|