---
id: 0039
title: Gestor de cookies y sesiones por profile
status: pending
priority: high
created: 2026-05-04
depends_on: [0038]
---

## Contexto y objetivo

Una vez que `0038` deja al app lanzar browsers externos con
`--user-data-dir` por profile, las cookies y el `localStorage` quedan
persistidas automaticamente por Chrome/Edge en disco. Este issue cubre
la **gestion explicita** de esas sesiones desde graph_explorer:
visualizarlas, exportarlas, importarlas, limpiarlas y monitorizar cuando
caducan.

Sin esto, los profiles son cajas negras que solo se manipulan abriendo
el browser. Un OSINT serio necesita ver "que sesiones tengo activas, en
que dominios, cuando expiran, que hacer si alguna se rompe".

## Alcance

### 1. Inventario de sesiones (CDP read-only)

Panel **Sessions** dentro o junto al panel **Browsers**:

```
┌─ Sessions in profile: linkedin ─────────────────────────┐
│ Domain               Cookies   Auth?   Expires          │
│ linkedin.com           42       Yes    2026-08-12       │
│ www.linkedin.com       18       Yes    session          │
│ static.licdn.com        6       No     2027-01-01       │
│ google-analytics.com    3       No     2026-11-01       │
│                                                          │
│ [Export profile]  [Import .json]  [Clear domain]        │
│ [Clear all cookies]  [Open browser]                     │
└──────────────────────────────────────────────────────────┘
```

Datos via `Network.getAllCookies` (CDP). Detectar "Auth?" heuristico:
cookie con flag `httpOnly=true` o nombre matching
`/sess|auth|token|sid|li_at|c_user/i`.

### 2. Export / import de profiles

- **Export profile** → `<app>/local_files/exports/<profile>-<date>.json`
  con TODAS las cookies + un manifest (`browser`, `version`,
  `created_at`, `domains`).
- **Import .json** → escribe via `Network.setCookie` sobre un profile
  vivo. Si el profile no esta vivo, lanzarlo headless solo para inyectar
  y cerrarlo.
- Formato JSON compatible con la extension EditThisCookie (estandar de
  facto), para que el usuario pueda importar/exportar fuera del app.

### 3. Limpieza selectiva

| Accion | Implementacion CDP |
|---|---|
| Clear all cookies | `Network.clearBrowserCookies` |
| Clear domain | iterar `getAllCookies` + `deleteCookies` filtrado |
| Clear cookie | `Network.deleteCookies` con name+domain |
| Clear localStorage de un origin | `Storage.clearDataForOrigin` |

Antes de cualquier "clear all" → confirmacion modal con el numero de
cookies que se van a perder.

### 4. Health check de sesiones autenticadas

Por profile, opcional, definir en `<profile>/.fn_session.yaml`:

```yaml
auth_check:
  - name: linkedin
    url: https://www.linkedin.com/feed/
    success_selector: "main[role='main']"
    redirect_to_login_means_failed: true
  - name: google
    url: https://myaccount.google.com
    success_selector: "[data-email]"
```

El app puede lanzar un check por sesion (CDP navigate + check selector)
y mostrar:

```
linkedin     ✓ Authenticated     (last check: 2 min ago)
google       ✗ Login expired     (redirected to /accounts/signin)
twitter      ? Never checked     [check now]
```

Health checks NO son automaticos por defecto — requieren click manual
para no quemar sesiones con polls innecesarios. Opcion de "check on
launch" por profile.

### 5. Lock / busy state

Cuando un enricher esta usando un profile (CDP busy), bloquear acciones
destructivas (clear, import) en el panel — solo lectura permitida.
Visual: candado al lado del profile + tooltip "in use by enricher
fetch_webpage_browser".

Lock implementado en `graph_explorer.db` tabla `browser_locks(profile,
holder, acquired_at)`. TTL 60s por si el holder muere sin liberar.

## Schema en `graph_explorer.db`

