diff --git a/fingerprint_client.py b/fingerprint_client.py index 11e295c..e84ce1e 100644 --- a/fingerprint_client.py +++ b/fingerprint_client.py @@ -72,6 +72,8 @@ import requests +import env_config + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("fingerprint_client") @@ -325,8 +327,28 @@ def playwright_init_script(fp: dict) -> str: def main() -> int: p = argparse.ArgumentParser(description="Fetch a browser fingerprint from 2captcha") - p.add_argument("--key", default=os.environ.get("TWOCAPTCHA_KEY"), - help="API key. Defaults to $TWOCAPTCHA_KEY (safer than argv).") + # Reads `.env` as well as the exported variable, through the family's own + # loader. It used to read `os.environ` alone, which meant a key put in + # `.env` exactly as the README and .env.example instruct worked for every + # engine and failed HERE with "No API key" — a documented mechanism not + # applied on one path, which is the shape of half the defects §16 lists. + # + # `.env` has to be LOADED before it can be read: `env_value` looks at + # os.environ, and `load_env` is what fills that from the file. Calling it + # here rather than relying on an engine having called it is the whole + # point — this is a standalone entry point. + # + # And it goes through `env_value` rather than `os.environ.get` so the + # PLACEHOLDER rule applies. Measured both ways: with + # TWOCAPTCHA_KEY=your_2captcha_api_key_here exported, `os.environ.get` + # sends the placeholder to the API and the run reports "Fingerprint API + # rejected the key (401) — note this is a separate subscription", which + # sends the reader off to check a subscription when they simply never + # filled the key in. `env_value` says so instead, by name. + env_config.load_env() + p.add_argument("--key", default=env_config.env_value("TWOCAPTCHA_KEY"), + help="API key. Defaults to TWOCAPTCHA_KEY from the " + "environment or .env (safer than argv).") # Measured against the live API on 2026-09-09, because the example this # file used to carry ("Windows,Chrome,Desktop") returns 400 every time: # accepted -> Windows, Microsoft Windows, Android diff --git a/smoke_test.py b/smoke_test.py index 80edf08..fd787e6 100644 --- a/smoke_test.py +++ b/smoke_test.py @@ -1325,6 +1325,55 @@ def test_no_capture_leaks(): return ok +def test_fingerprint_client_reads_env(): + group("fingerprint_client resolves its key the way the docs promise") + ok = True + import fingerprint_client as fpc + + # THE DEFECT THIS PINS, found the first time --fingerprint was run live + # here and confirmed present in five sibling repos: `--key` defaulted to + # `os.environ.get("TWOCAPTCHA_KEY")` alone. So a key put in `.env` — + # which is exactly what §3, the README and .env.example instruct — worked + # for every engine and failed HERE with "No API key". A documented + # mechanism not applied on one path, which is the shape of half the + # defects §16 lists. + src = inspect.getsource(fpc.main) + ok &= check("it loads .env itself, rather than hoping an engine did", + "env_config.load_env()" in src) + ok &= check("...and reads the key through the family's loader", + 'env_config.env_value("TWOCAPTCHA_KEY")' in src) + # Through `env_value` and NOT `os.environ.get`, because only the former + # applies the placeholder rule. Measured both ways with + # TWOCAPTCHA_KEY=your_2captcha_api_key_here exported: os.environ.get + # sends the placeholder to the API and the run reports "Fingerprint API + # rejected the key (401) — note this is a separate subscription", which + # sends the reader to check a subscription they never needed. + ok &= check("...not straight from os.environ, which skips the " + "placeholder rule", + 'os.environ.get("TWOCAPTCHA_KEY")' not in src) + # Behaviourally, not just by reading the source — and written + # self-contained so this check is byte-identical in every repo of the + # family rather than depending on a local helper. + saved = os.environ.get("TWOCAPTCHA_KEY") + try: + os.environ["TWOCAPTCHA_KEY"] = "your_2captcha_api_key_here" + read_back = env_config.env_value("TWOCAPTCHA_KEY") + finally: + if saved is None: + os.environ.pop("TWOCAPTCHA_KEY", None) + else: + os.environ["TWOCAPTCHA_KEY"] = saved + ok &= check("a placeholder still reads as unset on this path", + read_back is None) + + # The default must never reach `--help`. argparse prints a default only + # when the help string asks for it, so this is one substring away from + # printing a live credential to anyone who types --help. + ok &= check("the --key help text does not interpolate its default", + "%(default)s" not in src) + return ok + + def test_wording(): group("wording") ok = True @@ -1505,6 +1554,7 @@ def main() -> int: ok &= test_engines(skips) ok &= test_no_capture_leaks() ok &= test_ci_checks_is_actually_wired_up() + ok &= test_fingerprint_client_reads_env() ok &= test_wording() ok &= test_sample_output()