webhanger
v1.1.1
Published
Component-as-a-Service platform — bundle, sign, and deliver UI components via edge CDN
Readme
WebHanger
Component-as-a-Service (CaaS) — Bundle once. AES-256 encrypt. Deploy to edge CDN. Load anywhere with zero code.
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.
Packages
| Package | Install | Description |
|---|---|---|
| webhanger | npm install -g webhanger | CLI + Node.js library |
| webhanger-front | npm install webhanger-front | Browser + ESM SDK |
| webhanger-admin | npm install webhanger-admin | Admin SDK + dashboard |
| webhanger-auth | npm install webhanger-auth | OAuth authentication SDK |
Quick Start
npm install -g webhanger
wh init
wh ship ./components ./site 1.0.0 ./distLoad in any HTML:
<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
<script>WebHangerFront.initialize("./wh-manifest.json");</script>
<wh-component name="navbar"></wh-component>
<wh-component name="hero"></wh-component>
<wh-component name="footer" sandbox></wh-component>Load in Next.js / React / Vite:
import { load } from "webhanger-front";
const manifest = await fetch("/wh-manifest.json").then(r => r.json());
const c = manifest.components["navbar"];
await load(c.urls || c.url, manifest.pid, c.token, c.expires, "#nav-mount");CLI Reference
wh init
Interactive setup. Provisions S3 bucket + CloudFront automatically. Supports Firebase, Supabase, MongoDB. Optional Cloudflare Edge Worker setup.
wh dev ⭐ Development server
Local dev server with hot reload, live preview, and manifest auto-refresh.
wh dev ./components 4242Edit any component file → auto-deploys in 300ms → browser hot-reloads. Open http://localhost:4242 for live preview with a dev status bar at the bottom.
# Custom port + manifest output
wh dev ./components 3000 ./public/wh-manifest.jsonwh ship ⭐
Deploy + build + zip in one shot.
wh ship ./components ./site 1.0.0 ./dist- Deploys all components (bundle → AES-256 encrypt → upload → HMAC sign → register)
- Resolves dependency graph
- Writes
wh-manifest.json - Production builds the site
- Zips for upload
wh deploy
wh deploy ./components/navbar navbar 1.0.0wh graph-deploy
wh graph-deploy ./components 1.0.0 ./outputwh atomize
Split a single HTML page into CDN-powered components.
wh atomize ./docs/index.html ./atomized 1.0.0wh build
Production build — minifies HTML, extracts CSS/JS to hashed files.
wh build ./site ./distwh zip
wh zip ./dist ./deploy.zipwh analyze
wh analyze ./components/navbarwh convert
wh convert ./components/navbar navbar react ./output
# targets: react | vue | svelte | next | angular | astrowh breakdown
Extract embedded CSS/JS from a single HTML file.
wh breakdown ./components/navbarwh access
wh access grant
wh access revoke <key>
wh access listwh edge-init
wh edge-init
cd edge && wrangler deploywh auth init
Interactive OAuth setup — Google, GitHub, Facebook.
wh auth initPrompts for providers, base URL, client IDs/secrets. Generates wh-auth.config.json with callback URLs.
wh auth serve
Start the OAuth callback server.
wh auth serve
wh auth serve 3001 # custom portHandles OAuth redirects, token exchange, JWT issuance.
Component Structure
components/
navbar/
index.html
style.css
script.js
webhanger.component.jsonwebhanger.component.json
{
"assets": [
{ "type": "script", "url": "https://cdn.tailwindcss.com" }
],
"dependencies": ["[email protected]"],
"props": {
"brand": { "type": "string", "default": "MyApp" },
"ctaText": { "type": "string", "default": "Get Started" },
"ctaHref": { "type": "string", "default": "/signup" }
}
}Component Props System
Pass dynamic data into components via HTML attributes — no redeployment needed.
In your component HTML, use {{wh.propName}} placeholders:
<!-- index.html -->
<nav>
<span class="brand">{{wh.brand}}</span>
<a href="{{wh.ctaHref}}" class="cta">{{wh.ctaText}}</a>
</nav>Pass props via attributes:
<wh-component
name="navbar"
wh-brand="Acme Corp"
wh-cta-text="Sign Up Free"
wh-cta-href="/register">
</wh-component>Or programmatically:
await load(url, pid, token, 0, "#mount", null, [], {
props: {
brand: "Acme Corp",
ctaText: "Sign Up Free",
ctaHref: "/register"
}
});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.
Security
AES-256-GCM Encryption
Key = SHA-256(projectId + salt)
Payload = iv:tag:ciphertext (base64)
Salts = "::html" | "::css" | "::js"HMAC-SHA256 Signed URLs
token = HMAC-SHA256(projectId:path:expires, secretKey)Integrity Check
SHA-256 hash verified after decryption — detects tampering.
Domain Restriction
load(url, pid, token, 0, "[data-wh]", null, [], {
allowedDomains: ["mysite.com"]
});Manifest-based Delivery
Tokens, projectId, CDN URLs never in HTML. Fetched at runtime from wh-manifest.json.
Dependency Graph
[email protected]
├── [email protected]
└── [email protected]
└── [email protected]import { resolveGraph } from "webhanger";
const graph = await resolveGraph(config.db, projectId, "dashboard", "1.0.0");
// Returns: [chart, navbar, statsbar, dashboard]Multi-CDN Failover
{
"cdn": {
"url": "https://primary.cloudfront.net",
"fallbacks": ["https://fallback.r2.dev"]
}
}Edge Worker (Cloudflare Workers)
- HMAC token validation at edge
- Version resolution (
latest→1.2.0) - Geo-based routing
- Rate limiting (100 req/min per IP)
Edge Personalization
Serve different component variants to different users based on rules — country, device, role, A/B test, subscription plan — all resolved client-side or at the Cloudflare edge.
Setup
# Scaffold rules config
wh personalize init
# Test rule resolution (slot, config, country, device, role)
wh personalize test ./wh-personalization.json IN mobile premium
# Output:
# Context: country=IN device=mobile role=premium
# Resolved: hero → hero-india (first matching rule: country=IN)wh-personalization.json
Key rule: The slot key (e.g.
"hero") must exactly match thenameattribute on<wh-component name="hero">. The SDK uses the component name as the slot lookup key.
{
"hero": {
"rules": [
{ "if": { "country": "IN" }, "component": "hero-india" },
{ "if": { "country": "US" }, "component": "hero-us" },
{ "if": { "device": "mobile" }, "component": "hero-mobile" },
{ "if": { "role": "premium" }, "component": "hero-premium" },
{ "if": { "abTest": "variant-b" }, "component": "hero-variant-b"},
{ "if": { "hour": { "min": 9, "max": 17 } }, "component": "hero-business" }
],
"default": "hero"
},
"navbar": {
"rules": [
{ "if": { "role": "admin" }, "component": "navbar-admin" },
{ "if": { "plan": "premium" }, "component": "navbar-premium" }
],
"default": "navbar"
}
}Zero-code usage
<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
<script>
// Load rules first, then initialize
WebHangerFront.loadPersonalization("./wh-personalization.json");
WebHangerFront.initialize("./wh-manifest.json");
</script>
<!-- personalize attribute enables rule resolution -->
<wh-component name="hero" personalize></wh-component>
<wh-component name="navbar" personalize></wh-component>Programmatic
import { load, loadPersonalization } from "webhanger-front";
await loadPersonalization("./wh-personalization.json");
// Override context for testing
window._wh_ctx_override = { country: "IN", role: "premium" };
// Component resolves to hero-india (first matching rule)
await load(manifest.components["hero"].url, pid, token, 0, "#hero");Rule conditions
| Condition | Source | Example |
|---|---|---|
| country | window._wh_ctx_override or IP (edge) | "IN", ["IN","PK"] |
| device | User-Agent | "mobile", "tablet", "desktop" |
| role | JWT from webhanger-auth | "premium", "admin" |
| abTest | localStorage bucket (stable per user) | "variant-a", "variant-b" |
| lang | navigator.language | "en", "hi" |
| hour | time of day | { "min": 9, "max": 17 } |
| plan | localStorage wh_plan | "free", "premium" |
Rules are evaluated top-to-bottom — first match wins. Falls back to default if no rule matches.
Events
WebHangerFront.on("personalized", ({ slot, resolved, ctx }) => {
console.log(`${slot} → ${resolved} (country: ${ctx.country})`);
});A/B Testing
Each user is automatically assigned a stable bucket (variant-a or variant-b) stored in localStorage. 50/50 split by default.
// Check current bucket
console.log(localStorage.getItem("wh_ab_bucket")); // "variant-a" or "variant-b"Edge resolution (Cloudflare Workers)
When using wh edge-init, the worker resolves personalization server-side using Cloudflare headers — no client-side JS needed:
CF-IPCountry: IN → worker resolves hero-india → serves encrypted componentStore rules in KV:
wrangler kv:key put --binding=WH_VERSIONS "personalization:hero" '{"rules":[...],"default":"hero"}'Test it
node personalization-test/deploy.js
npx serve personalization-test/siteOpen http://localhost:3000 — use the simulator buttons to switch country, device, role, and A/B bucket. Watch the hero component change in real time.
Drop-in OAuth for any website. Supports Google, GitHub, Facebook. Zero backend code required beyond wh auth serve.
Setup
npm install -g webhanger # CLI
npm install webhanger-auth # browser SDK
wh auth init # configure providers
wh auth serve # start OAuth server (default port 3001)Zero-code HTML
<script src="https://unpkg.com/webhanger-auth/browser.min.js"></script>
<!-- Renders a styled OAuth button, handles the full flow -->
<wh-auth
provider="google"
on-success="/dashboard?name={{name}}&email={{email}}"
on-error="/login?error={{message}}"
theme="dark">
</wh-auth>
<wh-auth provider="github" on-success="/dashboard"></wh-auth>
<wh-auth provider="facebook" on-success="/dashboard"></wh-auth>Available on-success template variables: {{name}} {{email}} {{avatar}} {{provider}} {{id}}
Programmatic
// Login
WHAuth.login("google", {
onSuccess: (user) => {
console.log(user.email, user.name, user.avatar, user.provider);
redirect("/dashboard");
},
onError: (err) => showError(err.message)
});
// Check session
WHAuth.isLoggedIn(); // true/false
WHAuth.getUser(); // { email, name, avatar, provider, id }
WHAuth.getToken(); // JWT string
// Logout
WHAuth.logout();
// Events
WHAuth.on("success", ({ user, token }) => analytics.track("login", user));
WHAuth.on("error", ({ message }) => errorTracker.capture(message));
WHAuth.on("logout", () => clearSession());wh-auth.config.json (generated by wh auth init)
{
"baseUrl": "https://myapp.com",
"callbackPath": "/auth/callback",
"port": 3001,
"providers": {
"google": {
"clientId": "...",
"clientSecret": "...",
"callbackUrl": "https://myapp.com/auth/callback/google"
},
"github": {
"clientId": "...",
"clientSecret": "...",
"callbackUrl": "https://myapp.com/auth/callback/github"
}
},
"session": {
"secret": "auto-generated",
"expiresIn": "7d"
}
}Verify JWT server-side
import { verifyAuthToken } from "webhanger-auth";
const user = await verifyAuthToken(req.headers.authorization?.split(" ")[1]);
// { email, name, avatar, provider, id, exp, iat }Auth flow
User clicks <wh-auth provider="google">
└── popup opens → /auth/google
└── redirect to Google OAuth consent
└── Google → /auth/callback/google?code=xxx
└── exchange code → fetch profile
└── issue JWT
└── postMessage to opener
└── WHAuth.on("success") fires
└── redirect to on-success URLA local web UI + SDK for managing deployed components.
npm install webhanger-admin
# Start dashboard
npx wh-admin ./webhanger.config.json 5000Open http://localhost:5000
Or use programmatically:
import { WebHangerAdmin } from "webhanger-admin";
const admin = new WebHangerAdmin("./webhanger.config.json");
const components = await admin.listComponents();
const manifest = await admin.generateManifest();
await admin.saveManifest("./public/wh-manifest.json");
const { apiKey } = await admin.grantAccess("deployer", "CI/CD");
await admin.resignComponent("navbar", "1.0.0", 86400);
await admin.deleteComponent("navbar", "1.0.0");See webhanger-admin/README.md for the full API reference.
Browser SDK
Zero-code Custom Element
<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
<script>WebHangerFront.initialize("./wh-manifest.json");</script>
<wh-component name="navbar" wh-brand="MyApp" wh-cta-text="Start Free"></wh-component>
<wh-component name="footer" sandbox></wh-component>ESM (Next.js / React / Vite)
import { load, use, on, registerSW, clearCache, metrics, gpu } from "webhanger-front";Manual load with props
await load(
cdnUrl,
projectId,
token,
expires,
selector,
onSignal,
deps,
{
props: { brand: "MyApp", ctaText: "Get Started" },
sandbox: true,
allowedDomains: ["mysite.com"],
beforeMount: () => showSpinner(),
afterMount: () => hideSpinner(),
onError: (err) => showFallback(err)
}
);Signal callback
load(url, pid, token, 0, "[data-wh]", ({ stage, time, source }) => {
// stages: start → fetching → assets → deps → injecting → done | error
});Plugin system
use({
install({ on }) {
on("load", ({ time, source }) => analytics.track("load", { time, source }));
on("error", ({ message }) => errorTracker.capture(message));
on("metric", ({ name, value }) => dashboard.update(name, value));
on("gpu", ({ supported }) => console.log("WebGPU:", supported));
on("sw", ({ scope }) => console.log("SW:", scope));
}
});Observability
on("load", ({ time, source }) => console.log(time, source));
console.log(metrics); // { loads, cacheHits, errors, totalTime }WebGPU
console.log(gpu.supported);
on("gpu", ({ supported }) => console.log("GPU:", supported));Offline + Service Worker
await registerSW("./webhanger.sw.js");
await setOfflinePage(
"<h1>Offline</h1><p>Back soon.</p>",
"body { background: #030712; color: white; }"
);Offline behavior:
- Online first visit → loads from CDN, caches everything
- Online repeat visit → loads from SW cache instantly, badge shows
- Offline with cache → full page works
- Offline no cache → custom offline page with "⬡ Served by WebHanger" badge
Smart Cache Invalidation
Only re-fetches from CDN when components actually changed. Checks admin server on every page load — if nothing changed, loads instantly from cache.
// Instead of initialize(), use smartInitialize()
WebHangerFront.smartInitialize(
"./wh-manifest.json",
"http://localhost:5000" // wh-admin server URL
);Flow:
Page loads
└── GET /api/last-updated from admin server (2s timeout)
├── same as localStorage "wh_last_updated"
│ └── load all components from cache (instant, 0ms)
└── different (components updated since last visit)
└── clear component cache
└── reload from CDN
└── update localStorage timestampEvents:
WebHangerFront.on("cache-invalidated", ({ serverTs, cachedTs }) => {
console.log("Components updated — reloading from CDN");
});
WebHangerFront.on("cache-hit", ({ serverTs }) => {
console.log("Up to date — loaded from cache");
});If admin server is unreachable (offline, not running), falls back to normal cache behavior silently — zero errors.
Test it:
# 1. Deploy a component
node smart-cache-test/deploy.js 1.0.0
# 2. Start admin server
node admin/server.js ./webhanger.config.json 5000
# 3. Serve the test page
npx serve smart-cache-test/site
# 4. Open http://localhost:3000 — loads from CDN, caches timestamp
# 5. Refresh — loads from cache instantly (banner: "✓ Up to date")
# 6. Redeploy with new version
node smart-cache-test/deploy.js 2.0.0
# 7. Refresh — cache invalidates, loads fresh (banner: "🔄 Components updated")See smart-cache-test/ for the full working demo.
Hard flush
await clearCache();Caching
| Layer | Used for |
|---|---|
| localStorage | Components < 50KB |
| IndexedDB | Components ≥ 50KB |
| Service Worker | Offline + navigation cache |
| wh_last_updated | Smart cache invalidation timestamp |
Stale-while-revalidate — returns cached instantly, refreshes in background.
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.
Access Control
| Role | deploy | read | delete | manage_access | |---|---|---|---|---| | owner | ✅ | ✅ | ✅ | ✅ | | admin | ✅ | ✅ | ✅ | ✅ | | deployer | ✅ | ✅ | ❌ | ❌ | | viewer | ❌ | ✅ | ❌ | ❌ |
Node.js API
import { WebHanger } from "webhanger";
const wh = new WebHanger();
const result = await wh.deploy("./components/navbar", "navbar", "1.0.0", {
expiresInSeconds: 86400,
dependencies: ["[email protected]"]
});
const comp = await wh.resolve("navbar", "1.0.0");
await wh.resign("navbar", "1.0.0", { expiresInSeconds: 3600 });
await wh.remove("navbar", "1.0.0");Next.js Integration
npm install webhanger-front"use client";
import { useEffect, useRef } from "react";
import { load } from "webhanger-front";
export default function WebHangerComponent({ name, props = {} }: { name: string; props?: Record<string, string> }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
fetch("/wh-manifest.json")
.then(r => r.json())
.then(m => {
const c = m.components[name];
if (!c || !ref.current) return;
ref.current.id = `wh-${name}`;
load(c.urls || c.url, m.pid, c.token, c.expires, `#wh-${name}`, null, [], { props });
});
}, [name]);
return <div ref={ref} />;
}// app/page.tsx
<WebHangerComponent name="navbar" props={{ brand: "MyApp", ctaText: "Sign Up" }} />Examples & Tests
| Folder | What it tests |
|---|---|
| examples/ | Full component deploy + browser SDK demo |
| showcase/ | All 4 packages together |
| auth-test/ | OAuth login flow (Google, GitHub) |
| smart-cache-test/ | Smart cache invalidation demo |
| personalization-test/ | Edge personalization — country, device, role, A/B |
# Examples
node examples/deploy.js && npx serve examples/site
# Showcase (all packages)
node showcase/deploy.js && npx serve showcase/site
# Auth test
wh auth init && wh auth serve &
npx serve . # open /auth-test/login.html
# Smart cache test
node smart-cache-test/deploy.js 1.0.0
node admin/server.js ./webhanger.config.json 5000 &
npx serve smart-cache-test/site
# Personalization test
node personalization-test/deploy.js
npx serve personalization-test/siteSee examples/EXAMPLE.md for the full step-by-step guide.
Storage Providers
| Provider | Notes |
|---|---|
| s3 | AWS S3 — auto-provisions bucket + CloudFront |
| r2 | Cloudflare R2 — zero egress fees |
| minio | Self-hosted MinIO |
| local | Local disk — dev only |
Database Providers
| Provider | Notes |
|---|---|
| firebase | Firebase Firestore — free tier |
| supabase | Supabase Postgres |
| mongodb | MongoDB Atlas |
Architecture
Developer
└── wh ship ./components ./site
├── wh analyze → detect Tailwind, GSAP, deps
├── wh breakdown → extract CSS/JS from single HTML
├── bundle → html + css + js → single payload
├── props schema → stored in payload for runtime resolution
├── AES-256-GCM → encrypt each chunk
├── SHA-256 hash → integrity fingerprint
├── S3 upload → store encrypted payload
├── HMAC sign → project-scoped signed URL
├── DB register → metadata + dep graph
├── wh build → minify HTML, extract CSS/JS
└── wh zip → deploy.zip ready for upload
Browser
└── <wh-component name="navbar" wh-brand="MyApp">
├── fetch wh-manifest.json
├── check token expiry
├── stale-while-revalidate cache
├── fetch from CloudFront / Edge Worker
├── multi-CDN failover
├── load CDN assets
├── resolve dependency graph
├── AES-256-GCM decrypt in memory
├── SHA-256 integrity verify
├── resolve props ({{wh.brand}} → "MyApp")
├── domain restriction check
├── inject CSS → HTML → JS (or Shadow DOM)
├── fire lifecycle hooks
├── emit metrics + plugin events
├── WebGPU detection
└── Service Worker caches for offlineLatest Updates (v1.1.0)
wh analyze — Auto-generates props schema
wh analyze now scans your component HTML for {{wh.propName}} placeholders and auto-generates the props schema in webhanger.component.json:
wh analyze ./components/navbarProps detected ({{wh.*}} placeholders):
brand default: ""
link1Label default: ""
link1Href default: ""
link4Label default: ""
link4Href default: ""
✅ webhanger.component.json updated with 5 propsThis runs automatically on every wh deploy too — zero manual maintenance.
data-transfer-protocol on <wh-auth>
Control how auth results are sent back to your app:
<!-- redirect (default) — URL params -->
<wh-auth provider="google"
data-transfer-protocol="redirect"
on-success="/dashboard?name={{name}}&email={{email}}">
</wh-auth>
<!-- post — JSON body to your API -->
<wh-auth provider="github"
data-transfer-protocol="post"
on-success="https://api.myapp.com/auth/callback">
</wh-auth>
<!-- put — REST endpoint -->
<wh-auth provider="facebook"
data-transfer-protocol="put"
on-success="https://api.myapp.com/users/me/auth">
</wh-auth>
<!-- callback — JS event only, no redirect -->
<wh-auth provider="google"
data-transfer-protocol="callback">
</wh-auth>For post/put — your server receives:
{
"user": { "email": "...", "name": "...", "avatar": "...", "provider": "google" },
"token": "jwt-token"
}With Authorization: Bearer <jwt> header. Return { "redirect": "/dashboard" } to trigger redirect.
License
ISC
