Skip to content
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
mainfrom
feat/mvp-core
Draft

feat: MVP core - rebrand, CLI, config, security, desktop app#27
Kevin-layerV wants to merge 40 commits into
mainfrom
feat/mvp-core

Conversation

@Kevin-layerV
Copy link
Copy Markdown

Summary

Complete MVP implementation for the QURL Reverse Proxy, transforming the NHP-FRP project into a first-class QURL ecosystem component.

  • Rebrand from NHP-FRP to QURL Reverse Proxy (module path, binaries, banners, configs, Docker)
  • Cobra CLI with subcommands: run, add, remove, list, version, install-service, status
  • YAML config (qurl-proxy.yaml) with FRP config generation, env var resolution, and discovery
  • QURL API client with retry logic, typed errors, and resource CRUD operations
  • Session validation middleware with JWT (RS256), one-time-use tracking, and fail-closed behavior
  • JSONL audit logging with async channel-based pipeline (non-blocking, 4K buffer)
  • Service management for macOS (launchd) and Linux (systemd)
  • CI/CD pipeline with lint, build, test, security scan, and release workflows
  • Desktop app scaffold (Electron + React + TypeScript) with sidecar management and file sharing UI
  • 11 test suites across all new packages (race-detector clean)

Issues 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/... — clean
  • CGO_ENABLED=0 go build -tags frps ./cmd/frps — server builds
  • go vet ./cmd/frpc — client vets clean (CGO build requires SDK)
  • Manual: qurl-frpc run connects to FRP server
  • Manual: qurl-frpc add --target http://localhost:8080 --name test creates route
  • Manual: Desktop app npm install && npm run dev launches Electron window

🤖 Generated with Claude Code

Kevin-layerV and others added 2 commits April 6, 2026 17:39
…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>
@Kevin-layerV Kevin-layerV marked this pull request as draft April 7, 2026 00:57
Kevin-layerV and others added 27 commits April 6, 2026 18:00
- 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>
Kevin-layerV and others added 11 commits April 14, 2026 11:21
…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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rebrand from NHP-FRP to QURL Reverse Proxy

2 participants