merge: runtime env shim (window.__ENV + nginx docker-entrypoint)

# Conflicts:
#	app/bridge/src/bridge.config.ts
This commit is contained in:
2026-05-21 16:51:50 -07:00
6 changed files with 109 additions and 16 deletions
+12 -3
View File
@@ -1,8 +1,15 @@
# syntax=docker/dockerfile:1.7
#
# bridge.lux.network — Vite SPA served by ghcr.io/hanzoai/spa.
# bridge.lux.network — Vite SPA.
#
# Build context: repo root (pnpm workspace so @luxfi/bridge resolves
# to the sibling pkg/bridge package).
#
# Runtime config: nginx serves the static bundle and a templated
# /__ENV.js generated at container boot from BRIDGE_* env vars. The
# bundle's bridge.config.ts reads window.__ENV (populated by
# /__ENV.js) before falling back to Vite's build-time import.meta.env.
# This makes the image env-agnostic: one tag deployed to N envs.
FROM node:22-alpine AS build
RUN apk add --no-cache libc6-compat python3 make g++ git
@@ -12,5 +19,7 @@ COPY . .
RUN pnpm install --frozen-lockfile || pnpm install
RUN pnpm -C app/bridge build
FROM ghcr.io/hanzoai/spa
COPY --from=build /app/app/bridge/dist /public
FROM nginx:alpine
COPY --from=build /app/app/bridge/dist /usr/share/nginx/html
COPY app/bridge/docker-entrypoint.sh /docker-entrypoint.d/40-render-env.sh
RUN chmod +x /docker-entrypoint.d/40-render-env.sh
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# Generate /__ENV.js from container env at boot. Lets one image serve N envs.
#
# nginx:alpine looks in /docker-entrypoint.d/ for *.sh files and runs them
# before starting nginx. This file lands at /docker-entrypoint.d/40-render-env.sh
# via the Dockerfile.
#
# The SDK reads window.__ENV from /__ENV.js before falling back to
# build-time import.meta.env (see app/bridge/src/bridge.config.ts).
#
# Tenants (zoo, lux-mci, ...) copy this script verbatim — only the env
# var names need to match the keys the tenant's bridge.config.ts looks up.
set -eu
cat > /usr/share/nginx/html/__ENV.js <<EOF
window.__ENV = {
BRIDGE_API_HOST: '${BRIDGE_API_HOST:-}',
BRIDGE_ENV: '${BRIDGE_ENV:-}',
BRIDGE_CLIENT_ID: '${BRIDGE_CLIENT_ID:-}',
BRIDGE_IAM_ORG: '${BRIDGE_IAM_ORG:-}',
BRIDGE_LOGO_URL: '${BRIDGE_LOGO_URL:-}',
WC_PROJECT_ID: '${WC_PROJECT_ID:-}',
}
EOF
+12
View File
@@ -6,6 +6,18 @@
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%235b8def'/></svg>" />
<title>Lux Bridge</title>
<meta name="description" content="Cross-chain bridge for the Lux Network." />
<!--
Runtime env: /__ENV.js sets window.__ENV before the main bundle
loads. Serving image templates this file at boot from BRIDGE_*
env vars; bridge.config.ts reads window.__ENV first, then falls
back to Vite's import.meta.env. One image, N envs.
Placed in <head> (and before the module script) so the synchronous
parser-blocking load happens before any deferred module executes —
the ordering is unambiguous regardless of how Vite reorganises
the bundle.
-->
<script src="/__ENV.js"></script>
</head>
<body>
<div id="bridge-root"></div>
+14
View File
@@ -0,0 +1,14 @@
// Runtime env for the bridge SPA. Overwritten at container boot by the
// serving image's docker-entrypoint, or baked at build time.
//
// The SDK's bridge.config.ts reads window.__ENV before falling back to
// Vite's build-time import.meta.env. This decouples the deployable image
// from the environment it runs in: one image, N envs.
window.__ENV = window.__ENV || {
BRIDGE_API_HOST: '',
BRIDGE_ENV: '',
BRIDGE_CLIENT_ID: '',
BRIDGE_IAM_ORG: '',
BRIDGE_LOGO_URL: '',
WC_PROJECT_ID: '',
}
+40 -13
View File
@@ -5,7 +5,7 @@
// Lux-specific text/logos/colors live in the build.
//
// Source of truth for the Lux brand block: @luxfi/brand
// (https://github.com/luxfi/brand). We read every Lux value from the
// (https://github.com/luxfi/brand). Every Lux value is read from the
// package's brand.json so a brand bump in the package propagates here
// on the next install — no string copies, no drift. The JSON subpath
// is used because the npm-published @luxfi/[email protected] omits its .d.ts
@@ -13,21 +13,48 @@
// literal types from it under `resolveJsonModule`.
//
// Logo source of truth: @luxfi/logo (https://github.com/luxfi/logo).
// We import the canonical SVG generator and inline it as a data URL so
// the build artifact has zero external dependencies for the brand mark.
// The canonical SVG generator is inlined as a data URL so the build
// artifact has zero external dependencies for the brand mark.
//
// Env resolution order (per key):
// 1. window.__ENV — runtime, set by /__ENV.js at page load. One
// image, N envs. Serving container templates
// /__ENV.js from real env at boot.
// 2. import.meta.env — build-time Vite fallback, used in dev and as
// a safety net if the serving image fails to
// template /__ENV.js.
// 3. @luxfi/brand defaults — final fallback to the canonical Lux brand
// package's brand.json values.
import type { BridgeConfig } from '@luxfi/bridge'
import luxBrandJson from '@luxfi/brand/brand.json'
import { getColorSVG } from '@luxfi/logo'
declare global {
interface Window {
__ENV?: Partial<Record<string, string>>
}
}
const luxBrand = luxBrandJson.brand
const luxLogoDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(getColorSVG())}`
const runtime: Partial<Record<string, string>> =
(typeof window !== 'undefined' && window.__ENV) || {}
const build = import.meta.env
// `import.meta.env` is Vite's standard env namespace; missing values fall back
// to values read from @luxfi/brand (which in turn defaults to the production
// Lux network).
const env = import.meta.env
/**
* Look up a config value by key. Tries runtime (`window.__ENV`) first,
* then build-time (`import.meta.env.VITE_*`), then the supplied fallback.
*/
const env = (key: string, fallback?: string): string | undefined => {
const r = runtime[key]
if (r) return r
const b = build[`VITE_${key}`]
if (b) return b
return fallback
}
const luxLogoDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(getColorSVG())}`
const fallbackApiHost = luxBrand.appDomain
? `https://api.bridge.${luxBrand.appDomain}`
@@ -37,13 +64,13 @@ const fallbackDocsUrl = luxBrand.docsDomain
: 'https://docs.lux.network/bridge'
export const bridgeConfig: BridgeConfig = {
apiHost: env.VITE_BRIDGE_API_HOST ?? fallbackApiHost,
env: env.VITE_BRIDGE_ENV ?? 'mainnet',
clientId: env.VITE_BRIDGE_CLIENT_ID,
iamOrg: env.VITE_BRIDGE_IAM_ORG ?? 'lux',
apiHost: env('BRIDGE_API_HOST', fallbackApiHost)!,
env: env('BRIDGE_ENV', 'mainnet')!,
clientId: env('BRIDGE_CLIENT_ID'),
iamOrg: env('BRIDGE_IAM_ORG', 'lux'),
brand: {
name: `${luxBrand.shortName ?? 'Lux'} Bridge`,
logoUrl: env.VITE_BRIDGE_LOGO_URL ?? luxLogoDataUrl,
logoUrl: env('BRIDGE_LOGO_URL') || luxLogoDataUrl,
primaryColor: luxBrand.primaryColor ?? '#000000',
secondaryColor:
luxBrand.theme?.dark?.accent1 ?? luxBrand.theme?.light?.accent1,
+6
View File
@@ -1,11 +1,17 @@
/// <reference types="vite/client" />
// Build-time Vite envs. These are the fallback for the runtime
// window.__ENV mechanism (see src/bridge.config.ts). Vite inlines
// `import.meta.env.VITE_*` at build, so changing one after the
// bundle is built does nothing — that's why we layer window.__ENV
// (templated by the serving image at boot) on top.
interface ImportMetaEnv {
readonly VITE_BRIDGE_API_HOST?: string
readonly VITE_BRIDGE_ENV?: string
readonly VITE_BRIDGE_CLIENT_ID?: string
readonly VITE_BRIDGE_IAM_ORG?: string
readonly VITE_BRIDGE_LOGO_URL?: string
readonly VITE_WC_PROJECT_ID?: string
}
interface ImportMeta {