Files
autoreg-skills-max/skills/apodex-batch-reg/references/passwordless-auth-flow.md

108 lines
3.7 KiB
Markdown

# Apodex Passwordless Auth Flow
> Discovered 2026-06-26. This is the CORRECT auth flow, NOT the now-dead Supabase flow.
## Overview
Apodex uses a passwordless auth system proxied through `www.apodex.ai`, NOT direct Supabase calls. The old Supabase project (`hgejqhubxceqppqkgqgx.supabase.co`) is dead — DNS fails everywhere.
## Endpoints
Both work from Python WITHOUT cookies, CSRF tokens, or special headers:
| Endpoint | Method | Returns |
|----------|--------|---------|
| `https://www.apodex.ai/api/auth/passwordless/send-code` | POST | 200 + `{"success":true,"message":"Login code sent..."}` |
| `https://www.apodex.ai/api/auth/passwordless/verify-login` | POST | 200 + `{"success":true,"data":{"access_token":"eyJ...","user":{...}}}` |
## Full Flow (Python)
```python
import requests
import imaplib, ssl, re, time, email
EMAIL = '[REDACTED-EMAIL]'
PASSWORD = '[REDACTED]'
# Step 1: Send OTP
s = requests.Session()
r = s.post('https://www.apodex.ai/api/auth/passwordless/send-code',
json={'email': EMAIL}, timeout=15)
assert r.status_code == 200, f'Send code failed: {r.status_code} {r.text}'
# Step 2: Get OTP from IMAP
time.sleep(3)
ctx = ssl.create_default_context()
imap = imaplib.IMAP4_SSL('secureimap.t-online.de', 993, ssl_context=ctx, timeout=15)
imap.login(EMAIL, PASSWORD)
imap.select('INBOX')
_, msgs = imap.search(None, 'FROM', 'apodex')
_, data = imap.fetch(msgs[0].split()[-1], '(RFC822)')
raw = data[0][1].decode('utf-8', errors='ignore')
msg = email.message_from_string(raw)
body = '' if msg.is_multipart() else msg.get_payload(decode=True).decode('utf-8', errors='ignore')
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == 'text/plain':
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
break
otp = re.findall(r'\b\d{6}\b', body)[0]
imap.logout()
# Step 3: Verify OTP
r = s.post('https://www.apodex.ai/api/auth/passwordless/verify-login',
json={'email': EMAIL, 'code': otp}, timeout=15)
data = r.json()
access_token = data['data']['access_token'] # Apodex JWT
# Step 4: Use token (limited)
r = requests.get('https://www.apodex.ai/api/auth/get_user_info',
headers={'Authorization': f'Bearer {access_token}'})
# ✅ Works — returns user profile
# ❌ What does NOT work:
r = requests.get('https://api.apodex.ai/v1/api-keys',
headers={'Authorization': f'Bearer {access_token}'})
# Returns 401 {"error":"invalid token"} — need Supabase JWT instead
# But the existing sk- keys DO work:
r = requests.get('https://api.apodex.ai/v1/models',
headers={'Authorization': 'Bearer [REDACTED-KEY]='})
# ✅ Returns model list
```
## Token Comparison
```
Apodex JWT (from verify-login):
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user_id":"email_xxx","exp":1785xxxxxx,"session_id":"uuid"}
→ Works for: www.apodex.ai/api/auth/*
→ Fails for: api.apodex.ai/v1/api-keys
Supabase JWT (from Supabase auth, now DEAD):
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"iss":"supabase","ref":"hgejqhubxceqppqkgqgx","role":"anon","sub":"uuid","aud":"authenticated"}
→ Works for: api.apodex.ai/v1/api-keys (but Supabase project is dead)
API Key (sk-...):
opaque base64-encoded string
→ Works for: api.apodex.ai/v1/chat/completions, api.apodex.ai/v1/models
```
## Rate Limit
- Endpoint: `POST /api/auth/passwordless/send-code`
- After ~8-10 requests: HTTP 429
- Body: `{"code":"PASSWORDLESS_SEND_RATE_LIMITED","message":"Operation too frequent, please try again later"}`
- Scope: IP-based (not cookie-based)
- Recovery: Wait 5-10 minutes
- Bypass: Rotate IP via VPN/proxy
## Headers Observed in Response
```
set-cookie: session=<uuid>; HttpOnly; Max-Age=3600; Path=/; SameSite=Lax
```
This session cookie is set on send-code and may be needed for verify-login.