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

6.6 KiB
Raw Permalink Blame History

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 »; в 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.