Skip to content

feat(deploy): self-healing, access-gated Windows tunnel recipe - #110

Merged
L1M80 merged 3 commits into
L1M80:developfrom
Alphaxalchemy:feat/windows-tunnel-watchdog
Jul 25, 2026
Merged

feat(deploy): self-healing, access-gated Windows tunnel recipe#110
L1M80 merged 3 commits into
L1M80:developfrom
Alphaxalchemy:feat/windows-tunnel-watchdog

Conversation

@Alphaxalchemy

@Alphaxalchemy Alphaxalchemy commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What & why

Adds an optional, self-contained Windows recipe for keeping a Porta instance reachable at a public hostname through a Cloudflare tunnel while the task user is signed in. The watchdog starts and repairs the Porta app and tunnel independently, and the web access gate prevents the public dev server from being exposed without authentication.

This turns the machine-local fixes from a real 502 outage into a reproducible, version-controlled setup. The original failure combined a missing pnpm shim, a hidden Corepack prompt, an incorrect tunnel launch, and a watchdog that could not safely identify or stop its own processes.

Pipeline

Internet -> Cloudflare edge -> cloudflared -> Porta web :3070 -> Porta proxy :3170

The scheduled task uses an Interactive logon. It recovers automatically while that user is signed in and resumes at the next logon; true pre-login 24/7 service operation is outside this recipe.

Watchdog and lifecycle hardening

  • Standardizes the web/tunnel/watchdog origin on Porta's default port, 3070.
  • Installs and verifies the exact Windows pnpm.cmd shim and passes its absolute directory to the task.
  • Validates .env, the public host, CORS origin, access token, ports, and Cloudflare ingress before launch and on every watchdog tick.
  • Pins the validated auth/network values into the managed process environment, preventing .env.local, mode-specific files, or inherited environment variables from weakening the public deployment.
  • Tracks each runner by repo/task identity, PID, start time, exact command arguments, and configuration hashes instead of trusting any listener or cloudflared process.
  • Runs each component inside a kill-on-close Windows Job Object, so runner failure cannot orphan cmd, Node, or cloudflared descendants.
  • Stops the managed tunnel before repairing an unhealthy or unverified Porta process, then restarts it only after unsigned 401, authenticated 200, and proxy health checks pass.
  • Adds a lock-aware stop-watchdog.ps1 that validates task/process ownership, unregisters the task, and stops the tunnel and app trees. Legacy or unrelated launchers are reported rather than guessed at and killed.
  • Refuses to overwrite an existing same-name scheduled task or start beside an untracked listener/launcher using the required deployment resources.

Security: access gate

When PORTA_REQUIRE_AUTH is enabled, every page, /api request, and WebSocket upgrade must present PORTA_ACCESS_TOKEN. A valid one-time ?access_token=... request sets an HttpOnly, Secure, SameSite cookie. Missing or malformed cookies fail closed instead of crashing Vite.

The gate is intended to compose with Cloudflare Access at the edge. The installer requires explicit PORTA_ALLOWED_HOSTS and PORTA_CORS_ORIGINS, rejects wildcard host configuration for this recipe, and verifies that the first matching Cloudflare ingress rule routes the public URL exactly to http://127.0.0.1:3070.

Main files

  • deploy/windows/install-watchdog.ps1: validated one-time task and pnpm setup
  • deploy/windows/porta-watchdog.ps1: fail-closed health and recovery loop
  • deploy/windows/managed-runner.ps1: Job Object process-tree ownership
  • deploy/windows/stop-watchdog.ps1: ownership-checked stop/uninstall path
  • deploy/windows/watchdog-common.ps1: shared identity, validation, and process helpers
  • deploy/windows/watchdog-common.tests.ps1: Windows helper tests run in CI
  • packages/web/vite-access-gate.ts: HTTP and WebSocket access gate
  • packages/web/vite-access-gate.test.ts: focused malformed-input and bypass tests

The previous VBS launchers were removed; hidden child ownership is now handled directly by the managed PowerShell runner.