```sql
CREATE TABLE browser_profiles (
    name TEXT PRIMARY KEY,
    browser_path TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    notes TEXT
);

CREATE TABLE browser_locks (
    profile TEXT PRIMARY KEY REFERENCES browser_profiles(name),
    holder TEXT,                  -- enricher_id, panel, etc.
    acquired_at TIMESTAMP,
    expires_at TIMESTAMP
);

CREATE TABLE browser_session_checks (
    id INTEGER PRIMARY KEY,
    profile TEXT REFERENCES browser_profiles(name),
    check_name TEXT,              -- 'linkedin', 'google', ...
    status TEXT,                  -- 'authenticated' | 'expired' | 'error'
    detail TEXT,
    checked_at TIMESTAMP
);
```

## Plan de implementacion

| Fase | Entregable |
|---|---|
| 0039a | `cdp-cli cookies list --profile X` — JSON con todas las cookies por dominio. Lectura via `Network.getAllCookies`. |
| 0039b | `cdp-cli cookies export --profile X --out file.json` — formato EditThisCookie. |
| 0039c | `cdp-cli cookies import --profile X --in file.json` — `Network.setCookie` masivo. |
| 0039d | `cdp-cli cookies clear --profile X [--domain Y]` — clear selectivo. |
| 0039e | Panel **Sessions** en C++: tabla agregada por dominio, botones de export/import/clear. |
| 0039f | `cdp-cli session-check --profile X --config <file>.yaml` — health check con selectores. |
| 0039g | UI de health check en panel Sessions: status por sesion, boton "check now". |
| 0039h | Locks: implementar `browser_locks` + UI para mostrar profile busy. |
| 0039i | Migrar profiles preexistentes (creados manualmente) — boton "Register existing folder". |

## Riesgos y mitigaciones

| Riesgo | Mitigacion |
|---|---|
| Importar cookies sobreescribe datos validos | Dialogo de import con preview + opcion "merge" vs "replace". |
| Export con cookies de auth = secret en disco | `local_files/exports/` con permisos 0600 en POSIX. Warning explicito al exportar. Documentar en panel. |
| Health check quema rate limit del sitio | Caching: si ultimo check < 5 min, no re-checkear sin force. |
| Cookies httpOnly no se ven via JS pero si via CDP | Documentar: el panel muestra MAS cookies que las visibles desde DevTools de la pagina. |
| Diferencias de schema entre Chrome / Edge / Brave | CDP es estandar — mismo subset funciona en todos. Tests por browser en CI manual. |

## Definicion de hecho

- En el panel **Sessions** del profile `linkedin`, veo 42 cookies
  agrupadas por dominio, con flag de auth detectado y fecha de
  expiracion.
- Click en **Export profile** genera un JSON valido que la extension
  EditThisCookie puede importar en otro browser.
- Click en **Clear domain: google-analytics.com** elimina solo esas 3
  cookies sin tocar las demas.
- Configurando un `auth_check` para LinkedIn y haciendo click en
  **Check now**, el app lanza CDP, navega, evalua el selector y muestra
  `✓ Authenticated` en menos de 4s.
- Un enricher corriendo sobre `linkedin` bloquea las acciones
  destructivas del panel (boton "Clear all" deshabilitado con tooltip).
- Cerrar manualmente el browser y volver a lanzarlo desde el panel
  conserva todas las cookies del profile (verificado por el inventario
  pre/post).

## Fuera de alcance

- Cifrado at-rest del `user-data-dir` — Chrome ya cifra los secrets con
  DPAPI (Windows) / Keychain (Mac). En Linux Chrome usa cifrado debil
  por defecto, pero esta fuera de nuestro control.
- Sync de profiles entre PCs — los `local_files/browser_profiles/`
  estan gitignorados a proposito (secrets). Si en el futuro hace falta,
  un export manual + import en el otro PC es la via.
- Auto-renovacion de tokens (cuando un sitio refresca cookies via
  refresh_token) — fuera de v1, demasiado especifico por sitio.
