feat(deploy): self-healing, access-gated Windows tunnel recipe - #110
Conversation
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.
fc55ccc to
ec4a0e4
Compare
L1M80
left a comment
There was a problem hiding this comment.
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 buildpnpm lint- 304 unit tests
- 5 E2E tests
- PowerShell syntax checks for both
.ps1files
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:
-
The documented/watchdog port does not match Porta’s default web port.
The new recipe, tunnel configuration, and watchdog all assume port
3000, whilevite.config.tsdefaults to3070. On a clean checkout,pnpm devtherefore starts the web app on3070, while the tunnel and watchdog continue checking3000.Please either standardize the recipe on
3070, or explicitly configure and documentPORTA_WEB_PORT=3000. -
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.
-
A malformed encoded cookie can terminate the Vite process.
readCookie()callsdecodeURIComponent()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.
-
The documented “stop” command only disables future watchdog runs.
Unregister-ScheduledTaskdoes 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. -
The watchdog does not identify the specific Porta/tunnel instance.
Any process listening on the configured port is treated as Porta, and any
cloudflaredprocess is treated as the expected tunnel. Another application or another tunnel can therefore make the watchdog reportOKwhile 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.
|
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 Summary of the changes:
Validation completed:
The only remaining failing check is the existing branch name, 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. |
Addressed in e57c46c; dismissing the outdated change request after applying and validating the fixes.
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
pnpmshim, a hidden Corepack prompt, an incorrect tunnel launch, and a watchdog that could not safely identify or stop its own processes.Pipeline
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
3070.pnpm.cmdshim and passes its absolute directory to the task..env, the public host, CORS origin, access token, ports, and Cloudflare ingress before launch and on every watchdog tick..env.local, mode-specific files, or inherited environment variables from weakening the public deployment.cloudflaredprocess.cmd, Node, orcloudflareddescendants.401, authenticated200, and proxy health checks pass.stop-watchdog.ps1that 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.Security: access gate
When
PORTA_REQUIRE_AUTHis enabled, every page,/apirequest, and WebSocket upgrade must presentPORTA_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_HOSTSandPORTA_CORS_ORIGINS, rejects wildcard host configuration for this recipe, and verifies that the first matching Cloudflare ingress rule routes the public URL exactly tohttp://127.0.0.1:3070.Main files
deploy/windows/install-watchdog.ps1: validated one-time task and pnpm setupdeploy/windows/porta-watchdog.ps1: fail-closed health and recovery loopdeploy/windows/managed-runner.ps1: Job Object process-tree ownershipdeploy/windows/stop-watchdog.ps1: ownership-checked stop/uninstall pathdeploy/windows/watchdog-common.ps1: shared identity, validation, and process helpersdeploy/windows/watchdog-common.tests.ps1: Windows helper tests run in CIpackages/web/vite-access-gate.ts: HTTP and WebSocket access gatepackages/web/vite-access-gate.test.ts: focused malformed-input and bypass testsThe previous VBS launchers were removed; hidden child ownership is now handled directly by the managed PowerShell runner.
Verification
pnpm lintpnpm test: 322 tests passedpnpm buildpnpm test:e2e: 5 tests passedwatchdog-common.tests.ps1401, authenticated web200, proxy healthok, identity validation successful, and zero remaining listeners after the tracked tree was stoppedA 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.