Verification

  • pnpm lint
  • pnpm test: 322 tests passed
  • pnpm build
  • pnpm test:e2e: 5 tests passed
  • Windows PowerShell 5.1 parse check for every deployment script
  • watchdog-common.tests.ps1
  • Managed-process smoke test on Windows: unsigned web 401, authenticated web 200, proxy health ok, identity validation successful, and zero remaining listeners after the tracked tree was stopped

A live Cloudflare connector was not available in the final local environment, so the real tunnel connection was not re-run after the hardening changes. The CLI commands and readiness endpoint follow Cloudflare's documented locally managed tunnel interfaces.

Adds deploy/windows/, an optional recipe for keeping a Porta instance reachable
at a public hostname 24/7 via a Cloudflare tunnel, with a watchdog that restarts
either half if it dies -- plus a fail-closed access gate so the exposed endpoint
is not open to the world.

Watchdog recipe fixes a real outage (Cloudflare 502; every restart failed with
"Windows cannot find 'pnpm'"):
- install-watchdog.ps1 puts a corepack pnpm shim on PATH and disables corepack's
  interactive download prompt, which otherwise hangs hidden restarts.
- run-tunnel runs cloudflared by --config, not by a tunnel name that may not exist.
- porta-watchdog.ps1 heals each half independently, never force-kills a healthy
  tunnel, logs to a file (no modal dialogs), and guards against duplicate launches.

Access gate (packages/web/vite-access-gate.ts): with PORTA_REQUIRE_AUTH set, every
request -- page, /api, and WebSocket upgrades -- must carry PORTA_ACCESS_TOKEN or is
rejected. Cookie-based so it also covers WebSockets (browsers omit Basic-Auth on WS).
Composes with Cloudflare Access at the edge, which the app already supports via the
CF-Access service-token headers. Also ignores graphify-out/ tooling output.
@Alphaxalchemy
Alphaxalchemy force-pushed the feat/windows-tunnel-watchdog branch from fc55ccc to ec4a0e4 Compare July 21, 2026 00:01
@Alphaxalchemy Alphaxalchemy changed the title feat(deploy): self-healing Windows tunnel watchdog recipe feat(deploy): self-healing, access-gated Windows tunnel recipe Jul 21, 2026
L1M80
L1M80 previously requested changes Jul 23, 2026

@L1M80 L1M80 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for putting this together — and especially for documenting the original incident and its root causes before following through with an implementation.

Turning a collection of machine-local fixes into a self-contained, reproducible Windows recipe is genuinely valuable. The root-cause table, portable path handling, independent recovery logic, silent logging, and access gate are all thoughtful additions. This would be an excellent feature for people who want to keep a self-hosted Porta instance available remotely.

I checked out this PR and ran:

  • pnpm build
  • pnpm lint
  • 304 unit tests
  • 5 E2E tests
  • PowerShell syntax checks for both .ps1 files

All passed. I also smoke-tested the PR against a running Antigravity instance: project grouping, conversation lists, existing conversation content, search, settings, and the mobile layout all worked without console errors. The normal Porta UI path looks good.

I did find a few issues that I think need to be addressed before merging:

  1. The documented/watchdog port does not match Porta’s default web port.

    The new recipe, tunnel configuration, and watchdog all assume port 3000, while vite.config.ts defaults to 3070. On a clean checkout, pnpm dev therefore starts the web app on 3070, while the tunnel and watchdog continue checking 3000.

    Please either standardize the recipe on 3070, or explicitly configure and document PORTA_WEB_PORT=3000.

  2. The public-host setup is missing two required settings.

    With the README as written, Vite rejects the public hostname with 403 Blocked request, even after successful authentication. The proxy’s WebSocket origin validation will also reject conversation streams.

    The setup should include something equivalent to:

    PORTA_ALLOWED_HOSTS=<public-hostname>
    PORTA_CORS_ORIGINS=https://<public-hostname>

    Alternatively, the tunnel configuration could explicitly override the origin Host header, but the chosen approach should be documented and reproducible.

  3. A malformed encoded cookie can terminate the Vite process.

    readCookie() calls decodeURIComponent() without handling decoding errors. In the WebSocket upgrade path, that exception is not caught by the normal HTTP middleware error handling and causes the dev server to exit.

    Decoding failures should be treated as unauthenticated input, and it would be good to add focused HTTP/WebSocket tests for the access gate.

  4. The documented “stop” command only disables future watchdog runs.

    Unregister-ScheduledTask does not stop the Porta and cloudflared processes that were already launched asynchronously. The section should either be renamed to “disable automatic recovery” or include a safe way to stop the specific processes started by this recipe.

  5. The watchdog does not identify the specific Porta/tunnel instance.

    Any process listening on the configured port is treated as Porta, and any cloudflared process is treated as the expected tunnel. Another application or another tunnel can therefore make the watchdog report OK while the Porta tunnel is still unavailable.

    Matching the expected command/configuration or using an authenticated health check would make the recovery logic more reliable.

