# Session Learnings: ephron.ren QA (2026-05-03)

## Environment Facts
- 5 services: Home(8000), Auth(8001), Blog(8002), Canvas(8003), Prompt(8004)
- Auth: FastAPI + Tortoise ORM, `.ephron.ren` domain cookie
- RBAC: user(10) < admin(20) < owner(30)
- CSRF: `{unix_timestamp}:{sha256_hex}` format, 75 chars, per-GET refresh
- Rate limits: login 5/min, register 6/hour, comments 6/min, likes 11/min, save 21/min

## High-Value Findings (Reproducible Patterns)

### CSP script-src-elem Override (Critical)
- **Symptom**: Buttons with `onclick="fnName()"` do nothing, `typeof fnName` returns `undefined`
- **Root cause**: `script-src-elem 'self' https://cdn.example.com` overrides `script-src 'unsafe-inline'`
- **Detection**: `curl -sI URL | grep content-security-policy`, look for `script-src-elem` without `'unsafe-inline'`
- **Impact**: All inline JS blocked → save/publish/discard buttons broken, client-only validation bypassed

### CSP form-action Blocks Cross-Origin Redirects (Critical)
- **Date**: 2026-05-05
- **Symptom**: Login form submits (POST appears in network tab), server sets cookie, but browser stays on login page — no redirect
- **Root cause**: CSP `form-action 'self'` on the 303 redirect response blocks navigation to cross-origin targets
- **Reproduction**:
  1. Visit `https://auth.ephron.ren/login?redirect=aHR0cHM6Ly93d3cuZXBocm9uLnJlbi8=` (redirect=base64 of `https://www.ephron.ren/`)
  2. Fill username/password, click submit
  3. Browser sends POST to `/api/login` (same origin ✅ allowed)
  4. Server returns 303 to `https://www.ephron.ren/` with CSP header containing `form-action 'self'`
  5. Browser blocks redirect: `https://www.ephron.ren/` ≠ `self` (`https://auth.ephron.ren`)
- **Controlled test**: Same-origin redirect (`auth.ephron.ren/admin`) works fine; cross-origin fails
- **Console error**: `Sending form data to 'https://auth.ephron.ren/api/login' violates the following Content Security Policy directive: "form-action 'self'"`
- **Fix**: Skip CSP header on 303 responses (empty body, no protection value), or use JS redirect
- **Affected pages**: ALL pages that redirect to login with a cross-origin redirect target (www/blog/canvas/prompt subdomains)
- **Key source files**: `shared/security_headers.py` (CSP middleware), `auth/src/routes/api.py` (login endpoint), `auth/src/utils/redirect.py` (redirect validation)

### Server-Side Password Validation Missing
- **Test**: `curl -X POST /api/register -d 'username=test&password=123&password_confirm=456&invite_code=CODE'`
- **Expected**: 400/422 with validation error
- **Actual**: 303 redirect (registration succeeds with weak/mismatched passwords)
- **Root cause**: Validation only in client JS (blocked by CSP)
- **Lesson**: Always test form validation with curl, not just browser

### Fulltext Search Silent Failure
- **Test**: `GET /posts?q=openclaw&mode=fulltext` returns 0 results, `mode=simple` returns 6
- **Root cause**: BM25 index not built or jieba tokenizer not installed
- **Detection**: Compare simple vs fulltext results for same query

### API Auth Order Bug
- **Test**: `POST /api/service/posts` without token, with invalid body
- **Expected**: 401 (unauthenticated)
- **Actual**: 422 (body validation error — leaks endpoint info)
- **Root cause**: Pydantic validation middleware runs before auth middleware

## Delegate Task Sizing
- Curl-only tasks: max ~15-20 test cases per delegate (30+ cases timeout at 600s)
- Browser tasks: max ~5-8 interactions per delegate (each = 10-30s)
- Use `execute_code` with `from hermes_tools import terminal` for fastest execution
- Parallel delegates: 3 max, but each should be independently scoped

## Cookie Jar Synchronization
- CSRF token changes on every GET request
- Must use SAME cookie jar for GET (extract token) and POST (submit form)
- Multiple CSRF tokens on one page (one per form) — extract from specific form context
- Cross-service cookies: Domain=.ephron.ren should work for all subdomains
- If cross-service test fails, check cookie jar file, not the cookie itself

## Content Restoration Pattern (Playwright)
When homepage/admin content is accidentally overwritten, restore via Playwright:
1. Prepare JSON with original content (experience/projects/skills/contact/footer)
2. Login → navigate to admin page
3. Use `page.evaluate()` to set form fields by `id=` (NOT `name=` — admin forms use id):
   ```js
   document.getElementById('contact_email').value = '...';
   document.getElementById('footer_copyright').value = '...';
   ```
4. Set structured data: `initialContent.experience = [...]; renderExperience();`
5. Set `is_draft: false` for items that should be published
6. Collect and publish:
   ```js
   const content = collectFormData();
   document.querySelector('input[name="content_json"]').value = JSON.stringify(content);
   // Find form with content_json input, set action=/admin/publish, submit
   ```
7. Verify with `curl -s https://site/` checking for restored content strings

## Form Field Discovery
- Admin page fields may use `id=` instead of `name=` — check both:
  ```bash
  curl -s -b cookies /admin | grep -oP 'id="[^"]*"' | sort -u
  curl -s -b cookies /admin | grep -oP 'name="[^"]*"' | sort -u
  ```
- `collectFormData()` reads from visible form elements, not hidden `content_json`
- Setting `content_json` directly is overwritten by `collectFormData()` on submit

## Playwright vs curl for Form Submission
- **curl**: CSRF token sync is fragile (token changes per-GET, cookie jar must match)
- **Playwright**: Handles cookies/CSRF automatically, but CSP may block inline JS
- **Best approach**: Use Playwright + `page.evaluate()` to bypass CSP-blocked functions
- **Pattern**: Set form fields via JS → call `collectFormData()` → set `content_json` → submit form directly
