diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts index 85b5ccf49..4e363a1ec 100644 --- a/cli/src/commands/auth.ts +++ b/cli/src/commands/auth.ts @@ -32,7 +32,7 @@ export async function handleAuthCommand(args: string[]): Promise { if (!hasToken) { console.log('') console.log(chalk.yellow(' Token not configured. To get your token:')) - console.log(chalk.gray(' 1. Check the server startup logs (first run shows generated token)')) + console.log(chalk.gray(' 1. Check the server startup logs (hub now prints a local login URL)')) console.log(chalk.gray(' 2. Read ~/.hapi/settings.json on the server')) console.log(chalk.gray(' 3. Ask your server administrator (if token is set via env var)')) console.log('') diff --git a/cli/src/ui/tokenInit.ts b/cli/src/ui/tokenInit.ts index a3ac529a2..fe89cd200 100644 --- a/cli/src/ui/tokenInit.ts +++ b/cli/src/ui/tokenInit.ts @@ -55,7 +55,7 @@ async function promptForToken(): Promise { console.log(chalk.yellow('\nNo CLI_API_TOKEN found.')) console.log(chalk.gray('Where to find the token:')) - console.log(chalk.gray(' 1. Check the server startup logs (first run shows generated token)')) + console.log(chalk.gray(' 1. Check the server startup logs (hub now prints a local login URL)')) console.log(chalk.gray(' 2. Read ~/.hapi/settings.json on the server')) console.log(chalk.gray(' 3. Ask your server administrator (if token is set via env var)\n')) diff --git a/hub/src/index.ts b/hub/src/index.ts index a53ae3308..4d299933b 100644 --- a/hub/src/index.ts +++ b/hub/src/index.ts @@ -24,6 +24,7 @@ import { PushNotificationChannel } from './push/pushNotificationChannel' import { VisibilityTracker } from './visibility/visibilityTracker' import { TunnelManager } from './tunnel' import { waitForTunnelTlsReady } from './tunnel/tlsGate' +import { buildRelayDirectAccessUrl, buildTokenizedUrl } from './utils/directAccess' import QRCode from 'qrcode' import type { Server as BunServer } from 'bun' import type { WebSocketData } from '@socket.io/bun-engine' @@ -113,6 +114,8 @@ async function main() { const relayFlag = resolveRelayFlag(process.argv) const officialWebUrl = process.env.HAPI_OFFICIAL_WEB_URL || 'https://app.hapi.run' const config = await createConfiguration() + const localUrl = `http://localhost:${config.listenPort}` + const localDirectAccessUrl = buildTokenizedUrl(localUrl, config.cliApiToken) const baseCorsOrigins = normalizeOrigins(config.corsOrigins) const relayCorsOrigin = normalizeOrigin(officialWebUrl) const corsOrigins = relayFlag.enabled @@ -225,7 +228,8 @@ async function main() { console.log('') console.log('[Web] Hub listening on :' + config.listenPort) - console.log('[Web] Local: http://localhost:' + config.listenPort) + console.log('[Web] Local: ' + localUrl) + console.log('[Web] Login: ' + localDirectAccessUrl) // Initialize tunnel AFTER web service is ready let tunnelUrl: string | null = null @@ -257,12 +261,7 @@ async function main() { console.log('[Web] Public: ' + tunnelUrl) - // Generate direct access link with hub and token - const params = new URLSearchParams({ - hub: tunnelUrl, - token: config.cliApiToken - }) - const directAccessUrl = `${officialWebUrl}/?${params.toString()}` + const directAccessUrl = buildRelayDirectAccessUrl(officialWebUrl, tunnelUrl, config.cliApiToken) console.log('') console.log('Open in browser:') diff --git a/hub/src/utils/directAccess.test.ts b/hub/src/utils/directAccess.test.ts new file mode 100644 index 000000000..e6f8fbacd --- /dev/null +++ b/hub/src/utils/directAccess.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'bun:test' +import { buildRelayDirectAccessUrl, buildTokenizedUrl } from './directAccess' + +describe('buildTokenizedUrl', () => { + it('adds the token query parameter', () => { + expect(buildTokenizedUrl('http://localhost:3006', 'secret-token')) + .toBe('http://localhost:3006/?token=secret-token') + }) + + it('preserves existing query parameters', () => { + expect(buildTokenizedUrl('https://example.com/app?foo=bar', 'secret-token')) + .toBe('https://example.com/app?foo=bar&token=secret-token') + }) +}) + +describe('buildRelayDirectAccessUrl', () => { + it('adds hub and token query parameters', () => { + expect(buildRelayDirectAccessUrl('https://app.hapi.run', 'https://relay.example', 'secret-token')) + .toBe('https://app.hapi.run/?hub=https%3A%2F%2Frelay.example&token=secret-token') + }) + + it('preserves existing query parameters', () => { + expect(buildRelayDirectAccessUrl('https://app.hapi.run/?lang=zh-CN', 'https://relay.example', 'secret-token')) + .toBe('https://app.hapi.run/?lang=zh-CN&hub=https%3A%2F%2Frelay.example&token=secret-token') + }) +}) diff --git a/hub/src/utils/directAccess.ts b/hub/src/utils/directAccess.ts new file mode 100644 index 000000000..1375e29c3 --- /dev/null +++ b/hub/src/utils/directAccess.ts @@ -0,0 +1,12 @@ +export function buildTokenizedUrl(baseUrl: string, token: string): string { + const url = new URL(baseUrl) + url.searchParams.set('token', token) + return url.toString() +} + +export function buildRelayDirectAccessUrl(webUrl: string, hubUrl: string, token: string): string { + const url = new URL(webUrl) + url.searchParams.set('hub', hubUrl) + url.searchParams.set('token', token) + return url.toString() +}