One small housekeeping item: the Branch naming check is currently failing because feat/windows-tunnel-watchdog does not match the repository’s {type}/{issue_no}/{short-desc} convention.

I’m marking this as Request changes for now because the port and hostname configuration issues prevent the advertised clean-checkout deployment from working as documented. This is absolutely not a rejection of the feature or its direction — the feature is excellent, and I’d be happy to approve it once these deployment and robustness issues are addressed.

Thanks again for the detailed issue write-up and for contributing such a substantial improvement.

@L1M80

L1M80 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Thank you again for the substantial contribution and for enabling maintainer edits.

I went ahead and implemented the follow-up fixes directly on this PR branch in commit e57c46c.

Summary of the changes:

  • Standardized the Porta web origin, watchdog, documentation, and tunnel example on port 3070.
  • Added and documented the required PORTA_ALLOWED_HOSTS and PORTA_CORS_ORIGINS configuration.
  • Made malformed cookies fail closed without crashing HTTP or WebSocket handling, with focused tests for both paths.
  • Prevented unauthorized WebSocket upgrades from reaching Vite or proxy listeners.
  • Added strict validation for the access token, loopback binding, ports, public hostname, CORS origin, and the matching Cloudflare ingress rule.
  • Pinned the validated security and network values into the managed process environment so inherited variables or higher-priority Vite env files cannot disable authentication.
  • Replaced the detached VBS launchers with identity-scoped managed runners using Windows Job Objects, preventing orphaned Node or cloudflared processes.
  • Added exact process, task, command, config hash, and start-time validation so unrelated listeners and tunnels are never treated as this deployment.
  • Added stop-watchdog.ps1, including ownership checks, lock coordination, tunnel-first shutdown, and safe handling of legacy or untracked launchers.
  • Changed recovery to stop the managed tunnel before repairing an unhealthy Porta process and restart it only after unsigned 401, authenticated 200, and proxy health checks pass.
  • Added Windows helper tests and PowerShell parsing to CI.
  • Updated the PR description and Windows documentation to match the final implementation, including the Interactive-logon availability limitation.

Validation completed:

  • pnpm lint
  • pnpm test: 322 tests passed
  • pnpm build
  • pnpm test:e2e: 5 tests passed
  • Windows PowerShell 5.1 parsing and helper tests passed
  • Windows managed-process smoke test passed: unsigned web 401, authenticated web 200, proxy health ok, identity validation succeeded, and no listeners remained after stopping the tracked tree
  • GitHub Actions CI passed on Windows, macOS, Ubuntu, and E2E

The only remaining failing check is the existing branch name, feat/windows-tunnel-watchdog, which does not match this repository's branch naming convention. I did not attempt to rename the head branch of an open fork PR because that can disrupt the PR relationship.

A live Cloudflare connector was not available in my final local environment, so I did not rerun the real tunnel connection after these hardening changes. This limitation is also stated in the updated PR description.

This should address all of the blocking review findings. Thank you again for identifying the original outage, documenting it thoroughly, and turning the solution into a genuinely useful feature for Porta users.

@L1M80
L1M80 dismissed their stale review July 25, 2026 15:13

Addressed in e57c46c; dismissing the outdated change request after applying and validating the fixes.

@L1M80
L1M80 merged commit 91fb3db into L1M80:develop Jul 25, 2026
6 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants