mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
bridge: wire tenant brand to the SPA at runtime (white-label)
The app/bridge SPA baked @luxfi/brand at build time and the Go server served brand at /envs.js while the SPA loads /__ENV.js — so a white-label tenant (e.g. zooai/bridge-shim) rendered the Lux brand. Complete the stated 'jurisdiction-neutral, tenant supplies brand' design: - bridge.config.ts: brand name/favicon/primaryColor/supportEmail/docsUrl are now window.__ENV-overridable (logoUrl already was); @luxfi/brand stays the fallback so the Lux tenant is byte-unchanged when no override is set. - frontend.go: serve /__ENV.js (the endpoint the SPA actually loads) projecting the tenant brand+IAM into the BRIDGE_* keys; applyBrandMetadata() then themes title/favicon/logo/color at mount. Empty when no tenant → Lux fallback. - main.go: pass the tenant config into NewFrontend.
This commit is contained in:
@@ -170,17 +170,22 @@ export const bridgeConfig: BridgeConfig = {
|
||||
clientId: env('BRIDGE_CLIENT_ID'),
|
||||
iamOrg: env('BRIDGE_IAM_ORG', 'lux'),
|
||||
brand: {
|
||||
name: `${shortName ?? 'Lux'} Bridge`,
|
||||
// Every brand field is runtime-overridable via window.__ENV so a single
|
||||
// build serves any tenant (the serving container — or the Go single-binary
|
||||
// image's /__ENV.js — templates these from its tenant config). @luxfi/brand
|
||||
// stays the fallback, so the Lux tenant is unchanged when no override is set.
|
||||
name: env('BRIDGE_BRAND_NAME') || `${shortName ?? 'Lux'} Bridge`,
|
||||
logoUrl: env('BRIDGE_LOGO_URL') || luxLogoDataUrl,
|
||||
primaryColor: primaryColor ?? '#000000',
|
||||
faviconUrl: env('BRIDGE_FAVICON_URL'),
|
||||
primaryColor: env('BRIDGE_PRIMARY_COLOR') || primaryColor || '#000000',
|
||||
// secondaryColor intentionally omitted. brand.json's theme.{light,dark}.accent1
|
||||
// is a per-theme foreground (white on dark / black on light), NOT a brand
|
||||
// accent — wiring it to --brand-secondary breaks hover-color on the
|
||||
// opposite-theme surface. Fall back to the SDK's compiled default
|
||||
// (`var(--brand-secondary, #7a9ff5)` in pkg/bridge/src/app/styles/theme.css)
|
||||
// until @luxfi/brand exposes a real brand-accent field.
|
||||
supportEmail: supportEmail ?? '[email protected]',
|
||||
docsUrl: fallbackDocsUrl,
|
||||
supportEmail: env('BRIDGE_SUPPORT_EMAIL') || supportEmail || '[email protected]',
|
||||
docsUrl: env('BRIDGE_DOCS_URL') || fallbackDocsUrl,
|
||||
},
|
||||
wallet,
|
||||
mpc,
|
||||
|
||||
+49
-6
@@ -11,6 +11,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/bridge/pkg/tenant"
|
||||
)
|
||||
|
||||
// staticFS is the embedded SPA. The Dockerfile populates static/ from a
|
||||
@@ -24,13 +26,14 @@ var staticFS embed.FS
|
||||
// brand assets (/icon.svg, /logo.svg). Brand assets read from disk on every
|
||||
// request so a deploy can swap them without rebuilding the binary.
|
||||
type Frontend struct {
|
||||
cfg Config
|
||||
root fs.FS
|
||||
index []byte
|
||||
overlay string // optional disk dir for SPA + brand override
|
||||
cfg Config
|
||||
root fs.FS
|
||||
index []byte
|
||||
overlay string // optional disk dir for SPA + brand override
|
||||
runtimeEnv map[string]string // window.__ENV served at /__ENV.js (tenant brand)
|
||||
}
|
||||
|
||||
func NewFrontend(cfg Config, overlay string) (*Frontend, error) {
|
||||
func NewFrontend(cfg Config, overlay string, tcfg *tenant.Config) (*Frontend, error) {
|
||||
root, err := fs.Sub(staticFS, "static")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sub-fs: %w", err)
|
||||
@@ -39,11 +42,40 @@ func NewFrontend(cfg Config, overlay string) (*Frontend, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read index.html: %w", err)
|
||||
}
|
||||
return &Frontend{cfg: cfg, root: root, index: idx, overlay: overlay}, nil
|
||||
return &Frontend{cfg: cfg, root: root, index: idx, overlay: overlay, runtimeEnv: buildRuntimeEnv(tcfg)}, nil
|
||||
}
|
||||
|
||||
// buildRuntimeEnv projects the tenant brand + IAM config into the window.__ENV
|
||||
// keys the SPA's bridge.config.ts reads (BRIDGE_*). When there is no tenant
|
||||
// (the canonical Lux deployment), it returns an empty map so the SPA falls
|
||||
// back to its build-time @luxfi/brand defaults — Lux is unchanged. White-label
|
||||
// shims ship a tenant.yaml and get their brand applied at runtime, one image.
|
||||
func buildRuntimeEnv(tcfg *tenant.Config) map[string]string {
|
||||
e := map[string]string{}
|
||||
if tcfg == nil {
|
||||
return e
|
||||
}
|
||||
set := func(k, v string) {
|
||||
if v != "" {
|
||||
e[k] = v
|
||||
}
|
||||
}
|
||||
b := tcfg.Brand
|
||||
set("BRIDGE_BRAND_NAME", b.Name)
|
||||
set("BRIDGE_LOGO_URL", b.LogoURL)
|
||||
set("BRIDGE_FAVICON_URL", b.FaviconURL)
|
||||
set("BRIDGE_PRIMARY_COLOR", b.PrimaryColor)
|
||||
set("BRIDGE_DOCS_URL", b.DocsURL)
|
||||
set("BRIDGE_IAM_ORG", tcfg.IAM.Organization)
|
||||
set("BRIDGE_CLIENT_ID", tcfg.IAM.ClientID)
|
||||
return e
|
||||
}
|
||||
|
||||
func (f *Frontend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/__ENV.js":
|
||||
f.serveRuntimeEnv(w, r)
|
||||
return
|
||||
case "/envs.js":
|
||||
f.serveEnvs(w, r)
|
||||
return
|
||||
@@ -78,6 +110,17 @@ func (f *Frontend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(f.index)
|
||||
}
|
||||
|
||||
// serveRuntimeEnv emits window.__ENV — the runtime brand/config the embedded
|
||||
// SPA loads via <script src="/__ENV.js"> before its main bundle. This is the
|
||||
// tenant→frontend brand bridge: bridge.config.ts reads these BRIDGE_* keys and
|
||||
// applyBrandMetadata() applies name/favicon/logo/color to the document.
|
||||
func (f *Frontend) serveRuntimeEnv(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := json.Marshal(f.runtimeEnv)
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
fmt.Fprintf(w, "window.__ENV = %s;", body)
|
||||
}
|
||||
|
||||
func (f *Frontend) serveEnvs(w http.ResponseWriter, r *http.Request) {
|
||||
envs := map[string]any{
|
||||
"brand": f.cfg.Brand,
|
||||
|
||||
+1
-1
@@ -257,7 +257,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
frontend, err := NewFrontend(cfg, *staticDir)
|
||||
frontend, err := NewFrontend(cfg, *staticDir, tcfg)
|
||||
if err != nil {
|
||||
logger.Error("frontend init", "err", err)
|
||||
os.Exit(1)
|
||||
|
||||
Reference in New Issue
Block a user