* Add portless.json config file and zero-arg mode Adds an optional portless.json for configuring app name, script, and port without embedding portless in package.json scripts. Running bare `portless` from a workspace root auto-discovers pnpm packages and starts all dev scripts concurrently through the proxy. The --script flag overrides the default "dev" script for a single invocation. * Use npm scope as project base for multi-app names In multi-app mode, package names are now composed as subdomains of the npm scope (e.g., @og-sdk/docs becomes docs.og-sdk.localhost). Falls back to inferring the project name from the workspace root when packages are unscoped. * Pass through scripts that already invoke portless In multi-app mode, if a package's dev script starts with `portless`, run it as-is without registering a route or injecting PORT/HOST. The embedded portless invocation handles its own name and routing. * Fix multi-app PATH and deduplicate project name Augment PATH with node_modules/.bin from each package's directory so local binaries (tsup, next, vite, etc.) are found. Skip the project suffix when the package name already matches it (avoids names like portless.portless). * Fix proxy start prompting for trust on every launch Cache the CA fingerprint in a marker file (ca.trusted) after a successful trust so subsequent proxy starts skip the OS security check and avoid re-triggering the macOS authentication dialog. Also switch docs and example dev scripts to clean "next dev". * Skip proxy route for build-only tools, support portless config in package.json Non-server commands (tsup, tsc, esbuild, etc.) are auto-detected and run without a proxy route. Adds "proxy" config field for explicit control. Also supports a "portless" key in package.json — either as an object or a string shorthand for the app name. * Address PR review: config fixes, proxy auto-start, and refactors - Rewrite splitCommand to respect quoted arguments - Use frequency count for commonScope instead of first match - Extract ensureProxyRunning helper; call from multi-app mode - Remove loadConfig directory walk-up (check cwd only) - Deduplicate clearTrustMarker call in generateCA - Remove proxy field from config (system-level, not app-level) - Fix early script filter to respect per-app overrides - Add portless key to google-oauth example - Replace invalid JSON comments with markdown headings in docs - Extract spawnProxiedApp/spawnTaskApp helpers from handleDefaultMulti * Address PR review: docs fixes, config precedence, proxy option - Fix docs claiming config walks up directories (it does not) - Clean up resolveAppConfig to use explicit field return - Add JSDoc to isServerCommand explaining denylist design - Add comment on sequential proxy spawning (port collision) - Handle backslash escapes in splitCommand - Remove non-null assertion on discovered proxy state - Pass appConfig through to avoid redundant disk read - Fix config precedence: package.json portless key wins (closest) - Implement proxy: false config option for non-server scripts * Add package manager delegation, turborepo support, and fix TLS state discovery - Add detectPackageManager and resolveScriptCommand to delegate script execution to the correct package manager (pnpm/yarn/bun/npm) instead of parsing shell scripts directly. This enables reliable turborepo integration where `portless` is the dev script. - Fix discoverState inferring tls=false when the proxy is running on port 443 but state files in /tmp were cleaned by the OS. - Re-discover proxy state in multi-app mode when proxy is already running. - Add turborepo integration docs and tests. * Address PR 251 review: child lifecycle, shell safety, line buffering, proxy dedup, multi-PM workspaces, config errors - Log exit codes per child and print summary in multi-app mode - Spawn child processes directly without shell for PM-delegated commands - Buffer partial lines in prefixed output streams, flush on end - Extract resolveProxyDesiredState helper to deduplicate proxy config - Support npm/yarn/bun workspaces via package.json "workspaces" field - Replace process.exit in config validation with ConfigValidationError * Address PR review: SIGKILL fallback, YAML flow sequences, glob wildcards, stream fixes - Add 5s SIGKILL timeout after SIGTERM in multi-app cleanup to prevent hanging when children ignore SIGTERM - Handle YAML flow sequence syntax (packages: [a, b]) in pnpm workspace parser, with JSDoc documenting the supported YAML subset - Fall through from multi-app to single-app mode when no workspace packages have the target script, so workspace roots with their own portless.json still work - Refactor expandSingleGlob into recursive expandSegments to support mid-path wildcards (packages/*/src) and prefix wildcards (apps/team-*) - Use StringDecoder in prefixStream for correct multi-byte UTF-8 handling across chunk boundaries, and preserve empty lines from child output * Eliminate /tmp state dir, fix symlink LPE, and clean up multi-app output Move all proxy state from /tmp/portless to ~/.portless, fixing both the macOS /tmp cleanup problem (repeated trust prompts) and the symlink LPE vulnerability (GHSA-p67g-wf2j-g3h5). Add lstatSync symlink guard in fixOwnership as defense-in-depth. Remove the interactive "Start proxy?" prompt in favor of auto-starting. Rewrite multi-app startup output so URLs stand out clearly. Also addresses PR 251 review items: re-throw ConfigValidationError, fix splitCommand backslash handling in single quotes, warn on unknown config keys, and remove unused export. * Fix macOS CA cleanup when multiple certs share the same CN Loop delete-certificate calls to remove all matching "portless Local CA" entries from each keychain. The previous single call failed with "is ambiguous, matches more than one certificate" when duplicate CAs had accumulated from repeated trust prompts. * Auto-trust CA after proxy auto-start When the proxy is auto-started in single-app or multi-app mode and the CA is not yet trusted, run handleTrust which has proper sudo elevation logic. This prevents the scenario where a new user cancels the trust dialog during proxy start and is left with browser cert errors and no clear recovery path. * Fix prettier formatting in configuration docs * Use turbo for multi-app orchestration When turbo.json is present, portless delegates to `turbo run <script>` instead of spawning each app individually. Per-app PORT, HOST, and PORTLESS_URL are injected via a lightweight --require loader so turbo retains dependency ordering, output grouping, and task graph awareness. Falls back to direct spawning when turbo is absent or when the config sets `"turbo": false`. * Fix turbo loader double-escape, cleanup guards, and direct-spawn route cleanup - Remove redundant manual backslash escape in loaderSource; JSON.stringify already handles escaping for JS string literals - Quote loader path in buildNodeOptions when it contains spaces - Add cleanedUp guard to runWithTurbo and runWithDirectSpawn cleanup functions to prevent double execution on signal + explicit call - Strip trailing \r in prefixStream so Windows \r\n output renders cleanly - Track routes in runWithDirectSpawn and remove them in the signal handler, matching the pattern already used by runWithTurbo * Fix doc gaps: state dir, CA path, turbo field, pkg portless key, hostname convention - State directory docs still showed the old 3-tier table (port < 1024 -> /tmp/portless). All state now lives in ~/.portless; updated SKILL.md and docs configuration page. - CA path references in README and SKILL.md still mentioned /tmp/portless/ca.pem; simplified to ~/.portless/ca.pem. - Added the `turbo` config field to all three config tables. - Documented the package.json "portless" key as a standalone feature (was only shown in the Turborepo section). - Documented the multi-app hostname convention (<package>.<project>.localhost derived from common npm scope). * Make bare portless just work everywhere, monochrome output - Remove config gates: bare `portless` and `portless run` resolve the dev script from package.json without requiring portless.json - Extend PORTLESS=0 skip path to handle zero-arg mode - Monochrome CLI output: bold for headers/errors, dim for warnings/muted, no color codes - Multi-app display: show full package name (with scope), alphabetized, URLs dimmed - Update docs, help text, and tests to reflect zero-config defaults * Improve multi-app output and turbo integration - Show full scoped package names in startup list, alphabetized - Hide task-only packages from output (they still run) - Use root package.json dev script for turbo when available, preserving user's flags like --concurrency and --filter - Pass extra args after -- through to turbo * Restore red/yellow ANSI colors for errors and warnings
Google OAuth with portless
Google OAuth rejects .localhost subdomains as redirect URIs. This example shows how to use portless with a custom TLD to get a domain that Google accepts.
Why .localhost fails
Google's OAuth credentials page validates redirect URIs against a bundled copy of the Public Suffix List. .localhost is not in their copy, so subdomains like myapp.localhost:3000 are rejected with:
- "must end with a public top-level domain (such as .com or .org)"
- "must use a domain that is a valid top private domain"
Plain localhost works because Google hardcodes it in a whitelist, but subdomains do not.
The fix: use any valid TLD
Any TLD in the Public Suffix List works. portless lets you set a custom TLD with --tld:
portless proxy start --tld dev
portless oauth-test next dev
# -> https://oauth-test.dev
.dev is a real gTLD (owned by Google). It requires HTTPS because it's HSTS-preloaded, which portless handles automatically.
Recommended: use a domain you own
Using a bare TLD like .dev means your local domain (oauth-test.dev) could collide with a real domain someone else owns. For safer local development, use a subdomain of a domain you control:
portless proxy start --tld dev
portless oauth-test.local.ctate next dev
# -> https://oauth-test.local.ctate.dev
Since you own ctate.dev, nothing under local.ctate.dev will conflict with real internet traffic. This works for teams too: set a wildcard DNS record (*.local.yourcompany.dev -> 127.0.0.1) so every developer gets local resolution without /etc/hosts.
Setup
1. Install portless
npm install -g portless
2. Start the proxy
portless proxy start --tld dev
Portless defaults to HTTPS on port 443 (auto-elevates with sudo), so URLs don't need a port number.
3. Create a Google OAuth client
- Go to Google Cloud Console > Credentials
- Click "Create Credentials" > "OAuth client ID"
- Choose "Web application"
- Add your domain to "Authorized JavaScript origins" (e.g.
https://oauth-test.dev) - Add the callback URL to "Authorized redirect URIs" (e.g.
https://oauth-test.dev/api/auth/callback/google) - Copy the Client ID and Client Secret
4. Configure the example
cd examples/google-oauth
cp .env.example .env
Edit .env with your credentials:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
NEXTAUTH_URL=https://oauth-test.dev
Generate a secret:
openssl rand -base64 32
5. Install and run
pnpm install
pnpm dev
This runs portless oauth-test next dev, which serves the app at https://oauth-test.dev.
6. Test
Open https://oauth-test.dev in your browser and click "Continue with Google".
Alternative: plain localhost for the callback
If you only need Google OAuth and don't want to change TLDs, you can add http://localhost:3000/api/auth/callback/google as an additional redirect URI. Google allows plain localhost with any port. Configure NextAuth to use it:
NEXTAUTH_URL=http://localhost:3000
The downside is you lose the portless benefits (named URLs, no port conflicts) for the OAuth callback flow.