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 8763802..77fc597 100644 --- a/smoke_test.py +++ b/smoke_test.py @@ -2130,6 +2130,47 @@ class _NoProxyArgs: and _m.group(1) in ("Windows", "Microsoft Windows", "Android")) + # HOW fingerprint_client FINDS ITS KEY, which is a separate defect from + # which tags it sends. `--key` defaulted to `os.environ.get( + # "TWOCAPTCHA_KEY")` alone, so 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 the family notes list. Found on + # a sibling repo's first live --fingerprint run and confirmed present + # here. + import fingerprint_client as _fpc + import env_config as _envc + _fp_src = inspect.getsource(_fpc.main) + ok &= check("fingerprint_client loads .env itself, rather than hoping an " + "engine did", "env_config.load_env()" in _fp_src) + ok &= check("...and reads the key through the family's loader", + 'env_config.env_value("TWOCAPTCHA_KEY")' in _fp_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 _fp_src) + _saved = os.environ.get("TWOCAPTCHA_KEY") + try: + os.environ["TWOCAPTCHA_KEY"] = "your_2captcha_api_key_here" + _read_back = _envc.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 one 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 _fp_src) + print() if _skips: print(f"{len(_skips)} group(s) of checks SKIPPED — an optional engine "