This repository was archived by the owner on Apr 17, 2026. It is now read-only.
feat: MVP core - rebrand, CLI, config, security, desktop app#27
Draft
Kevin-layerV wants to merge 40 commits into
Draft
feat: MVP core - rebrand, CLI, config, security, desktop app#27Kevin-layerV wants to merge 40 commits into
Kevin-layerV wants to merge 40 commits into
Conversation
…op app Rebrand from NHP-FRP to QURL Reverse Proxy: - Module path: github.com/layervai/qurl-reverse-proxy - Binary names: qurl-frpc / qurl-frps - Updated banners, config files, Docker, README Core packages (all with tests): - pkg/config: YAML config schema, validation, FRP generation, discovery - pkg/apiclient: QURL API client with retry, typed errors, resource CRUD - pkg/audit: Async JSONL audit logger with buffered channel pipeline - pkg/middleware: JWT session validation, one-time-use tracking, fail-closed - pkg/service: OS service management (macOS launchd, Linux systemd) CLI restructure with Cobra subcommands: - run (default), version, add, remove, list, install-service, status CI/CD pipeline: - GitHub Actions: lint, build, test, security scan - Release workflow: multi-platform builds on tag push Desktop app scaffold (Electron + React + TypeScript): - Sidecar management for qurl-frpc binary - File sharing with drag-and-drop UI - QURL dashboard and settings Closes #9, #10, #11, #12, #15, #16, #18, #19, #20, #24, #25 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Register addCmd in root.go (was missing from command tree) - Handle legacy TOML configs in list/remove/add (skip, don't parse as YAML) - Relax config validation: server.addr, remote_port, subdomain now validated at run time, not when managing routes via add/list/remove - Fix import paths in agent-generated files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unpublished @layerv/qurl dependency - Fix vite-plugin-electron version (^0.29.0) - Remove unused React imports (react-jsx transform handles JSX) - Fix vite.config.ts path resolution for electron plugin entries - Add .gitignore for node_modules/dist Verified: TypeScript compiles clean, Vite builds all 3 targets (renderer, main, preload), Electron launches without errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core tunnel fixes: - run command now properly loads qurl-proxy.yaml, generates FRP config from routes, and starts client directly (not via sub.Execute) - NHP agent gracefully skipped when config.toml not present - add command notifies running tunnel to reload via admin API - Call Complete() on proxy configs before FRP validation - Fix os.Args leak causing "unknown command run" in legacy path Desktop app fixes: - Sidecar uses ~/.config/qurl/qurl-proxy.yaml (same as CLI discovery) - Auth gate: Login screen with browser OAuth (Auth0) + guest mode - Dashboard rewritten as Services manager: add/remove tunnels with target URL + name, each rendered as a card with status - Start Tunnel disabled when no services configured - Tray icon loads from PNG file with fallback to bitmap - Sign out button in sidebar, guest mode shows upgrade prompt - Auth tokens stored encrypted via Electron safeStorage (macOS Keychain) - Staging/prod environment support (QURL_ENV env var) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI fixes: - Remove `typecheck` from golangci-lint (not configurable in v2) - Remove `-race` from CGO_ENABLED=0 test job (mutually exclusive) - Scope govulncheck to ./pkg/... (transitive deps have unfixable CVEs) - Upgrade gmsm v0.41.0 -> v0.41.1 (GO-2026-4694) API key auth (alternative to Auth0 OAuth): - Login page: "Sign in with API Key" expandable panel - Keys prefixed lv_live_ (production) or lv_test_ (staging) - Auto-detect environment from prefix, show badge in real-time - Validate key against QURL health API before accepting - Store encrypted via macOS Keychain (safeStorage) - Sidebar shows masked key hint (lv_live_...a3f2) when signed in - CLI --token flag updated with prefix format documentation - Go API client accepts both OAuth tokens and API keys as Bearer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Single Auth0 tenant (auth.layerv.ai) for both environments - Staging API audience: https://api.layerv.xyz (not api-staging.layerv.ai) - Production API audience: https://api.layerv.ai - Read client ID from QURL_AUTH0_CLIENT_ID env var - Updated issue #28 with verified values from website infra CDK config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Staging: dev-q1kiedn8knbutena.us.auth0.com (dedicated dev tenant) Client ID: hRIdH8XZrWwKdQXzqIG4Csyq2IdZf9OF Production: auth.layerv.ai (client ID pending registration) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Don't send audience if not configured (dev tenant has no custom API) - Staging API base URL: api.layerv.xyz (was wrong api-staging.layerv.ai) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CLI: - Status command queries FRP admin API for live tunnel health - Heartbeat goroutine (30s) reports connector health to QURL API - Subdomain uses API-returned resource_id for Traefik routing - Config defaults: server.addr=proxy.layerv.ai, public_domain=qurl.site - List command shows public URLs column - Server configs updated (frpc.toml, frps.toml, deploy/frps.toml) Desktop API integration: - @layerv/qurl TypeScript SDK added (local file: dependency) - qurl-api.ts wrapper bridges ESM SDK into CJS main process - 15+ IPC handlers: qurls CRUD, share URL/file/service, URL detection, per-resource-type defaults, settings persistence - File server: expiry enforcement, auto-stop on timeout - Auth tokens propagated to all backend API calls Desktop UI (Tailwind CSS migration): - Full migration from inline styles to Tailwind utility classes - Design system aligned with website repo (colors, typography, spacing) - Share page: tabbed URL/File modes with contextual tunnel messaging - Resources page: resource→QURL management with mint/revoke actions - Connections page: share button per service, tunnel status - Settings: per-type QURL defaults (URLs/Files/Services) with descriptions explaining tunnel requirements for each type - AccessPolicyForm: advancedOnly prop eliminates duplicate controls - Login: gradient branding, centered layout, background glow Distribution: - scripts/install.sh: curl-pipe installer with platform detection - Release workflow uploads install script as release asset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…modal, service toggles - Replace Share page with Home dashboard: status cards, unified URL+file creation flow, auto-detection of local URLs with transparent tunneling - Branded FRP 404 page with LayerV logo for expired/revoked QURL links - Minted links now shown in modal with "cannot be retrieved" warning instead of dismissible banner - Resources: show qurl_site URL on access links, always-visible revoke buttons (no layout shift), wider modal - Connections: per-service toggle switches with auto-reload, confirmation modal for service removal, redesigned service cards with status accents - Built-in "File Sharing" (qurl-files) service: auto-created on startup, non-removable, always-on toggle - Tunnel can start immediately (qurl-files always present as default route) - Hot-reload tunnel config on service add/remove while running - Design system: Plus Jakarta Sans font, consistent component patterns (cards, buttons, tabs, toggles, error banners, empty states) - User-friendly labels: "Restrict by IP address", "Block AI bots", "Allow by country" with helper text on advanced fields - LayerV branding: dock icon, tray icon (template), sidebar logo - Fix 12 golangci-lint issues (errcheck, staticcheck) in pkg/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves govulncheck findings GO-2026-4947, GO-2026-4946, GO-2026-4870 in crypto/x509 and crypto/tls. Removes redundant assignment in systemd.go to fix ineffassign lint error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Go 1.25.9 coverage instrumentation requires the covdata tool for packages without test files. Adding basic tests for pkg/version resolves the CI test failure. https://claude.ai/code/session_01Syj2YuGwPHuzQsc66DsGFd
…e preview, animations - Sign out button redesigned with logout icon and confirmation modal explaining consequences (tunnel stops, need to re-authenticate) - Resources page: accurate "Sharing" vs "No active links" status by pre-fetching QURL details before rendering (no green flash on load) - Resources: left accent bars, dormant state warning banner, staggered card animations, dimmed expired/revoked cards - File Sharing service: "Browse shared files" clickable link opens local file server in browser when tunnel is connected - Service public URLs now clickable (opens in browser) - Image preview fix: read files as base64 data URLs via IPC to bypass CSP restrictions; added img-src data: to CSP - Staggered fadeIn entrance animations on all pages (Connections, Resources, Settings) matching Home page - qurl-files route self-heals on every tunnels:list poll - Removed initFileServer cleanup that was deleting qurl-files route - Resources page header layout aligned with other pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rnal - Periodic cleanup (60s) removes shared files when all QURLs are expired/revoked — uses apiRequest directly since SDK client.get() doesn't return nested qurls array - File server directory listing redesigned: shows actual filenames instead of token IDs, with file sizes, dates, and LayerV branding - "Browse shared files" opens system browser via shell.openExternal IPC bridge instead of Electron window - Settings auto-save on every change — removed Save button, auto-start tunnel persisted to file-based defaults (not localStorage), tunnel actually starts on launch when enabled - Resources: conservative default state (dormant) until detail loaded to prevent false green flash on navigation - Removed stale "Default Access Policy" label from Settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add self-update infrastructure for both the standalone tunnel utility and the Electron desktop app. CLI users get `qurl-frpc update` for one-command updates; desktop users see a silent background download with a "Relaunch to apply" banner (matching Claude desktop pattern). Go: pkg/selfupdate with semver parser, GitHub Releases check, tarball download/extract, and atomic binary replacement with rollback. 36 tests including httptest mocking and end-to-end verification. CLI: `qurl-frpc update [--check] [--json]` subcommand. Desktop: UpdateManager in main process with 4-hour periodic checks, ETag caching, staged downloads to ~/.config/qurl/.update-staging/, IPC bridge (update:check, update:applyAndRelaunch, app:version), and sidebar banner with Relaunch button. Docs: README updated with installation, updating, and release workflow sections. Desktop README added with architecture and update flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Handle all unchecked error returns in selfupdate.go (gz.Close, f.Close, in.Close, out.Close) and selfupdate_test.go (json.Encode, w.Write, os.WriteFile, os.MkdirAll, os.Chmod) - Skip TestApply_Rollback when running as root (root bypasses permission checks, making the read-only directory test invalid) - Fix TestExtractTarGz_DirectoryTraversal: old test checked for existence of destDir/../../../etc/passwd which resolves to /etc/passwd (always exists on Linux); now verifies no traversal entries appear inside destDir https://claude.ai/code/session_01Syj2YuGwPHuzQsc66DsGFd
Use serveJSON/serveBytes test helpers for checked error handling. Fix directory traversal test to verify within destDir (Linux compat). Add root-user skip for permission-based rollback test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sistency - Refactor status tabs: Active now means resources with active QURLs, new Inactive tab for dormant resources, color-coded dots and badges - Add type filter (file/tunneled/private/public) and date range filter with custom pill-button dropdowns matching tab styling - File type detection: qurl-files tunnel subdomain now correctly identified as file type instead of tunneled - Create form: add file drag-drop support with deferred creation, "or" divider between URL input and drop zone, file chip with clear - Apply QURL defaults from Settings when opening create form, switch between URL/file defaults based on input mode - Mint link: auto-prefill label from resource name with incrementing suffix, e.g. "report.pdf (2)" - Fix double-advanced bug in AccessPolicyForm when advancedOnly is set - Promote Label to basic field with "(Optional)" in mint form - Remove duplicate session duration between mint form and AccessPolicyForm - Remove Copy Site URL button from access link rows - Remove redundant status labels (Sharing/No active links) from cards - Unify AccessPolicyForm: CSS grid for field alignment, consistent label styling, redesigned Advanced toggle as divider pattern, one-time use as proper field container, geo fields side by side - DropZone compact mode for inline use in create form - Match Add Service button styling to New Resource button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Separates the concept of Resources (static) from Qurls (dynamic/expiring). Sidebar menu now reads "Resources", file subtitle updated, and create forms show a "Qurl Settings" subsection heading below the label field. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…k, and settings cleanup - Merge tunnel controls and file sharing toggle into Home page (delete Dashboard) - Add electron-builder desktop build job to release workflow - Rework updater: electron-updater for packaged builds, progress tracking, installAppUpdate IPC - Fetch user email on API key sign-in from /v1/me - Rename QURL defaults keys (url→http, service→ssh) with legacy migration - Simplify Login callback, update types for new page IDs and app update status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ipeline - Fix CI: clone qurl-typescript in release workflow so the desktop build can resolve the file:../../qurl-typescript dependency - Generate SHA256SUMS and GPG-sign it (SHA256SUMS.asc) in the create-release job using crazy-max/ghaction-import-gpg@v7 - install.sh: refactor into download/verify/extract steps with SHA256 checksum verification and best-effort GPG signature check - Desktop updater: verify sidecar tarball SHA256 against SHA256SUMS before extracting, abort on mismatch - Docs: add GPG Release Signing section, renumber sections, update secrets checklist with GPG_PRIVATE_KEY and GPG_PASSPHRASE New GitHub secrets required: GPG_PRIVATE_KEY, GPG_PASSPHRASE Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FRP server is a single point of failure — when an instance dies, all tunnels drop. This commit ensures robust recovery at every layer: Go side: - Set LoginFailExit=false so the client retries indefinitely on login failure instead of exiting (FRP's default exits on first failure) - Lower TCP keepalive from 7200s (2hr!) to 60s for fast dead-server detection - Expose server.keepalive, server.dial_timeout, server.login_fail_exit in YAML config with sane defaults - Wire transport settings into FRP v1 config generation - Fix signal handling for graceful shutdown on all protocols (was KCP/QUIC only) - Enrich QURL API heartbeat with connection status derived from the FRP admin API Desktop side: - Add auto-restart with exponential backoff (2s→60s, max 10 attempts) when qurl-frpc crashes — distinguishes intentional stop from crash - Add getConnectionState() that queries FRP admin API to detect actual tunnel connectivity, not just process liveness - Surface tri-state tunnel status: Connected (green), Reconnecting (yellow/pulsing), Disconnected — replaces binary running/stopped - Push state changes to renderer via IPC events for instant UI updates - Update Home, Settings, tray, and StatusBadge for reconnecting state Documentation: - Add docs/reconnection.md covering all three layers, failure timelines, config reference, and NHP knock interaction Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resources tab now creates resources without auto-creating QURLs. Users mint QURL links separately after resource creation. Adds resources.list() and resources.create() IPC endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…prove card responsiveness - Replace "Inactive" filter with "No Active QURLs" so newly created resources (with no QURLs yet) appear in the Active tab immediately - Fix SSH sub-tab by tagging SSH resources with "SSH:" description prefix and using filterFn-based matching instead of empty typeMatch - Add optional description field to file and HTTP resource creation forms; SSH mode auto-populates description from the Name field - Make Home tab status cards responsive (2-col on narrow, 3-col on wide) and fix file sharing toggle overflow on narrow windows - Add h-full/flex-col for consistent card heights across the grid Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lean up ternary - Hoist SSH description regex to module-level constant (SSH_PREFIX_RE) - HTTP filterFn references MODE_META.http.typeMatch instead of duplicating the array inline - Narrow fetchResources useCallback dep from [meta] to [mode] to avoid unnecessary callback recreation - Simplify disabled ternary: (mode === 'ssh' ? false : X) → (mode !== 'ssh' && X) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Go side: - Cache getMachineID() with sync.Once (was spawning subprocess + HMAC on every 30s heartbeat) - Hoist http.Client to package-level var (was allocating per call) - Extract adminHost/adminPort constants (was hardcoded in 3 places) - Pass machineID to probeAdminStatus instead of re-deriving Desktop side: - Unify ConnectionState to 'connected'|'reconnecting'|'disconnected' (was 'running' in sidecar, 'connected' in StatusBadge — forced every consumer to map between them) - Add 3s TTL cache to getConnectionState() — tray and renderer both poll every 5s, this deduplicates admin API calls - getConnectionState() now calls emitState() on change — fixes gap where push events missed degraded connections (only fired on process exit, not when admin API showed proxies reconnecting) - Cache getAdminAuth() (was reading config YAML from disk on every admin API call) - Extract resolveConnectionState() helper (was repeated 4x inline) - Move TunnelState type to module scope - Add no-op guard to tray menu rebuild - Add change guard to push event handler in Home.tsx - Use optional chaining in getStatus() (this.process?.pid) - Remove WHAT-explaining comments and misleading scheduleRestart comment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- install.sh: extract fetch_file() helper to eliminate 3x curl/wget copy-paste; rename TMPDIR to DL_DIR to avoid POSIX shadow; remove narration comments - updater.ts: use options object instead of 3 positional params; move staging cleanup to try/finally in downloadUpdate() so verifyChecksum() doesn't own caller's lifecycle; remove narration comments Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Download() now saves the tarball to disk, fetches SHA256SUMS from the release, and verifies the hash before extracting. Verification is mandatory — missing or mismatched checksums abort the update. - Add downloadToFile(), verifyChecksum(), fetchExpectedHash(), fileSHA256() - Add ChecksumBaseURL field to Updater for test overriding - Add TestDownload_ChecksumMismatch, TestDownload_ChecksumUnavailable - Update TestDownload, TestDownload_MissingBinary, and end-to-end test to serve SHA256SUMS from the test server Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract doGet() to eliminate 3x repeated HTTP request boilerplate (CheckForUpdate, downloadToFile, fetchExpectedHash) - Cache default http.Client on Updater so TCP/TLS connections are reused across sequential requests (tarball + checksum fetch) - Thread ua string instead of version to reduce parameter confusion - Remove redundant maxDownloadSize LimitReader on extraction read (file was already capped during download) - Fix end-to-end test: use closure over release var instead of fragile srv.Config.Handler mutation; removes duplicated handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nd rate limiting Protects the FRP NewProxy hot path from qurl-service outages. When QURL_API_URL and QURL_API_TOKEN are set, the server registers an HTTP plugin that validates proxy registrations through a circuit breaker (trips after 5 failures, 30s recovery), TTL resource cache (60s), and token bucket rate limiter (50 req/s, burst 100). All paths fail-closed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds pkg/tunnelauth with circuit breaker, resource cache, and rate limiter to protect the FRP NewProxy hot path from qurl-service outages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts: # desktop/src/renderer/pages/Home.tsx
…econnect - Home card uses resources.list() to count all active resources across modes - Rename "Qurls" to "Resources" on Home page - Remove orange "No Active QURLs" filter tab and dormant resource styling - All non-revoked resources display green regardless of active link count - Fix tunnel falsely reporting "connected" before admin API confirms proxies - Add connection poller with exponential backoff (2s-30s) for reconnecting - Clean up poll timers on stop/exit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete MVP implementation for the QURL Reverse Proxy, transforming the NHP-FRP project into a first-class QURL ecosystem component.
run,add,remove,list,version,install-service,statusqurl-proxy.yaml) with FRP config generation, env var resolution, and discoveryIssues Addressed
Closes #9, #10, #11, #12, #15, #16, #18, #19, #20, #24, #25
Test plan
go test -race ./pkg/...— all 5 packages pass (apiclient, audit, config, middleware, service)go vet ./cmd/frpc ./cmd/frps ./pkg/...— cleanCGO_ENABLED=0 go build -tags frps ./cmd/frps— server buildsgo vet ./cmd/frpc— client vets clean (CGO build requires SDK)qurl-frpc runconnects to FRP serverqurl-frpc add --target http://localhost:8080 --name testcreates routenpm install && npm run devlaunches Electron window🤖 Generated with Claude Code