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
+11
View File
@@ -125,6 +125,17 @@ function AppInner() {
queryClient.clear()
}, [baseUrl, queryClient])
// Clean up URL params after successful auth (for direct access links)
useEffect(() => {
if (!token || !api) return
const url = new URL(window.location.href)
if (url.searchParams.has('server') || url.searchParams.has('token')) {
url.searchParams.delete('server')
url.searchParams.delete('token')
window.history.replaceState({}, '', url.toString())
}
}, [token, api])
useEffect(() => {
if (!api || !token) {
pushPromptedRef.current = false
+15
View File
@@ -21,6 +21,12 @@ function getTelegramInitData(): string | null {
return initData || null
}
function getTokenFromUrlParams(): string | null {
if (typeof window === 'undefined') return null
const query = new URLSearchParams(window.location.search)
return query.get('token')
}
function getAccessTokenKey(baseUrl: string): string {
return `${ACCESS_TOKEN_PREFIX}${baseUrl}`
}
@@ -79,6 +85,15 @@ export function useAuthSource(baseUrl: string): {
return
}
// Check for URL token parameter (for direct access links)
const urlToken = getTokenFromUrlParams()
if (urlToken) {
storeAccessToken(accessTokenKey, urlToken) // Save to localStorage for refresh
setAuthSource({ type: 'accessToken', token: urlToken })
setIsLoading(false)
return
}
// Check for stored access token as fallback
const storedToken = getStoredAccessToken(accessTokenKey)
if (storedToken) {
+20 -1
View File
@@ -26,6 +26,17 @@ export function normalizeServerUrl(input: string): ServerUrlResult {
return { ok: true, value: parsed.origin }
}
function getServerFromUrlParams(): string | null {
if (typeof window === 'undefined') return null
const query = new URLSearchParams(window.location.search)
const server = query.get('server')
if (server) {
const normalized = normalizeServerUrl(server)
return normalized.ok ? normalized.value : null
}
return null
}
function readStoredServerUrl(): string | null {
try {
const stored = localStorage.getItem(SERVER_URL_KEY)
@@ -65,7 +76,15 @@ export function useServerUrl(): {
setServerUrl: (input: string) => ServerUrlResult
clearServerUrl: () => void
} {
const [serverUrl, setServerUrlState] = useState<string | null>(() => readStoredServerUrl())
const [serverUrl, setServerUrlState] = useState<string | null>(() => {
// Priority: URL params > localStorage
const fromUrl = getServerFromUrlParams()
if (fromUrl) {
writeStoredServerUrl(fromUrl) // Save to localStorage for refresh
return fromUrl
}
return readStoredServerUrl()
})
const fallbackOrigin = typeof window !== 'undefined' ? window.location.origin : ''
const baseUrl = useMemo(() => serverUrl ?? fallbackOrigin, [serverUrl, fallbackOrigin])