Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ PORTA_CORS_ORIGINS= # Required for a public host, e.g. https://porta.exam

# ── Cloudflare deployment (used by pnpm dev:cloud / pnpm deploy) ──
# PORTA_TUNNEL_NAME= # cloudflared tunnel name (e.g. my-porta-tunnel)
# PORTA_CLOUDFLARED_CONFIG= # config path when not using ~/.cloudflared/config.yml
# PORTA_CF_PROJECT= # Cloudflare Pages project name (e.g. porta)

# ── Public web dev server (used by pnpm dev / the Windows tunnel recipe) ──
Expand All @@ -25,4 +26,4 @@ PORTA_WEB_PORT=3070 # Must match the watchdog origin port and cloudflared

# ── Web (build-time, production only) ──
# VITE_API_BASE= # Absolute API URL (e.g. https://api.example.com)
# # Leave unset for local dev — Vite's proxy handles /api/* routing
# # Leave unset for local dev or the Cloudflare Pages Edge proxy
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.16.1] - 2026-09-04

### Changed

- Documented the complete Cloudflare named tunnel ingress configuration and
validation flow, including explicit custom config path handling. (#135)
- Added regression coverage for preserving the selected target engine across
page reloads and WebSocket reconnection after app resume. (#134)

### Security

- Named tunnel setup now requires Cloudflare Access before the proxy is exposed
and includes an explicit check that unauthenticated API requests are rejected.
(#135)
- Updated the web dependency graph from #131 and enforced patched transitive
versions for `@babel/core`, `@humanfs/node`, `brace-expansion`, `esbuild`,
`fast-uri`, `js-yaml`, `nanoid`, `postcss`, `serialize-javascript`, and
`undici` after the release security audit.

## [0.16.0] - 2026-08-17

### Added
Expand Down
76 changes: 60 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![CI](https://github.com/L1M80/porta/actions/workflows/ci.yml/badge.svg)](https://github.com/L1M80/porta/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
![Version](https://img.shields.io/badge/version-0.16.0-green)
![Version](https://img.shields.io/badge/version-0.16.1-green)

Remote web interface for [Antigravity](https://antigravity.google/) Agent Manager.
Access your local Antigravity sessions from your phone, tablet, or any remote browser through a lightweight LSP bridge.
Expand Down Expand Up @@ -148,7 +148,7 @@ To avoid stale copy-pasted instructions, follow Cloudflare's current docs:
- [Quick Tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)
- [Cloudflare Tunnel setup](https://developers.cloudflare.com/tunnel/setup/)

Use a named tunnel instead if you want a stable `VITE_API_BASE`, a fixed Cloudflare
Use a named tunnel instead if you want a stable API hostname, a fixed Cloudflare
Pages deployment, or long-lived remote access.

### Option B: Named tunnel + Pages (recommended for regular remote use)
Expand All @@ -159,7 +159,7 @@ This is the stable pattern for ongoing remote access. It requires:
- **Cloudflare Tunnel** (`cloudflared`) installed and authenticated
- A **Cloudflare Pages** project (for hosting the static SPA)
- A domain managed by **Cloudflare** for the tunnel hostname
- Optionally, **Cloudflare Zero Trust** for authentication
- **Cloudflare Zero Trust** for authentication before exposing the API

### 1. Configure `.env`

Expand All @@ -170,6 +170,7 @@ Set the proxy runtime and Cloudflare-related variables in `.env`:
PORTA_CORS_ORIGINS=https://<YOUR_PAGES_DOMAIN>
PORTA_TUNNEL_NAME=<YOUR_TUNNEL_NAME>
PORTA_CF_PROJECT=<YOUR_PROJECT_NAME>
PORTA_CLOUDFLARED_CONFIG=/path/to/config.yml
```

### 2. Create the named tunnel
Expand All @@ -181,13 +182,36 @@ cloudflared tunnel create <YOUR_TUNNEL_NAME>
cloudflared tunnel route dns <YOUR_TUNNEL_NAME> <YOUR_API_SUBDOMAIN>
```

Then add an ingress rule to your `cloudflared` config so the named tunnel sends
traffic to Porta's proxy:

```yaml
tunnel: <YOUR_TUNNEL_ID>
credentials-file: /path/to/<YOUR_TUNNEL_ID>.json

ingress:
- hostname: <YOUR_API_SUBDOMAIN>
service: http://127.0.0.1:3170
- service: http_status:404
```

Save this file at the path assigned to `PORTA_CLOUDFLARED_CONFIG`. This makes
sure `pnpm dev:cloud` starts `cloudflared` with the same config you validate.

You can verify the config resolves to the proxy before starting the tunnel:

```bash
cloudflared tunnel --config /path/to/config.yml ingress rule https://<YOUR_API_SUBDOMAIN>/api/health
```

### 3. Create `.env.production`

Create `.env.production` in the repo root for the web build:

```bash
# .env.production
VITE_API_BASE=https://<YOUR_API_SUBDOMAIN>
# Do not set VITE_API_BASE here. The frontend must use the Pages Edge proxy so
# Cloudflare Access service credentials are never exposed to the browser.
# Optional when hosting the web UI below a path such as https://example.com/porta/
PORTA_BASE_PATH=/porta
```
Expand All @@ -201,18 +225,13 @@ pnpm deploy
This uses `PORTA_CF_PROJECT` from `.env`. If you prefer, you can run the
equivalent `wrangler pages deploy` command manually.

### 5. Start the proxy + named tunnel

```bash
pnpm dev:cloud
```

This reads `PORTA_TUNNEL_NAME` from `.env` and starts the proxy and
`cloudflared tunnel run` together.
### 5. Secure your API with Cloudflare Access (Zero Trust)

### 6. Securing your API with Cloudflare Access (Zero Trust)
Complete this step **before starting the tunnel**. Porta's proxy exposes
conversation, command approval, file, and raw RPC endpoints and does not provide
its own authentication. CORS is not an authentication boundary.

Exposing your local API to the public internet can be dangerous. To completely lock down your setup, you should protect **both** your frontend and your API using Cloudflare Access.
Protect **both** your frontend and your API using Cloudflare Access.

Porta's built-in Edge Proxy securely bridges the two by injecting Machine-to-Machine authentication tokens, completely hiding your backend from the internet.

Expand Down Expand Up @@ -242,11 +261,36 @@ In your **Cloudflare Zero Trust** dashboard, under **Access > Applications**, yo

**5. Route Frontend Traffic Through the Proxy**
By default, the Porta frontend tries to fetch the API directly. To force it to use the secure Edge Proxy:
1. In your `.env.production` file, **remove or comment out** `VITE_API_BASE`.
1. Confirm your `.env.production` file does **not** define `VITE_API_BASE`.
2. Without `VITE_API_BASE`, the frontend falls back to root-relative API paths (`/api/*`), routing traffic through the Cloudflare Pages Edge proxy. `PORTA_BASE_PATH` only changes the frontend asset, router, and PWA paths.
3. Run `pnpm deploy` again.

> **Backwards Compatibility Note:** If `VITE_API_BASE` is defined, the frontend will bypass the proxy entirely and attempt to communicate directly with the backend. This is fully supported and recommended for local development (LAN access) or deployments where the backend is not protected by Cloudflare Access. Additionally, the proxy will gracefully skip Service Token injection if the `CF_ACCESS_CLIENT_ID` environment variables are missing.
> **Backwards Compatibility Note:** If `VITE_API_BASE` is defined, the frontend
> bypasses the Pages Edge proxy and connects directly to the backend. Use this
> only for local/LAN development or when the backend has its own authentication;
> never point it at an unprotected public tunnel. The Edge proxy skips Service
> Token injection when the `CF_ACCESS_CLIENT_ID` variables are missing.

### 6. Start the proxy + named tunnel

Only start the tunnel after the Backend API Access application and its Service
Auth policy are active:

```bash
pnpm dev:cloud
```

This reads `PORTA_TUNNEL_NAME` and `PORTA_CLOUDFLARED_CONFIG` from `.env`, then
starts the proxy and `cloudflared tunnel run` together.

After the tunnel connects, verify that an unauthenticated request is rejected:

```bash
curl -I https://<YOUR_API_SUBDOMAIN>/api/health
```

Expect an Access redirect or a `403` response. A `200` response means the API
is publicly reachable; stop the tunnel and fix the Access policy before use.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "porta",
"version": "0.16.0",
"version": "0.16.1",
"private": true,
"scripts": {
"dev": "node scripts/dev.mjs",
Expand Down
Loading
Loading