autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# SPA Auth/OTP Form Debugging (headless patchright/Playwright, 2026-09)
|
||||
|
||||
General techniques learned while automating a React SPA auth form (email → OTP code → onboarding) behind Cloudflare + Statsig. Applies to any signup/login flow (Notion, Clerk-style, etc.).
|
||||
|
||||
## 1. Gate on the API RESPONSE, never the request or a fixed sleep
|
||||
|
||||
The form submits via XHR (e.g. `sendTemporaryPassword`). Matching the *request* or sleeping 6s and moving on produces false "stalled" verdicts — the response may fire 10-30s later.
|
||||
|
||||
```python
|
||||
st = {"stp": None}
|
||||
page.on("response", lambda r: st.__setitem__("stp", r.status)
|
||||
if "sendTemporaryPassword" in r.url else None)
|
||||
btn.click()
|
||||
for _ in range(30):
|
||||
time.sleep(1)
|
||||
if st["stp"] is not None:
|
||||
break
|
||||
# st["stp"] is None => genuinely stalled; != 200 => rejected; == 200 => proceed to mailbox
|
||||
```
|
||||
|
||||
## 2. "Loading..." button = click does nothing
|
||||
|
||||
SPA submit buttons can render as `Loading...\nContinue` while client-side gates (Statsig init, config fetch) finish. Clicking then is a no-op.
|
||||
|
||||
- Probe before click: `btn.inner_text()` — if it contains "Loading", wait/re-click in a loop (2s interval, ~60s deadline).
|
||||
- Deep probe via `page.evaluate` on the raw `<button>`: check `disabled`, `aria-disabled`, className.
|
||||
- If the button never enables: check the network log for failed gate requests — e.g. `exp.notion.com/v1/initialize` returning `ERR_ABORTED` leaves the form permanently disabled. This is a client-side experiment/config gate, not a captcha. Reload-retry (3-5x) sometimes passes; if the gate endpoint is blocked at the proxy/network level, no amount of clicking helps.
|
||||
|
||||
## 3. Pre-check API responses are NOT skip signals
|
||||
|
||||
Auth pages often call something like `getLoginOptions` on load — BEFORE the user typed anything. Its `hasAccount` field at that point reflects page init, not the email you're about to submit. Treating an early pre-check as "mailbox already registered → skip" burned 7/12 attempts falsely in one run. Only read such fields from a response that fired AFTER your submit click.
|
||||
|
||||
## 4. Screenshots hang "waiting for fonts to load" on proxied headless pages
|
||||
|
||||
`page.screenshot()` can time out indefinitely on slow-proxy headless Chrome (font fetch never completes), killing the diagnostic script. Mitigations:
|
||||
|
||||
- `page.screenshot(timeout=10000, animations="disabled")` — still can fail on fonts.
|
||||
- Prefer non-visual diagnostics: `page.content()` → save HTML; `page.locator("body").inner_text()`; network event log. Dump `html.count("<input")` and grep for markers instead of screenshots.
|
||||
|
||||
## 5. "Page renders but 0 inputs" = JS-blocked or challenge page
|
||||
|
||||
If `page.content()` is ~29KB with zero `<input>` tags and body text says "JavaScript must be enabled" — the browser got the noscript fallback (bot detection or JS blocked). Checks that distinguish causes:
|
||||
|
||||
- `page.title()`, frame list (`page.frames` → all URLs), HTML markers: `challenge-platform`, `cf-turnstile`, "Just a moment".
|
||||
- Empty frames + noscript text + no CF markers = JS execution blocked at network level (often the proxy), not a visible captcha. Rotate proxy/exit-IP rather than fight the page.
|
||||
|
||||
## 6. Post-OTP redirect can stall on /login?redirectURL=...
|
||||
|
||||
After submitting the code, some SPAs fail to complete the redirect chain and sit on `/login?redirectURL=<target>`. Fix: re-navigate to the original target URL (`page.goto(APPLY_URL)`) — the session cookie is already set, so the app lets you through and the offer/flow attached to redirectURL applies. Then continue with onboarding.
|
||||
|
||||
## 7. Diagnostic script skeleton
|
||||
|
||||
Keep a standalone `diag_apply.py` that runs ONE attempt with:
|
||||
- `page.on("response")` filtered to API endpoints (drop ping/analytics noise), logging `status + url`.
|
||||
- `page.on("requestfailed")` → `f"FAILED {url} :: {failure}"` — this is where gate endpoints (statsig/exp/splunk) show up.
|
||||
- DOM probes via `page.evaluate` (button state, input inventory).
|
||||
- No screenshots (see #4). HTML dump to file when structure looks wrong.
|
||||
Run it before touching the farm loop — one clean attempt reveals which of the 6 failure modes you're in, instead of burning 20 mailboxes on a wrong hypothesis.
|
||||
|
||||
## 8. Sync vs async patchright
|
||||
|
||||
If your onboarding/helper functions are synchronous, drive the browser with `patchright.sync_api` end-to-end. Handing an async Page to sync helpers raises `TypeError: argument of type 'coroutine' is not iterable` deep in the flow and wastes a whole run.
|
||||
|
||||
## Ops boundary (important)
|
||||
|
||||
These techniques are for forms backed by mailboxes you control (your own domains, temp-mail services you operate, Gmail aliases). If a farm workflow depends on logging into IMAP accounts belonging to third parties (combo lists of real people's mailboxes), that is unauthorized access to other people's accounts — it crosses from offer-farming into account compromise, and the pipeline should be refused/redesigned onto self-controlled mail sources.
|
||||
Reference in New Issue
Block a user