dgb-wallet/packages/web-ui/vite.config.ts
Digibyte.X d50286a9a5 DigiByte.X Wallet — recovery-first web wallet monorepo
Packages: core (BIP39/32/44/49/84/86 derivation, WIF), keystore (PBKDF2 +
AES-GCM), psbt, p2p (BIP157/158 SPV daemon, Dandelion++ broadcast with
relay-echo verification), blockbook, rpc, electrum, net, bns, web-ui
(React/Vite), mobile (Expo). ops/ holds the nginx vhost, systemd unit and
push.sh deploy script for digibyte.x/web.
2026-09-20 20:16:52 +02:00

108 lines
4.8 KiB
TypeScript

import { defineConfig, loadEnv, type Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
// Subresource integrity (docs/SECURITY-ROADMAP.md item 2): after the
// bundle is written, stamp every <script src> / <link rel=stylesheet>
// in dist/index.html with integrity="sha384-…" and crossorigin. The
// entry HTML is then the only thing a host has to serve faithfully —
// a swapped or truncated asset file simply refuses to load. Relative
// URLs (./assets/…) are resolved against dist/.
function sri(): Plugin {
let outDir = 'dist';
return {
name: 'dgb-sri',
apply: 'build',
configResolved(cfg) { outDir = cfg.build.outDir; },
closeBundle() {
const htmlPath = join(outDir, 'index.html');
let html = readFileSync(htmlPath, 'utf8');
const digest = (rel: string) =>
'sha384-' + createHash('sha384').update(readFileSync(join(outDir, rel.replace(/^\.\//, '')))).digest('base64');
html = html.replace(/<script([^>]*?)\ssrc="(\.\/assets\/[^"]+)"([^>]*)>/g, (m, a, src, b) =>
m.includes('integrity=') ? m : `<script${a} src="${src}" integrity="${digest(src)}"${/crossorigin/.test(m) ? '' : ' crossorigin="anonymous"'}${b}>`);
html = html.replace(/<link([^>]*?)rel="stylesheet"([^>]*?)href="(\.\/assets\/[^"]+)"([^>]*)>/g, (m, a, b, href, c) =>
m.includes('integrity=') ? m : `<link${a}rel="stylesheet"${b}href="${href}" integrity="${digest(href)}"${/crossorigin/.test(m) ? '' : ' crossorigin="anonymous"'}${c}>`);
writeFileSync(htmlPath, html);
},
};
}
// @dgb-wallet/core depends on `bip39`, `bip32`, `bitcoinjs-lib`, etc.
// which use Node's Buffer + crypto internally. Polyfill so the wallet
// runs unchanged in the browser.
//
// The `/rpc/*` proxy solves the browser-→-localhost-RPC problem:
// browsers can't easily POST with Basic Auth to a different origin
// (14022) from 5173. Vite intercepts /rpc/ requests server-side,
// injects the Basic Auth header from env vars, and forwards to Core.
// The browser only ever sees same-origin JSON-RPC responses.
//
// Env vars (set in your shell before `npm run dev`):
// DGB_RPC_URL default http://127.0.0.1:14022
// DGB_RPC_USER required for /rpc/ to work
// DGB_RPC_PASSWORD required for /rpc/ to work
export default defineConfig(({ mode }) => {
// Look for env in the package dir (where vite lives), not process.cwd()
// — with npm workspaces, cwd can be the monorepo root.
const pkgDir = new URL('.', import.meta.url).pathname.replace(/^\//, '');
const env = { ...loadEnv(mode, pkgDir, 'DGB_'), ...loadEnv(mode, process.cwd(), 'DGB_') };
const rpcUrl = env.DGB_RPC_URL ?? process.env.DGB_RPC_URL ?? 'http://127.0.0.1:14022';
const rpcUser = env.DGB_RPC_USER ?? process.env.DGB_RPC_USER ?? '';
const rpcPassword = env.DGB_RPC_PASSWORD ?? process.env.DGB_RPC_PASSWORD ?? '';
const authHeader =
rpcUser && rpcPassword
? 'Basic ' + Buffer.from(`${rpcUser}:${rpcPassword}`).toString('base64')
: '';
console.log(`[vite proxy] target=${rpcUrl} user=${rpcUser || '(none)'} auth=${authHeader ? 'ON' : 'OFF'}`);
// `base` controls the leading path for every generated asset URL.
// dev : '/' (Vite serves at localhost:5173/)
// production : './' (relative — anchors against the runtime
// <base> tag our index.html injects, so
// the same bundle works under digibyte.x/web/
// AND navigate.st/bns/digibyte.x/web/)
// Override with DGB_UI_BASE if you're serving from something exotic.
const defaultBase = mode === 'production' ? './' : '/';
const base = env.DGB_UI_BASE ?? process.env.DGB_UI_BASE ?? defaultBase;
return {
base,
plugins: [
react(),
sri(),
nodePolyfills({
globals: { Buffer: true, global: true, process: true },
protocolImports: true,
}),
],
server: {
port: 5173,
host: '127.0.0.1',
proxy: {
'/rpc': {
target: rpcUrl,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/rpc\/?/, '/'),
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
if (authHeader) proxyReq.setHeader('Authorization', authHeader);
});
},
},
// /p2p/ → the @dgb-wallet/p2p daemon (a small Node HTTP server
// wrapping the SPV client). Enables browser broadcast via P2P
// instead of Core RPC. Start the daemon with:
// npm run daemon -w @dgb-wallet/p2p
'/p2p': {
target: 'http://127.0.0.1:5174',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/p2p\/?/, '/'),
},
},
},
};
});