Files
autoreg-skills-max/skills/ai-provider-autoreg/references/inertia-laravel-otp-and-promo-claim.md

47 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Laravel/Inertia OTP-авторизация и claim промокодов (MLH / Hacktoberfest)
Реверс Laravel+Inertia+Vite сайтов (www.mlh.com) и Next.js API-фронтов (hacktoberfest.com → hacktoberfest-api.mlh.com). Проверено E2E: OTP sign-in, OAuth, offers, claim 2 из 4 кодов.
## 1. Распознавание стека и чтение props
- Страница Inertia: весь стейт в `<script id="..." data-page="app" type="application/json">{...}</script>`. Парсить regex `type="application/json">(.*?)</script>` + `html.unescape()` ПЕРЕД `json.loads` — атрибут HTML-экранирован.
- Ключевые props: `component`, `version`, `isSignedIn`, `currentUser`, `errors`, `flash`, `apiBaseUrl`, `authBaseUrl`.
- Vite-манифест: `https://<host>/vite/.vite/manifest.json` → маппинг `pages/...tsx -> assets/...js`. Искать по имени компонента из props (напр. `Events/Partners/PromoCodes/Redeem.tsx` → `Redeem-*.js`) — в чанке видны точные URL, data-testid кнопок и payload форм.
## 2. Inertia POST — правила
- Заголовки: `X-Inertia: true`, `X-Inertia-Version: <version из data-page JSON>` (НЕ из data-version атрибута — его может не быть), `X-CSRF-Token: <urldecode(XSRF-TOKEN cookie)>`, `Referer: <URL страницы>`, `Content-Type: application/json`.
- Ответ на POST = ЧИСТЫЙ JSON (component+props), не HTML — парсить `json.loads(resp)`, а не искать `<script>`.
- HTTP 409 = version conflict: GET страницу заново, взять свежий version. ПЕРЕД ретраем проверить, не konsumировано ли действие (POST мог пройти до 409 на повторе) — читать signed/claimed state с главной.
- PITFALL «silent no-op»: корректный на вид POST может вернуть 200 с тем же компонентом, `promoCode: null`, `errors: []`, `flash: null` — действие НЕ зарегистрировано, сервер молча игнорирует. Если после POST state не изменился — НЕ долбить HTTP, а идти в браузер (п.4).
## 3. MLH OTP sign-in (флоу октября 2026, компонент Auth/Session/New)
1. GET `https://www.mlh.com/signin` → version + XSRF из data-page.
2. POST `/signin` `{email, return_to, cf-turnstile-response}` → в props `otpId` — ЭТО ULID-СТРОКА, regex `"otpId":\s*"?([A-Za-z0-9_\-]+)`; числовой `(\d+)` схватит только «01».
3. GET `/signin/<otpId>/otp` — ОБЯЗАТЕЛЬНО перед POST, чтобы взять свежий version (иначе 409).
4. Код из письма (Gmail IMAP): subject «One-Time Login Code», в теле `Login Code is NNNNNN` (regex `Login Code is\s*(\d{6})`). Magic-link «Sign in (» — legacy-путь, теперь ведёт на OTP-страницу.
5. POST `https://www.mlh.com/signin/<otpId>/otp` `{otp_id, otp, email, return_to, cf-turnstile-response}` → signed. Проверять GET `/` → props `isSignedIn:true`; куки сохранить (cookie string в JSON-стейт).
Turnstile MLH: sitekey `0x4AAAAAAARi-8xgLgfKOXIb` (публичный, в HTML). **siteverify ОТБИВАЕТ plain-токены sidecar'а (invalid-input-response) — только real_page (camoufox) solve**; action для signin = `authentication`, для телефона = `phone_verification`.
## 4. OAuth и browser-claim (работает там, где HTTP no-op)
- Raw approve формы OAuth (`hacktoberfest-api.mlh.com/oauth/mlh`) через urllib = 422 (не угадать скрытые поля Inertia-формы). РАБОЧИЙ ПУТЬ: Camoufox headless → `ctx.add_cookies([{name,value,domain:'.mlh.com',path:'/'}])` из сохранённой сессии (в т.ч. `mlh_token`) → goto OAuth start → `wait_for_load_state('networkidle')` → клик `button[type=submit]` на authorize-странице → landing `hacktoberfest.com/my/`.
- Токен API: localStorage ключ `hacktoberfest.session` → JSON → refreshToken → POST `https://hacktoberfest-api.mlh.com/auth/refresh` → accessToken; `Authorization: Bearer <accessToken>` на `/api/me/offers`, `/api/me`, `/api/me/fests`.
- Claim промокода: только КЛИКОМ в браузере по `[data-testid="claim-promo-code-button"]` (href=redeemUrl, method post, data {token}). После клика код читать из `page.inner_text('body')` — секция «Just claimed! ... Promo Code <CODE>»; в props промокод может остаться null даже после успеха.
## 5. Offers/redeem структура (MLH events)
- Redeem-страница: `https://www.mlh.com<redeemUrl>/redeem?token=<token>`; props: `token`, `redeemUrl`, `validLink`, `userMismatch`, `redemptionLimitExceeded`, `unmetClaimRequirements`, `claimRequirements`, `promoCode`, `previouslyClaimedPromoCodes`, `sponsor{name,slug}`.
- `unmetClaimRequirements=[]` → можно клеймить. Непустой → сначала закрыть гейты:
- `verified_phone`: POST `/api/phone_numbers` `{phone_number, channel:'sms', 'cf-turnstile-response'}` (turnstile action `phone_verification`) → POST `/api/phone_numbers/<id>/confirm_verification` `{code}` (6 цифр из SMS). Нужен реальный приём SMS.
- `github_oauth`: привязка GitHub-аккаунта к MLH (SocialProfiles); `.tech` дополнительно требует `github_account_age` (акк GH ≥30 дней — см. mlh_account_age_30_days в Redeem-чанке).
- Claimed-коды видны на `https://www.mlh.com/account/promo_codes` (component Account/PromoCodes, props `sponsors`, `totalCount`).
## 6. Сессии и переиспользование
- Стейт: `hf_mlh_sessions.json` (cookies string на акк) — переиспользуется и в urllib (http.cookiejar, domain `.mlh.com`) и в Camoufox (add_cookies). Сессия живёт днями; при `signed:False` — полный OTP-прогон заново (hf_reauth2.py паттерн).
- Долгие прогоны (captcha real_page 30-90с + IMAP wait) — только background с логом; foreground упрётся в timeout инструмента.
- Артефакты сессии-первоисточника: `C:/Users/User/tmp/hf_reauth2.py` (OTP), `hf_oauth_browser.py` (Camoufox OAuth), `hf_redeem_props.json`, `hf_codes_final.json`, `hf_mlh_sessions.json`.