feat: add wireguard relay integration for public server access with End-to-End Encryption

Integrates tunwg (WireGuard tunnel) to enable optional public access
to the hapi server. Tunnel is disabled by default and enabled via
--relay flag or HAPI_TUNNEL=true environment variable.

Users can now run 'hapi server --relay' and get a direct link like:
https://app.hapi.run/?server=https://xxx.relay.hapi.run&token=xxx
This commit is contained in:
weishu
2026-01-13 20:18:11 +08:00
parent ecae91fa1c
commit 5defb6dbfc
22 changed files with 723 additions and 28 deletions
+34 -1
View File
@@ -67,6 +67,24 @@ function areToolsUnpacked(unpackedPath: string): boolean {
return expectedFiles.every((file) => existsSync(file));
}
function isTunwgReady(runtimeRoot: string): boolean {
const isWin = platform() === 'win32';
const tunwgBinary = isWin ? 'tunwg.exe' : 'tunwg';
const tunwgPath = join(runtimeRoot, 'tools', 'tunwg', tunwgBinary);
return existsSync(tunwgPath);
}
function ensureTunwgExecutable(runtimeRoot: string): void {
if (platform() === 'win32') {
return;
}
const tunwgPath = join(runtimeRoot, 'tools', 'tunwg', 'tunwg');
if (existsSync(tunwgPath)) {
chmodSync(tunwgPath, 0o755);
}
}
function unpackTools(runtimeRoot: string): void {
const platformDir = getPlatformDir();
const toolsDir = join(runtimeRoot, 'tools');
@@ -114,7 +132,7 @@ function unpackTools(runtimeRoot: string): void {
}
function runtimeAssetsReady(runtimeRoot: string): boolean {
return areToolsUnpacked(join(runtimeRoot, 'tools', 'unpacked'));
return areToolsUnpacked(join(runtimeRoot, 'tools', 'unpacked')) && isTunwgReady(runtimeRoot);
}
export async function ensureRuntimeAssets(): Promise<void> {
@@ -142,5 +160,20 @@ export async function ensureRuntimeAssets(): Promise<void> {
}
unpackTools(runtimeRoot);
ensureTunwgExecutable(runtimeRoot);
writeFileSync(markerPath, packageJson.version, 'utf-8');
}
export function getTunwgPath(): string {
const isWin = platform() === 'win32';
const tunwgBinary = isWin ? 'tunwg.exe' : 'tunwg';
if (isBunCompiled()) {
return join(runtimePath(), 'tools', 'tunwg', tunwgBinary);
}
// Development mode: use downloaded binary from server/tools/tunwg
const platformDir = getPlatformDir();
const devBinaryName = isWin ? `tunwg-${platformDir}.exe` : `tunwg-${platformDir}`;
return join(__dirname, '..', '..', '..', 'server', 'tools', 'tunwg', devBinaryName);
}
+48 -16
View File
@@ -4,6 +4,7 @@ import difftasticArchiveLicense from '../../tools/archives/difftastic-LICENSE' a
import ripgrepArchiveLicense from '../../tools/archives/ripgrep-LICENSE' assert { type: 'file' };
import difftasticLicense from '../../tools/licenses/difftastic-LICENSE' assert { type: 'file' };
import ripgrepLicense from '../../tools/licenses/ripgrep-LICENSE' assert { type: 'file' };
import tunwgLicense from '../../../server/tools/tunwg/LICENSE' assert { type: 'file' };
export interface EmbeddedAsset {
relativePath: string;
@@ -21,67 +22,98 @@ const COMMON_ASSETS: EmbeddedAsset[] = [
asset('tools/archives/difftastic-LICENSE', difftasticArchiveLicense),
asset('tools/archives/ripgrep-LICENSE', ripgrepArchiveLicense),
asset('tools/licenses/difftastic-LICENSE', difftasticLicense),
asset('tools/licenses/ripgrep-LICENSE', ripgrepLicense)
asset('tools/licenses/ripgrep-LICENSE', ripgrepLicense),
asset('tools/tunwg/LICENSE', tunwgLicense)
];
async function selectEmbeddedAssets(): Promise<EmbeddedAsset[]> {
if (feature('HAPI_TARGET_DARWIN_ARM64')) {
const [{ default: difftasticArm64Darwin }, { default: ripgrepArm64Darwin }] = await Promise.all([
const [
{ default: difftasticArm64Darwin },
{ default: ripgrepArm64Darwin },
{ default: tunwgArm64Darwin }
] = await Promise.all([
import('../../tools/archives/difftastic-arm64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-arm64-darwin.tar.gz', { assert: { type: 'file' } })
import('../../tools/archives/ripgrep-arm64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../../server/tools/tunwg/tunwg-arm64-darwin', { assert: { type: 'file' } })
]);
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-arm64-darwin.tar.gz', difftasticArm64Darwin),
asset('tools/archives/ripgrep-arm64-darwin.tar.gz', ripgrepArm64Darwin)
asset('tools/archives/ripgrep-arm64-darwin.tar.gz', ripgrepArm64Darwin),
asset('tools/tunwg/tunwg', tunwgArm64Darwin)
];
}
if (feature('HAPI_TARGET_DARWIN_X64')) {
const [{ default: difftasticX64Darwin }, { default: ripgrepX64Darwin }] = await Promise.all([
const [
{ default: difftasticX64Darwin },
{ default: ripgrepX64Darwin },
{ default: tunwgX64Darwin }
] = await Promise.all([
import('../../tools/archives/difftastic-x64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-x64-darwin.tar.gz', { assert: { type: 'file' } })
import('../../tools/archives/ripgrep-x64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../../server/tools/tunwg/tunwg-x64-darwin', { assert: { type: 'file' } })
]);
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-x64-darwin.tar.gz', difftasticX64Darwin),
asset('tools/archives/ripgrep-x64-darwin.tar.gz', ripgrepX64Darwin)
asset('tools/archives/ripgrep-x64-darwin.tar.gz', ripgrepX64Darwin),
asset('tools/tunwg/tunwg', tunwgX64Darwin)
];
}
if (feature('HAPI_TARGET_LINUX_ARM64')) {
const [{ default: difftasticArm64Linux }, { default: ripgrepArm64Linux }] = await Promise.all([
const [
{ default: difftasticArm64Linux },
{ default: ripgrepArm64Linux },
{ default: tunwgArm64Linux }
] = await Promise.all([
import('../../tools/archives/difftastic-arm64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-arm64-linux.tar.gz', { assert: { type: 'file' } })
import('../../tools/archives/ripgrep-arm64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../../server/tools/tunwg/tunwg-arm64-linux', { assert: { type: 'file' } })
]);
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-arm64-linux.tar.gz', difftasticArm64Linux),
asset('tools/archives/ripgrep-arm64-linux.tar.gz', ripgrepArm64Linux)
asset('tools/archives/ripgrep-arm64-linux.tar.gz', ripgrepArm64Linux),
asset('tools/tunwg/tunwg', tunwgArm64Linux)
];
}
if (feature('HAPI_TARGET_LINUX_X64')) {
const [{ default: difftasticX64Linux }, { default: ripgrepX64Linux }] = await Promise.all([
const [
{ default: difftasticX64Linux },
{ default: ripgrepX64Linux },
{ default: tunwgX64Linux }
] = await Promise.all([
import('../../tools/archives/difftastic-x64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-x64-linux.tar.gz', { assert: { type: 'file' } })
import('../../tools/archives/ripgrep-x64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../../server/tools/tunwg/tunwg-x64-linux', { assert: { type: 'file' } })
]);
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-x64-linux.tar.gz', difftasticX64Linux),
asset('tools/archives/ripgrep-x64-linux.tar.gz', ripgrepX64Linux)
asset('tools/archives/ripgrep-x64-linux.tar.gz', ripgrepX64Linux),
asset('tools/tunwg/tunwg', tunwgX64Linux)
];
}
if (feature('HAPI_TARGET_WIN32_X64')) {
const [{ default: difftasticX64Win32 }, { default: ripgrepX64Win32 }] = await Promise.all([
const [
{ default: difftasticX64Win32 },
{ default: ripgrepX64Win32 },
{ default: tunwgX64Win32 }
] = await Promise.all([
import('../../tools/archives/difftastic-x64-win32.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-x64-win32.tar.gz', { assert: { type: 'file' } })
import('../../tools/archives/ripgrep-x64-win32.tar.gz', { assert: { type: 'file' } }),
import('../../../server/tools/tunwg/tunwg-x64-win32.exe', { assert: { type: 'file' } })
]);
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-x64-win32.tar.gz', difftasticX64Win32),
asset('tools/archives/ripgrep-x64-win32.tar.gz', ripgrepX64Win32)
asset('tools/archives/ripgrep-x64-win32.tar.gz', ripgrepX64Win32),
asset('tools/tunwg/tunwg.exe', tunwgX64Win32)
];
}