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
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,23 @@ flight search JFK LHR --dep 2026-08-15 --backend matrix --pp-only

PointsPath requires a paid subscription (free tier is the browser extension only). Three login modes:

**1. Headed browser login (default, recommended).** Opens a Playwright Chromium so you can sign in normally; the CLI captures the resulting session into `~/.config/flight-cli/pp.json`. Independent of any Chrome PP session you have open elsewhere — different server-side Supabase session, so the refresh chains never race.
**1. Headed browser login (default, recommended).** Opens a Patchright Chrome so you can sign in normally; the CLI captures the resulting session into `~/.config/flight-cli/pp.json`. Independent of any Chrome PP session you have open elsewhere — different server-side Supabase session, so the refresh chains never race.

We use [Patchright](https://pypi.org/project/patchright/) (a drop-in Playwright fork that patches the CDP `Runtime.enable` leak and the `navigator.webdriver` flag) because pointspath.com is behind Cloudflare's bot fingerprint check, which stock Playwright fails. The browser profile is persisted at `~/.cache/flight-cli/browser-profile/` so the Cloudflare `cf_clearance` cookie survives across login sessions — you usually only have to clear the human-check once.

```sh
# One-time: download Chromium into Playwright's global cache
# (~/Library/Caches/ms-playwright on macOS). The browser install is
# disk-cached, not venv-resident.
uvx --from playwright playwright install chromium
# One-time: download real Chrome (~150MB) into Patchright's cache.
# `channel="chrome"` uses the real Chrome binary because its TLS
# fingerprint matches real Chrome traffic — bundled Chromium doesn't.
uvx --from patchright patchright install chrome

# Then log in. `--with playwright` adds the Python package ephemerally
# Then log in. `--with patchright` adds the Python package ephemerally
# for this one invocation — no need to mutate flight-cli's venv.
uv run --with playwright flight auth pp login
uv run --with patchright flight auth pp login
flight auth pp whoami # confirm
```

If you'd rather make playwright a permanent venv resident (skip `--with` every time), there's an optional install extra: `uv pip install -e '.[browser-login]'`. Most users don't need this.
If you'd rather make patchright a permanent venv resident (skip `--with` every time), there's an optional install extra: `uv pip install -e '.[browser-login]'`. Most users don't need this.

**2. `--from-chrome` (cookie import).** Reads Supabase cookies from your local Chrome profile via `rookiepy`. Quicker than headed login since you don't sign in again — but the CLI then *shares* Chrome's refresh-token chain. Supabase rotates refresh tokens single-use, so a refresh on one side will eventually invalidate the other. Use this when you don't mind re-importing periodically.

Expand Down
14 changes: 9 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ dependencies = [
]

[project.optional-dependencies]
# Heavier interactive-login path. Optional because the package is small
# but `playwright install chromium` is ~150MB — non-PP users shouldn't pay.
browser-login = ["playwright>=1.40"]
# Heavier interactive-login path. Patchright is a Playwright fork that
# patches CDP leaks + navigator.webdriver detection so Cloudflare's bot
# fingerprint check passes — required for the PP login flow (Cloudflare-
# protected). Package itself is small; `patchright install chrome` is what
# pulls the real Chrome binary (~150MB) and is only needed for the headed
# login path, so it's not auto-installed.
browser-login = ["patchright>=1.59"]

[dependency-groups]
dev = [
Expand All @@ -40,9 +44,9 @@ dev = [
"pytest>=8.3",
"pytest-cov>=5",
"hypothesis>=6.100",
# Pulled in only for type-checking; `playwright install chromium` is NOT
# Pulled in only for type-checking; `patchright install chrome` is NOT
# run automatically — the dev Python package itself is small.
"playwright>=1.40",
"patchright>=1.59",
]

[project.scripts]
Expand Down
58 changes: 43 additions & 15 deletions src/flight_cli/pp/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,35 +289,63 @@ def login_from_chrome() -> Tokens:
return tokens_from_supabase_cookies(cookies)


# Persistent browser profile for the PP login flow. Cloudflare uses cf_clearance
# cookies that are bound to the browser fingerprint; persisting them across runs
# means the user only has to clear the human-check once.
BROWSER_PROFILE_DIR = (
Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache")))
/ "flight-cli"
/ "browser-profile"
)


def login_via_browser(
*,
timeout_secs: int = _DEFAULT_BROWSER_LOGIN_TIMEOUT_SECS,
poll_interval_secs: float = 1.0,
) -> Tokens:
"""Open a headed Playwright Chromium for the user to log into PointsPath.

Independent of any user-facing Chrome session — this is the recommended
primary path. Polls for the Supabase auth-token cookie; returns as soon
as it appears, or raises PPAuthError on timeout / browser close.
"""Open a headed Patchright Chromium for the user to log into PointsPath.

Uses `patchright` (a Playwright fork that patches the CDP `Runtime.enable`
leak and `navigator.webdriver` exposure) so Cloudflare's bot fingerprint
check passes. Launches with `channel="chrome"` to use the real Chrome
binary (whose JA3/JA4 TLS fingerprint matches real Chrome traffic) and a
persistent context under ~/.cache/flight-cli/browser-profile so cf_clearance
cookies survive between login sessions.

Independent of any user-facing Chrome session. Polls for the Supabase
auth-token cookie; returns as soon as it appears, or raises PPAuthError
on timeout / browser close.
"""
try:
from playwright.sync_api import Error as PlaywrightError # noqa: PLC0415
from playwright.sync_api import sync_playwright # noqa: PLC0415
from patchright.sync_api import Error as PlaywrightError # noqa: PLC0415
from patchright.sync_api import sync_playwright # noqa: PLC0415
except ImportError as e: # pragma: no cover — install-time concern
raise PPAuthError(
"playwright isn't installed (needed for headed browser login). "
"Quickest path: rerun this command via "
"`uv run --with playwright flight auth pp login` "
"(plus a one-time `uvx --from playwright playwright install chromium`). "
"Alternatives: --from-chrome or --tokens-file PATH.",
"patchright isn't installed (needed for Cloudflare-resistant browser "
"login). Quickest path: rerun this command via "
"`uv run --with patchright flight auth pp login` "
"(plus a one-time `uvx --from patchright patchright install chrome`). "
"Alternatives: --from-chrome (reads cookies from your real Chrome) "
"or --tokens-file PATH.",
) from e

import time # noqa: PLC0415

BROWSER_PROFILE_DIR.mkdir(parents=True, exist_ok=True)

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
# launch_persistent_context (not launch + new_context) is the
# recommended Patchright pattern for Cloudflare: it persists cookies +
# storage between runs, and lets the browser appear as a fully real
# Chrome session.
context = p.chromium.launch_persistent_context(
user_data_dir=str(BROWSER_PROFILE_DIR),
channel="chrome",
headless=False,
no_viewport=True,
)
try:
context = browser.new_context()
page = context.new_page()
page.goto(_PP_HOME_URL)

Expand All @@ -339,4 +367,4 @@ def login_via_browser(
"Re-run `flight auth pp login` and complete sign-in before the timeout.",
)
finally:
browser.close()
context.close()
10 changes: 6 additions & 4 deletions src/flight_cli/pp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,14 @@ def pp_login(
) -> None:
"""Authenticate with PointsPath. Three modes (mutually exclusive).

Default (no flag): open a headed Playwright Chromium so you can sign
Default (no flag): open a headed Patchright Chrome so you can sign
in normally. Captures the resulting Supabase session into
~/.config/flight-cli/pp.json. Independent of any user-facing Chrome
PP session. Needs playwright at runtime (ephemeral or installed): the
README PP Setup section covers `uv run --with playwright` + the
one-time `uvx --from playwright playwright install chromium`.
PP session. Uses Patchright (a Playwright fork that patches the CDP
Runtime.enable leak + navigator.webdriver) to clear Cloudflare's
bot fingerprint check. Needs patchright at runtime (ephemeral or
installed): README PP Setup covers `uv run --with patchright` + the
one-time `uvx --from patchright patchright install chrome`.

--from-chrome: import the session from your local Chrome profile via
cookies. Quicker, but the CLI then shares Chrome's refresh-token
Expand Down
28 changes: 14 additions & 14 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading