{"id":49748851,"url":"https://github.com/philbir/tap","last_synced_at":"2026-05-10T08:05:13.838Z","repository":{"id":355720163,"uuid":"1228045701","full_name":"philbir/tap","owner":"philbir","description":"Aspire-friendly HTTP traffic inspector and Cloudflared tunnel integration","archived":false,"fork":false,"pushed_at":"2026-05-04T20:36:10.000Z","size":3308,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-05T01:14:27.813Z","etag":null,"topics":["aspire","dev-tunnel","http-inspection"],"latest_commit_sha":null,"homepage":"https://philbir.github.io/tap/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/philbir.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-03T14:20:07.000Z","updated_at":"2026-05-04T20:46:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/philbir/tap","commit_stats":null,"previous_names":["philbir/tap"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/philbir/tap","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philbir%2Ftap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philbir%2Ftap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philbir%2Ftap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philbir%2Ftap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/philbir","download_url":"https://codeload.github.com/philbir/tap/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philbir%2Ftap/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32751758,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T02:14:30.463Z","status":"ssl_error","status_checked_at":"2026-05-07T02:14:29.405Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspire","dev-tunnel","http-inspection"],"created_at":"2026-05-10T08:05:12.425Z","updated_at":"2026-05-10T08:05:13.798Z","avatar_url":"https://github.com/philbir.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cp\u003e\n    \u003cimg src=\"assets/tap-logo.svg\" alt=\"Tap\" width=\"150\"\u003e\n  \u003c/p\u003e\n\n  \u003cpicture\u003e\n    \u003csource srcset=\"assets/tap-hero-dark.png\" media=\"(prefers-color-scheme: dark)\"\u003e\n    \u003cimg src=\"assets/tap-hero.png\" alt=\"Tap tunnel and HTTP inspector illustration\" width=\"620\"\u003e\n  \u003c/picture\u003e\n\n  \u003cp\u003e\u003cstrong\u003eEasy tunneling with an HTTP inspector built in.\u003c/strong\u003e Test mobile app hooks, webhook deliveries, auth callbacks, partner integrations, and temporary demos from your local machine without changing the app you are building.\u003c/p\u003e\n\n  \u003cp\u003e\n    \u003ca href=\"https://philbir.github.io/tap/\"\u003e\u003cstrong\u003eLanding page and docs\u003c/strong\u003e\u003c/a\u003e\n  \u003c/p\u003e\n\n  \u003cp\u003e\n    \u003ca href=\"https://philbir.github.io/tap/\"\u003e\u003cimg alt=\"Docs\" src=\"https://img.shields.io/badge/docs-GitHub%20Pages-14945f\"\u003e\u003c/a\u003e\n    \u003cimg alt=\".NET\" src=\"https://img.shields.io/badge/.NET-10-512bd4?logo=dotnet\"\u003e\n    \u003cimg alt=\"Aspire\" src=\"https://img.shields.io/badge/Aspire-ready-7b2ff7\"\u003e\n    \u003cimg alt=\"Cloudflare Tunnel\" src=\"https://img.shields.io/badge/Cloudflare-Tunnel-f38020?logo=cloudflare\"\u003e\n    \u003cimg alt=\"Tailscale Funnel\" src=\"https://img.shields.io/badge/Tailscale-Funnel-5e64f4?logo=tailscale\"\u003e\n    \u003cimg alt=\"UI\" src=\"https://img.shields.io/badge/UI-React%2019-14945f?logo=react\"\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n\n---\n\nTap is for the local-development moment when you need a real public URL and a clear view of what hit it. Mobile app development hooks, webhook deliveries, third-party OAuth redirects, auth provider callbacks, partner integrations, and \"can you hit my laptop for a minute?\" demos all need the same thing: a tunnel that is quick to bring up and a request log that tells you what actually happened.\n\nTap gives you both. Run it directly from the `tap` CLI when you want an ad hoc tunnel for one upstream, or add it to a .NET Aspire AppHost when tunnel wiring should live beside the rest of your distributed app.\n\nQuick tunnels are free with TryCloudflare and do not need a Cloudflare account. If you want stable hostnames, use a free Cloudflare account with a domain you control; a `.dev` domain is a nice fit for developer projects and is usually inexpensive depending on registrar. Tap itself is meant to feel like tap water: free, useful, and available whenever you need another glass.\n\n\u003e [!IMPORTANT]\n\u003e Tap makes local services reachable through public URLs. Treat exposed endpoints as internet-facing. Prefer short-lived TryCloudflare tunnels for quick demos, use Cloudflare Access or Tap's inspector auth options for sensitive services, and never tunnel a privileged local admin endpoint without an explicit access boundary.\n\n\u003e [!WARNING]\n\u003e **Public tunnels are scanned within minutes.** The moment a public hostname's TLS cert appears in a CT log (which happens immediately when you bring up Cloudflare Tunnel or Tailscale Funnel), opportunistic scanners hit it looking for admin endpoints, debug routes, and known-CVE banners. **Always pair public tunnels with auth or edge controls.** Tap's auth options (header / CIDR / country / OIDC) gate the proxy port before traffic reaches your upstream; for Cloudflare hostnames, Cloudflare Access and WAF rules are another good outer layer. Those attempts show up directly in the Inspector request log, often seconds after the tunnel is reachable. For Tailscale, prefer `WithTailscaleServe(...)` (tailnet-only) over `WithTailscaleFunnel(...)` (public) unless you actually need internet exposure.\n\n## Run Modes\n\n| | When to use |\n|---|---|\n| **CLI** | You want to point Tap at an upstream URL now: `tap run http://localhost:3000 --quick`. |\n| **Aspire** | You want tunnels and inspectors modeled in your AppHost with generated resource URLs. |\n| **Standalone inspector** | You want a local capture proxy without Cloudflare. |\n| **Quick tunnel** | You need a throwaway `*.trycloudflare.com` URL with no Cloudflare account or DNS setup. |\n| **Existing tunnel** | You already manage a tunnel in the Cloudflare dashboard and want Tap to run `cloudflared --token` against it. |\n| **API-managed tunnel** | You want the AppHost to look up or create a named tunnel, write local credentials, and manage DNS. |\n| **Dynamic hostname** | You want fresh per-run hostnames such as `api-1a2b3c4d-tap.example.com` for demos or parallel dev loops. |\n| **Tailscale Serve (default)** | Tailnet-only: reachable from your other tailnet devices but not the public internet. The safe default for Tailscale. |\n| **Tailscale Funnel (public, opt-in)** | Public URL via your tailnet node — pair with auth. |\n| **Tailscale (ephemeral)** | AppHost / CLI: spin up a per-session userspace `tailscaled` from an auth key (Process or Docker). Node disappears when the run stops. |\n\n## Use Cases\n\n| Use case | Why Tap helps |\n|---|---|\n| **Mobile app callbacks** | Point native or emulator builds at a public URL while still serving from localhost. The inspector's **QR tab** (or `http://localhost:\u003cuiPort\u003e/#qr`) lets you scan the public URL straight onto your phone. |\n| **Webhook development** | See the raw headers, body, status code, and replay path for every provider delivery. |\n| **Auth callbacks** | Test OAuth/OIDC redirect URIs against a real HTTPS hostname. |\n| **Streaming protocols** | Tap proxies and live-captures **Server-Sent Events** (`text/event-stream`) and **WebSockets** end-to-end. The inspector UI renders dedicated **SSE** and **WS** tabs with a live frame/event timeline (direction, payload, timestamps) — open while the connection is in flight and watch messages append in real time. |\n| **Partner demos** | Share a temporary URL to work running on your machine, then tear it down. |\n| **Aspire demos** | Put the same tunnel and inspector wiring in the AppHost so the whole team gets it. |\n\n## Install\n\nPick whichever fits — all three install the same `tap` CLI.\n\n### .NET global tool\n\nNeeds the .NET 10 SDK on PATH. Cross-platform.\n\n```bash\ndotnet tool install -g Tap\ndotnet tool update    -g Tap\ndotnet tool uninstall -g Tap\n```\n\nMake sure `~/.dotnet/tools` (Linux/macOS) or `%USERPROFILE%\\.dotnet\\tools` (Windows) is on your `PATH`.\n\n### Self-contained binary (Linux/macOS)\n\nNo .NET install required. Downloads the latest release for your platform, verifies the SHA256 checksum, and writes a launcher to `~/.local/bin/tap`.\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/philbir/tap/main/install.sh | sh\n```\n\nPin a version with `TAP_VERSION=0.1.0 ...`, override paths with `TAP_INSTALL_DIR` / `TAP_BIN_DIR`. To uninstall: `rm -rf ~/.local/share/tap ~/.local/bin/tap`.\n\nArchives are also available directly from the [GitHub Releases](https://github.com/philbir/tap/releases) page as `tap-\u003cversion\u003e-\u003crid\u003e.tar.gz`, with a `SHA256SUMS` file alongside.\n\n### Windows one-liner\n\nWraps the .NET global-tool install — needs the .NET 10 SDK on PATH.\n\n```powershell\nirm https://raw.githubusercontent.com/philbir/tap/main/install.ps1 | iex\n```\n\nPin a version with `$env:TAP_VERSION = \"0.2.3\"` before running. To uninstall: `dotnet tool uninstall -g Tap`.\n\n### cloudflared\n\nCloudflare-tunnel features need [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) on `PATH`. Install it once with `brew install cloudflared`, `winget install Cloudflare.cloudflared`, or run `tap install-cloudflared` after Tap is installed.\n\n### Tailscale\n\nTailscale support can use `tailscale serve` for private tailnet-only access or `tailscale funnel` for public internet access. Host-process modes need the [`tailscale`](https://tailscale.com/download) CLI on `PATH`; Docker mode runs the official `tailscale/tailscale` image instead. One-time tailnet setup:\n\n1. Install Tailscale (`brew install tailscale` on macOS, `tailscale.com/download/linux` on Linux, or the GUI installer on Windows) and sign in with `tailscale up` when using system mode.\n2. In the [admin console](https://login.tailscale.com/admin/), enable **HTTPS Certificates** under DNS. This is required for both `serve` and `funnel`.\n3. For public Funnel only, add a `nodeAttrs` rule to your tailnet ACL granting the `funnel` capability:\n\n```json\n{\n  \"nodeAttrs\": [\n    { \"target\": [\"*\"], \"attr\": [\"funnel\"] }\n  ]\n}\n```\n\nVerify with `tailscale status --json | grep -i funnel` — `\"funnel\"` should appear in your node's `CapMap`. Funnel only listens on ports `443`, `8443`, and `10000`. `tailscale serve` is the Tap default and stays private to your tailnet.\n\n## Quick Start\n\n### CLI\n\n```bash\ntap run http://localhost:3000\n```\n\nThat starts the inspector with a local proxy on \u003chttp://localhost:4444\u003e and the UI on \u003chttp://localhost:4445\u003e.\n\nAdd a quick TryCloudflare tunnel:\n\n```bash\ntap run http://localhost:3000 --quick\n```\n\nUse an existing dashboard-managed tunnel token:\n\n```bash\ntap run http://localhost:3000 \\\n  --token \"$CLOUDFLARE_TUNNEL_TOKEN\" \\\n  --hostname api-local.example.com\n```\n\nUse Cloudflare API-managed DNS and a fresh dynamic hostname:\n\n```bash\ntap run http://localhost:3000 \\\n  --api-token \"$CLOUDFLARE_API_TOKEN\" \\\n  --account \"$CLOUDFLARE_ACCOUNT_ID\" \\\n  --api-managed tap-cli \\\n  --dynamic example.com\n```\n\nIf `cloudflared` is not installed, run:\n\n```bash\ntap install-cloudflared\n```\n\nUse Tailscale (system tailscaled — requires the Tailscale CLI on PATH and a tailnet you're signed in on):\n\n```bash\n# Tailnet-only (safe default — reachable from your other tailnet devices, not the public internet):\ntap run http://localhost:3000 --tailscale\n\n# Public Funnel (URL is on the internet — pair with auth):\ntap run http://localhost:3000 --tailscale --tailscale-public \\\n  --auth-header \"X-Tap-Key=$TAP_KEY\"\n```\n\nOr run a per-session userspace `tailscaled` with an auth key (no system Tailscale install needed beyond the CLI):\n\n```bash\nexport TAILSCALE_AUTHKEY=tskey-...        # or pass --tailscale-authkey\ntap run http://localhost:3000 --tailscale\n```\n\nThe CLI spawns `tailscaled --tun=userspace-networking` under a temp state dir, runs `tailscale up --authkey ...`, configures `tailscale serve` (or `funnel` with `--tailscale-public`), and tears everything down (including the state dir) on Ctrl+C. macOS/Linux only — on Windows pair the auth key with `--docker` to use the `tailscale/tailscale` container.\n\nDon't have a host `tailscaled` binary? Run the official `tailscale/tailscale` Docker image instead — works on any host with Docker, including macOS where the GUI Tailscale client doesn't expose `tailscaled`:\n\n```bash\nexport TAILSCALE_AUTHKEY=tskey-...\ntap run http://localhost:3000 --tailscale --docker\n```\n\nThe same `--docker` flag controls Cloudflare and Tailscale: with `--tailscale` it runs `tailscale/tailscale`; without, it runs `cloudflare/cloudflared`. For Tailscale, tap starts the container with `TS_USERSPACE=true` and drives funnel config via `docker exec` (bind-mounted unix sockets don't survive macOS Docker Desktop's VM boundary). The container reaches the inspector through Docker's `host.docker.internal` host-gateway alias (auto on Docker Desktop; `--add-host` is added on Linux). Container is `--rm` and force-removed on shutdown.\n\nCLI options can also come from environment variables and an optional `tap.config` file. Command-line flags win, then environment variables, then config file defaults.\n\n```json\n{\n  \"upstream\": \"http://localhost:3000\"\n}\n```\n\n### Aspire: standalone inspection\n\n```csharp\nusing Aspire.Hosting;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar api = builder.AddProject\u003cProjects.Sample_Api\u003e(\"api\");\n\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e();\napi.WithTap(tap);\n\nbuilder.Build().Run();\n```\n\nOpen \u003chttp://localhost:5198\u003e for the inspector UI. Send traffic through \u003chttp://localhost:5199\u003e and Tap records the request, response, headers, status, timing, and supported bodies before forwarding to the upstream service. WebSocket upgrades and Server-Sent Events are forwarded through the same proxy port; their frames/events stream live in the inspector's **WS** and **SSE** tabs.\n\n### Aspire: quick public tunnel\n\n```csharp\nusing Aspire.Hosting;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar api = builder.AddProject\u003cProjects.Sample_Api\u003e(\"api\");\n\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e(\n        name: \"tap-quick\",\n        proxyPort: 5307,\n        uiPort: 5306)\n    .WithQuickTunnel();\n\napi.WithTap(tap);\n\nbuilder.Build().Run();\n```\n\n`cloudflared` assigns a random TryCloudflare URL at startup. Tap watches the tunnel logs, surfaces the public URL on the tap, and routes Cloudflare traffic through the tap before it reaches `api`.\n\n### Aspire: existing Cloudflare tunnel\n\n```csharp\nusing Aspire.Hosting;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar api = builder.AddProject\u003cProjects.Sample_Api\u003e(\"api\");\n\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e()\n    .WithTunnel(\"tap-tunnel\", t =\u003e\n        t.WithExistingTunnel(builder.Configuration[\"Cloudflare:TunnelToken\"]));\n\napi.WithTap(tap, \"api-local.example.com\");\n\nbuilder.Build().Run();\n```\n\nConfigure the token with user-secrets:\n\n```bash\ndotnet user-secrets set Cloudflare:TunnelToken \"\u003ctoken\u003e\" \\\n  --project samples/Sample.AppHost\n```\n\n`WithExistingTunnel` expects a Cloudflare Tunnel you have already created. Create the tunnel in the Cloudflare dashboard first, copy its connector token, and pass that token to Tap. Tap will run `cloudflared tunnel run --token ...`; it will not create or reconfigure that dashboard-managed tunnel.\n\n### Aspire: Tailscale (private by default)\n\n```csharp\nusing Aspire.Hosting;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar api = builder.AddProject\u003cProjects.Sample_Api\u003e(\"api\");\n\n// Tailnet-only — reachable only from other devices on your tailnet (the safe default).\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e(mode: \"tunnel\")\n    .WithTailscaleServe(\"tap-serve\", t =\u003e t.WithSystemDaemon());\napi.WithTap(tap);\n\nbuilder.Build().Run();\n```\n\nFor a public URL on the internet (pair with auth!):\n\n```csharp\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e(mode: \"tunnel\")\n    .WithTailscaleFunnel(\"tap-funnel\", t =\u003e t.WithSystemDaemon())\n    .WithHeaderAuth(\"X-Tap-Key\", builder.Configuration[\"Tap:Key\"]!);\napi.WithTap(tap);\n```\n\nFor a per-session userspace daemon (clean tailnet membership, throw-away node):\n\n```csharp\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e(mode: \"tunnel\")\n    .WithTailscaleFunnel(\"tap-funnel\", t =\u003e t\n        .WithEphemeralDaemon(builder.Configuration[\"Tailscale:AuthKey\"]!)\n        .WithFunnelPort(8443));   // 443 (default), 8443, or 10000\napi.WithTap(tap);\n```\n\nOr run the userspace daemon in Docker (`tailscale/tailscale` image — useful on macOS where the GUI client doesn't expose a `tailscaled` binary):\n\n```csharp\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e(mode: \"tunnel\")\n    .WithTailscaleFunnel(\"tap-funnel\", t =\u003e t\n        .WithEphemeralDaemon(builder.Configuration[\"Tailscale:AuthKey\"]!),\n        hostMode: TailscaleHostMode.Docker);\napi.WithTap(tap);\n```\n\nIn Docker mode the funnel target is auto-rewritten from `localhost:\u003cport\u003e` to `host.docker.internal:\u003cport\u003e` so the container can reach the inspector on the host. The companion `tailscaled` Aspire resource shows up in the dashboard as a `docker run` child of the funnel resource — its logs are the container's logs, and Aspire's shutdown kills the docker process which `--rm`s the container.\n\nFunnel exposes one URL per tailnet node, so each `WithTailscaleFunnel(...)` is bound to exactly one upstream — register multiple funnels for multiple upstreams. Tap shells out to `tailscale funnel` and parses MagicDNS for the public URL; the `TailscaleLifecycleHook` provisions everything before the funnel resource starts and removes only the path-specific rule on shutdown so other funnel/serve rules survive.\n\n### Aspire: API-managed tunnel and DNS\n\n```csharp\nusing Aspire.Hosting;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar api = builder.AddProject\u003cProjects.Sample_Api\u003e(\"api\");\n\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e()\n    .WithTunnel(\"tap-tunnel\", t =\u003e t\n        .WithApiManagedTunnel(\n            builder.Configuration[\"Cloudflare:ApiToken\"]!,\n            builder.Configuration[\"Cloudflare:AccountId\"]!,\n            tunnelName: \"tap-dev\")\n        .WithDynamicHostname(\"example.com\", prefix: \"api-\", suffix: \"-tap\"));\n\napi.WithTap(tap);\n\nbuilder.Build().Run();\n```\n\nThe lifecycle hook runs before `cloudflared` starts. It looks up or creates the named tunnel, writes a temporary credentials file, resolves the Cloudflare zone, mints hostnames when needed, ensures CNAME records, and then starts `cloudflared` with a local ingress config.\n\n## CLI Reference\n\n| Option | Purpose |\n|---|---|\n| `\u003cupstream\u003e` | Target URL to inspect, for example `http://localhost:3000`. |\n| `--proxy-port` | Captured traffic port. Default `4444`. |\n| `--ui-port` | Inspector UI/API port. Default `4445`. |\n| `--quick` | Start a TryCloudflare quick tunnel. |\n| `--token` | Connector token for an existing Cloudflare Tunnel. |\n| `--hostname` | Public hostname for token or API-managed mode. |\n| `--api-token` | Cloudflare API token for managed tunnel/DNS operations. |\n| `--account` | Cloudflare account id. |\n| `--api-managed` | Named tunnel to create or reuse. |\n| `--dynamic` | Zone where Tap should mint a fresh hostname. |\n| `--docker` | Run the active provider in Docker. With `--tailscale`: `tailscale/tailscale` (ephemeral, userspace networking). Without: `cloudflare/cloudflared`. |\n| `--auto-install` | Install `cloudflared` if missing. |\n| `--tailscale` | Route through Tailscale (system `tailscaled` by default — tailnet-only via `tailscale serve`; pair with `--tailscale-public` for `tailscale funnel`). |\n| `--tailscale-public` | Switch from `serve` (tailnet-only, default) to `funnel` (public internet). Pair with auth flags. |\n| `--tailscale-port` | Funnel/serve port. Allowed: `443` (default), `8443`, `10000`. |\n| `--tailscale-authkey` | Auth key. Switches to ephemeral mode — the CLI spawns a userspace `tailscaled` per session and joins the tailnet with the key. Env: `TAILSCALE_AUTHKEY`. |\n| `--tailscale-system` | Force system mode even when an auth key is present (CLI flag, env, or profile). Use when `TAILSCALE_AUTHKEY` is exported globally but you want one run on the host's existing node. |\n| `--tailscale-login-server` | Override Tailscale coordination server (Headscale, etc.). Env: `TAILSCALE_LOGIN_SERVER`. |\n| `--config` | Load defaults from a JSON `tap.config` file. |\n\nUseful environment variables:\n\n| Variable | Purpose |\n|---|---|\n| `TAP_UPSTREAM` | Upstream URL when omitted from the command line. |\n| `CLOUDFLARE_TUNNEL_TOKEN` | Token tunnel connector token. |\n| `CLOUDFLARE_API_TOKEN` | API-managed tunnel token. |\n| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account id. |\n| `TAILSCALE_AUTHKEY` | Tailscale auth key — picked up by `--tailscale` to enable ephemeral mode. |\n| `TAILSCALE_LOGIN_SERVER` | Override Tailscale coordination server (Headscale, etc.). |\n\n## Tailscale Setup\n\n\u003e [!CAUTION]\n\u003e Default to **`tailscale serve`** (tailnet-only). Only switch to **`tailscale funnel`** (public) when you actually need internet exposure, and always pair public tunnels with auth — opportunistic scanners hit new public hostnames within minutes.\n\nSystem mode (CLI + AppHost):\n\n1. Install Tailscale and run `tailscale up` so the node is authenticated.\n2. Enable **HTTPS Certificates** in the admin console (one-time per tailnet — needed for both `serve` and `funnel`).\n3. For Funnel only: grant the `funnel` capability via tailnet ACL `nodeAttrs` (see the install section above). `serve` mode doesn't need this.\n\nEphemeral mode (CLI + AppHost):\n\n1. Generate a reusable auth key in the admin console under **Settings → Keys** and apply tags that grant the `funnel` capability.\n2. CLI: pass `--tailscale-authkey`, set `TAILSCALE_AUTHKEY`, or save the key in a profile. Tap spawns `tailscaled --tun=userspace-networking` for the run, then tears it down on Ctrl+C.\n3. AppHost: stash it in user-secrets with `dotnet user-secrets set Tailscale:AuthKey \"tskey-...\" --project samples/Sample.AppHost`, then use `WithEphemeralDaemon(authKey)`.\n4. Windows ephemeral process mode is not supported; pair the auth key with `--docker` in the CLI or `hostMode: TailscaleHostMode.Docker` in Aspire.\n\nThe inspector dialog (Tunnel chip in the Inspector header) shows live daemon state — backend state, MagicDNS name, tailnet, version — and a table of every active `tailscale funnel` / `serve` rule on the node.\n\n## Cloudflare Setup\n\nFor token mode:\n\n1. In Cloudflare Zero Trust, create a Cloudflare Tunnel.\n2. Copy the `cloudflared tunnel run --token ...` connector command.\n3. Use only the token value with `tap run --token` or `WithExistingTunnel(...)`.\n4. Route the hostname you pass to Tap to that tunnel in Cloudflare.\n\nFor API-managed mode:\n\n1. Create a Cloudflare API token with account-level Cloudflare Tunnel edit permission.\n2. Add DNS edit permission for the zone Tap will manage.\n3. Provide `Cloudflare:ApiToken` and `Cloudflare:AccountId` through user-secrets, environment variables, or normal .NET configuration.\n4. Use `WithApiManagedTunnel(...)`; add `WithDynamicHostname(...)` when Tap should mint hostnames and DNS CNAMEs.\n\nCloudflare references: [tunnel tokens](https://developers.cloudflare.com/tunnel/advanced/tunnel-tokens/) and [API token permissions](https://developers.cloudflare.com/fundamentals/api/reference/permissions/).\n\n## Authentication\n\nTap auth gates the proxy branch before traffic reaches the upstream. The inspector UI port stays local and is not gated by these checks.\n\nCLI static checks:\n\n```bash\ntap run http://localhost:3000 --quick \\\n  --auth-header \"X-Tap-Key=$TAP_KEY\" \\\n  --auth-cidr \"203.0.113.0/24\" \\\n  --auth-country \"CH\"\n```\n\nCLI OIDC:\n\n```bash\ntap run http://localhost:3000 --quick \\\n  --auth-oidc-authority \"https://issuer.example.com\" \\\n  --auth-oidc-client-id \"$OIDC_CLIENT_ID\" \\\n  --auth-oidc-client-secret \"$OIDC_CLIENT_SECRET\"\n```\n\nAspire auth:\n\n```csharp\nvar tap = builder.AddTap\u003cProjects.Tap_Server\u003e()\n    .WithHeaderAuth(\"X-Tap-Key\", builder.Configuration[\"Tap:Key\"]!)\n    .WithIpAllowList(\"203.0.113.0/24\")\n    .WithCountryAllowList(\"CH\")\n    .WithOidcAuth(\n        authority: builder.Configuration[\"Auth:Authority\"]!,\n        clientId: builder.Configuration[\"Auth:ClientId\"]!,\n        clientSecret: builder.Configuration[\"Auth:ClientSecret\"]);\n\napi.WithTap(tap);\n```\n\nEnabled checks are combined. If header auth, CIDR allowlist, country allowlist, and OIDC are all configured, every request must satisfy every configured check.\n\n## Packages\n\n| Package | Purpose |\n|---|---|\n| `Tap.Hosting` | Aspire AppHost extensions: `AddTap`, `AddTapContainer`, `WithTap`, `tap.WithTunnel`, `tap.WithQuickTunnel`, `tap.WithTailscaleServe` (tailnet-only, default), `tap.WithTailscaleFunnel` (public, opt-in), `WithExistingTunnel`, `WithApiManagedTunnel`, `WithDynamicHostname`, `WithSystemDaemon`/`WithEphemeralDaemon`/`WithFunnelPort`. |\n| `Tap.Server` | ASP.NET Core capture server: YARP reverse proxy, capture middleware, WebSocket-terminating proxy, SSE event parser, REST API, `/api/stream` push channel, and bundled React UI with live **WS** and **SSE** message timelines. |\n| `Tap.Cli` | Local command host that reuses the same inspector server code. Tailscale system-mode profiles run from the CLI; ephemeral mode requires the AppHost. |\n\nBoth entry points use the same `Tap.Server` host internally. The CLI builds `TapInspectorOptions` from command-line flags, environment variables, and optional `tap.config`; Aspire writes the same options through project environment variables.\n\nConsumer AppHost projects must reference both `Tap.Hosting` and `Tap.Server`. `Tap.Server` supplies the generated `Projects.Tap_Server` metadata type used by `AddTap\u003cTTapServer\u003e()`; `Tap.Hosting` should be referenced with `IsAspireProjectResource=\"false\"` because it is a library, not a launchable resource.\n\n```xml\n\u003cProjectReference Include=\"..\\..\\src\\Tap.Hosting\\Tap.Hosting.csproj\"\n                  IsAspireProjectResource=\"false\" /\u003e\n\u003cProjectReference Include=\"..\\..\\src\\Tap.Server\\Tap.Server.csproj\" /\u003e\n```\n\n## Configuration\n\n### AppHost Cloudflare settings\n\n| Key | Purpose |\n|---|---|\n| `Cloudflare:TunnelToken` | Connector token for dashboard-managed token tunnels. |\n| `Cloudflare:ApiToken` | API token for API-managed tunnels, DNS, and tunnel details. |\n| `Cloudflare:AccountId` | Cloudflare account id used with `Cloudflare:ApiToken`. |\n| `Cloudflare:Zone` | Default zone used by the sample AppHost. |\n| `Cloudflare:Hostnames:*` | Optional sample hostnames for token and managed scenarios. |\n\nFor API-managed DNS, the Cloudflare token needs tunnel edit permission on the account and DNS edit permission on the relevant zone.\n\n### AppHost Tailscale settings\n\n| Key | Purpose |\n|---|---|\n| `Tailscale:AuthKey` | Auth key used by `WithEphemeralDaemon(authKey)` to spawn a userspace `tailscaled` per AppHost run. Reusable keys are recommended. |\n| `Tailscale:UseSystem` | Sample AppHost only: set to `true` to enable the system-daemon Tailscale scenario. |\n| `Tailscale:UseDocker` | Sample AppHost only: set to `true` (with `Tailscale:AuthKey`) to enable the Tailscale + Docker scenario. |\n\nThe sample AppHost can be filtered by provider:\n\n```bash\ndotnet run --project samples/Sample.AppHost                          # all scenarios (default)\ndotnet run --project samples/Sample.AppHost -- --scenarios tailscale  # standalone + ts-* only\ndotnet run --project samples/Sample.AppHost -- --scenarios cloudflare # standalone + cf-* only\n```\n\n### Inspector server settings\n\nTap.Hosting writes these for you when running under Aspire. The CLI maps its flags to the same server options.\n\n| Variable | Purpose |\n|---|---|\n| `Inspector__ProxyPort` | Port that receives proxied app traffic. Default `5199`. |\n| `Inspector__UiPort` | Port for the local inspector UI and API. Default `5198`. |\n| `Inspector__Mode` | `standalone` or `tunnel`. |\n| `Inspector__Provider` | `cloudflare` or `tailscale`. Gates provider-specific UI panes and API endpoints. |\n| `Inspector__Ingress` | JSON array of `{ hostname, upstream, tunnelMode, tunnelName, publicUrl }`. |\n| `Inspector__Tunnel__*` | Optional tunnel context surfaced by `/api/tunnel/details`. |\n| `Inspector__Tunnel__SocketPath` | Tailscale daemon socket path (set automatically in ephemeral mode). |\n| `Inspector__Auth__*` | Optional proxy-side auth gate: header, CIDR, country, and OIDC settings. |\n\n## Development\n\n```bash\ndotnet restore Tap.slnx\ndotnet build Tap.slnx\ndotnet run --project samples/Sample.AppHost\n```\n\nUI source lives in `ui/` and is built into `src/Tap.Server/wwwroot/` during server builds:\n\n```bash\ncd ui\nyarn\nyarn dev\nyarn build\n```\n\nUse `-p:SkipTapUiBuild=true` when iterating on C# only.\n\n### Docs site\n\n```bash\ncd docs-site\nyarn\nyarn build\nyarn preview\n```\n\nThe docs site is a static Vite app configured with `base: \"./\"` so the built `dist/` directory can be deployed under GitHub Pages project paths.\n\n## Architecture\n\nAt runtime Tap splits traffic across two ports:\n\n```text\nInternet -\u003e Cloudflare      -\u003e cloudflared -\u003e Tap proxy port -\u003e upstream app\n         -\u003e Tailscale Funnel -\u003e tailscaled  -\u003e Tap proxy port -\u003e upstream app\n                                            \\-\u003e Tap UI port -\u003e inspector UI/API\n```\n\nThe proxy branch captures request and response data, stores the latest records in a bounded in-memory ring, and publishes new records over server-sent events. WebSocket upgrade requests are intercepted by the capture middleware and re-originated against the upstream so that every text and binary frame can be recorded in both directions; the inspector renders them in a dedicated **WS** tab alongside the existing **SSE** view. The UI branch serves the React inspector and exposes REST endpoints for request history, replay, ingress, and tunnel details. With Cloudflare credentials configured the UI can show and update tunnel ingress rules; with Tailscale it shows live daemon state and active funnel/serve rules read from `tailscale status --json` and `tailscale serve status --json`.\n\nFor the deeper technical background, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).\n\n## Layout\n\n```text\nassets/              README logo and hero assets\ndocs/                Technical documentation\nsrc/Tap.Core/        Shared auth and Cloudflare/cloudflared primitives\nsrc/Tap.Hosting/     Aspire integration and lifecycle hook\nsrc/Tap.Server/      Capture server, YARP proxy, SSE API, bundled UI host\nsrc/Tap.Cli/         CLI host for the inspector server\nui/                  Vite + React inspector source\nsamples/             Sample AppHost and upstream API\n```\n\n## License\n\nTBD.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphilbir%2Ftap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphilbir%2Ftap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphilbir%2Ftap/lists"}