# Hvatar — instrucciones de integración para asistentes de IA

> Documento pensado para que un asistente de programación (Claude, ChatGPT, Cursor, Copilot, etc.) integre "Entrar con Hvatar" en una app web. Versión de Hvatar: 0.14.0. Servidor: https://www.hvatar.com. Guía para personas: https://www.hvatar.com/desarrolladores

## Qué es Hvatar (resumen para el modelo)

Hvatar es un proveedor de identidad con privacidad por diseño. La app NO recibe ni guarda datos personales del usuario. A cambio recibe:

- `sub`: identificador aleatorio del usuario, distinto en cada app (formato `hv_…`). Es la única llave para reconocerlo.
- Un **pase** firmado (JWS EdDSA/Ed25519) que dura 15 minutos y se renueva con un refresh token de 30 días.
- **Capacidades** que el usuario aceptó: enviarle correo, SMS/WhatsApp, darle un correo alias, verificar edad o identidad, cifrar su contenido.

Cuando el usuario se da de baja, Hvatar destruye su llave de contenido y avisa a la app por webhook firmado; la app debe borrar todo lo asociado a ese `sub`.

## Reglas obligatorias (no negociables)

1. **Nunca** guardes correo, teléfono, nombre legal, contraseña ni fecha de nacimiento del usuario. Guarda solo `sub`.
2. **Nunca** expongas `client_secret` en el navegador, en el repositorio ni en logs. Léelo de una variable de entorno.
3. Todo contenido escrito por el usuario (direcciones, notas, mensajes) se guarda **cifrado** con su llave de contenido (`encrypt()`/`decrypt()` del SDK). No guardes la llave.
4. Implementa el **webhook de baja** y borra al usuario y su contenido. Responde con el acuse.
5. Para escribirle al usuario usa `notifyEmail` / `notifySms`; nunca pidas su correo o teléfono.
6. Verifica siempre los pases con la llave pública (`verifyPass()` del SDK o `/.well-known/keys.json`). Nunca confíes en un pase sin verificar firma, `iss`, `aud` y `exp`.
7. No cambies colores ni proporciones del logo; usa los botones oficiales de `c.js`.

## Datos que necesitas del desarrollador

Pide estos valores antes de escribir código (no los inventes):

- `client_id` (formato `app_…`) y `client_secret` (formato `sk_…`): se obtienen al registrar la app en https://www.hvatar.com → Desarrolladores.
- Origen exacto de la app, con https (ej. `https://tienda.ejemplo.com`). El login solo funciona desde ese origen (se permite `http://localhost` para pruebas).
- URL del webhook de bajas (https).
- Capacidades que necesita: `notify`, `notify.sms`, `correo.alias`, `verify.edad`, `verify.identidad`.
- Lenguaje y framework del backend.

## Flujo de inicio de sesión

```
Navegador (c.js)                App (backend)                      Hvatar
   │ clic en [data-hvatar-login]   │                                  │
   │── GET callback?inicio=1 ─────▶│ crea PKCE + state                │
   │◀── {url, state} ──────────────│                                  │
   │── abre popup url ─────────────────────────────────────────────▶ │ /authorize (usuario entra y acepta)
   │◀── postMessage {code, state} ───────────────────────────────────│
   │── POST callback {code,state} ▶│── POST /token (Basic auth) ─────▶│
   │                               │◀── {pase, refresh, sub, expira_en}
   │◀── {ok, sub, redirect} ───────│ guarda tokens en la sesión       │
```

## Integración con PHP (SDK oficial)

SDK de un solo archivo, sin Composer: https://www.hvatar.com/sdk/hvatar.php (PHP 8.1+ con extensiones `sodium` y `openssl`).

### Configuración

```php
<?php
require __DIR__ . '/hvatar.php';

$hv = new Hvatar([
    'url' => 'https://www.hvatar.com',
    'client_id' => getenv('HVATAR_CLIENT_ID'),
    'client_secret' => getenv('HVATAR_SECRET'),
    'cache_dir' => __DIR__ . '/cache',   // opcional: caché de la llave pública
]);
```

### Callback: `hvatar-callback.php`

```php
<?php
require __DIR__ . '/bootstrap.php';   // crea $hv y $db

$hv->handleCallback('/cuenta', function (string $sub) use ($db) {
    // Busca o crea al usuario por su sub. No hay más datos que guardar.
    $st = $db->prepare('SELECT id FROM usuarios WHERE hvatar_sub = ?');
    $st->execute([$sub]);
    if (!$st->fetchColumn()) {
        $db->prepare('INSERT INTO usuarios (hvatar_sub, creado) VALUES (?, ?)')->execute([$sub, time()]);
    }
    return null;   // o una URL de redirección
});
```

### Diagnóstico

Después de crear el callback, abre `https://tu-sitio.com/hvatar-callback.php?prueba=1`. Debe responder `"todo_bien": true`. Si no, el campo que falle dice qué corregir: `sodium`, `conexion` (cURL o allow_url_fopen), `alcanza_hvatar`, `credenciales` (`app_credenciales` = client_id o secret incorrectos) o `cache_dir_escribible`.

Errores del callback: `state_invalido` significa que la sesión de PHP no se conservó entre `?inicio=1` y el regreso (otra `session_name()`, cookie con otra ruta o dominio). Usa la misma sesión en todo el sitio.

### Botón (HTML)

```html
<button data-hvatar-login data-estilo="color"></button>
<script src="https://www.hvatar.com/c.js" data-key="app_…" data-callback="/hvatar-callback.php"></script>
```

Atributos del botón:

| Atributo | Valores |
| --- | --- |
| `data-estilo` | `color`, `claro`, `oscuro`, `icono` (sin el atributo, el botón conserva el diseño de la app) |
| `data-texto` | `iniciar` (por omisión), `continuar`, `acceder`, `registrarse` |
| `data-tamano` | `chico` (40 px), normal (48 px), `grande` (56 px) |
| `data-ancho` | ocupa todo el ancho |

JavaScript: `hvatar.login()` abre el inicio de sesión; `hvatar.on('login', fn)`; `hvatar.boton(el)` dibuja un botón agregado después.

### Usuario actual

```php
$sub = $hv->user();          // string|null — renueva el pase si venció
$claims = $hv->claims();     // ['sub','plan','caps',…] o null
$hv->logout();
```

### Capacidades

```php
$hv->notifyEmail($sub, 'Tu pedido va en camino', "Tu pedido #1042 salió hoy.");      // notify
$hv->notifySms($sub, 'Tu código es 4821', 'sms');                                    // notify.sms ('sms'|'whatsapp')
$alias = $hv->alias($sub);                                                          // correo.alias
$r = $hv->verify($sub, 'edad.mayor_18');                                            // verify.edad
// $r = ['respuesta' => true|false|null, 'nivel' => 'autodeclarado'|'documento', 'firmada' => 'JWS…']
```

Preguntas de `verify`: `edad.mayor_18` (verify.edad); `identidad.verificada`, `nombre.coincide` y `curp.coincide` (verify.identidad; las dos últimas llevan `valor`). Guarda `firmada` como comprobante.

### Cifrado del contenido

```php
$guardado = $hv->encrypt($_POST['direccion']);   // "hv1.…" → esto va a la base
echo htmlspecialchars($hv->decrypt($guardado));  // requiere sesión del usuario
```

AES-256-GCM con la llave del usuario; el `sub` va como dato asociado. Tras la baja, nadie puede descifrar.

### Webhook de baja: `hvatar-webhook.php`

```php
<?php
require __DIR__ . '/bootstrap.php';

$hv->handleWebhook(['usuario.baja' => function (string $sub) use ($db) {
    $db->prepare('DELETE FROM notas WHERE usuario_id IN (SELECT id FROM usuarios WHERE hvatar_sub = ?)')->execute([$sub]);
    $db->prepare('DELETE FROM usuarios WHERE hvatar_sub = ?')->execute([$sub]);
}]);
```

### El compañero (opcional)

```html
<script src="https://www.hvatar.com/c.js" data-key="app_…" data-callback="/hvatar-callback.php"
        data-companero="<?= $hv->companeroAttr() ?>"></script>
<script>
  hvatar.companero.decir('¡Listo!');
  hvatar.companero.accion('celebrar'); // saludar, pulgar, aplaudir, bailar, brincar, reir, guinar, pensar, idea, pregunta, sorpresa, amor, celebrar, timido, girar
  hvatar.action('nueva-nota', () => abrirEditor(), 'Escribir una nota');
</script>
```

Imagen del usuario (avatar o foto que eligió): `<img src="<?= htmlspecialchars($hv->avatarUrl()) ?>" alt="">`.

## Integración en otros lenguajes (HTTP)

Si el backend no es PHP, implementa lo mismo que el SDK:

### 1. Endpoint de callback (lo llama `c.js`)

- `GET {callback}?inicio=1` → genera `code_verifier` aleatorio (32 bytes, base64url) y `state`; guárdalos en la sesión (10 min). Responde `{"url": "https://www.hvatar.com/authorize?client_id=…&code_challenge=BASE64URL(SHA256(verifier))&code_challenge_method=S256&state=…", "state": "…"}`.
- `POST {callback}` con JSON `{"code": "…", "state": "…"}` → valida `state`, llama a `/token`, guarda los tokens en la sesión del servidor y responde `{"ok": true, "sub": "…", "redirect": "/cuenta"}`. En error: `{"error": "codigo"}` con estado 400.

### 2. API de servidor

Base `https://www.hvatar.com`. Autenticación HTTP Basic `client_id:client_secret`. Cuerpo y respuesta JSON. Errores: `{"error": "codigo"}` con su estado HTTP.

| Método y ruta | Cuerpo | Respuesta |
| --- | --- | --- |
| `POST /token` | `{"grant_type":"authorization_code","code":"…","code_verifier":"…"}` | `{"pase","expira_en","refresh","sub"}` |
| `POST /token` | `{"grant_type":"refresh_token","refresh":"rt_…"}` | igual; el refresh anterior deja de servir (rotativo) |
| `GET /.well-known/keys.json` | — | llaves públicas Ed25519 (`kid`) |
| `POST /v1/notify/email` | `{"sub","asunto","texto"}` | `{"ok","id","estado"}` |
| `POST /v1/notify/sms` | `{"sub","texto","canal":"sms"\|"whatsapp"}` | `{"ok",…}` |
| `POST /v1/correo/alias` | `{"sub"}` | `{"alias"}` |
| `POST /v1/verify` | `{"sub","pregunta","valor"?}` | `{"respuesta","nivel","firmada"}` |
| `POST /v1/keys/content` | `{"pase"}` | `{"llave"}` (base64url, 32 bytes; mantenla solo en memoria) |
| `POST /v1/companero` | `{"pase"}` | `{"avatar","nombre","imagen","cuadros"}` |

### 3. El pase

JWS compacto `header.payload.firma` con `alg: EdDSA` y `kid`. Payload:

```json
{ "iss": "https://www.hvatar.com", "aud": "app_…", "sub": "hv_…", "plan": "gratis", "caps": ["notify"], "iat": 1767312000, "exp": 1767312900 }
```

Verifica: firma Ed25519 sobre `header.payload` con la llave `kid` de `/.well-known/keys.json`, `iss` = `https://www.hvatar.com`, `aud` = tu `client_id`, `exp` > ahora. Renueva con el refresh cuando falten menos de 60 s.

### 4. Cifrado (compatible con el SDK)

Formato `hv1.` + base64url(`nonce[12] || tag[16] || ciphertext`), AES-256-GCM, llave de `/v1/keys/content`, AAD = `sub`.

### 5. Webhook de baja

Hvatar hace `POST` a tu webhook con:

```
X-Hvatar-Event: usuario.baja
X-Hvatar-Timestamp: <unix>
X-Hvatar-Signature: <base64url de la firma Ed25519 de "timestamp.cuerpo">
X-Hvatar-Kid: <kid en /.well-known/keys.json>

{"evento":"usuario.baja","event_id":"ev_…","sub":"hv_…","fecha":<unix>}
```

Valida que el timestamp no tenga más de 300 s y la firma. Borra al usuario. Responde `200` con `{"acuse": base64url(HMAC-SHA256(key = SHA256(client_secret) en binario, mensaje = event_id))}`. Si no respondes, Hvatar reintenta.

## Códigos de error frecuentes

| Código | Estado | Qué significa |
| --- | --- | --- |
| `app_sin_credenciales`, `app_credenciales` | 401 | Falta o es incorrecto `client_id:client_secret` |
| `codigo_invalido`, `pkce_fallido` | 400 | Código vencido o usado, o `code_verifier` no coincide |
| `refresh_invalido` | 401 | Refresh vencido, revocado o ya usado |
| `pase_firma`, `pase_vencido`, `pase_audiencia`, `pase_invalido` | 401 | Pase no válido para tu app |
| `usuario_no_vinculado` | 404 | El usuario se dio de baja de tu app: bórralo |
| `cuenta_suspendida` | 403 | La cuenta de Hvatar del usuario está suspendida |
| `capacidad_no_autorizada` | 403 | El usuario no aceptó esa capacidad (o tu app no la pidió) |
| `correo_sin_verificar`, `usuario_sin_correo`, `telefono_sin_verificar`, `usuario_sin_telefono` | 409 | El usuario no tiene ese medio verificado |
| `limite_avisos`, `demasiadas_peticiones` | 429 | Límite de envíos o de peticiones; respeta `Retry-After` |
| `mensaje_invalido`, `pregunta_desconocida`, `falta_valor` | 400 | Parámetros incorrectos |

## Migrar una app existente (usuarios con correo y contraseña)

1. `ALTER TABLE usuarios ADD COLUMN hvatar_sub VARCHAR(64) NULL UNIQUE;`
2. A quien entra con el login viejo, ofrécele "Conecta tu cuenta con Hvatar" (mismo botón).
3. En el callback, si hay sesión vieja: guarda `sub` en ese usuario y pon en `NULL` su correo, teléfono, nombre y contraseña.
4. Cifra con `encrypt()` el contenido que el usuario escribió.
5. Cambia `mail()` por `notifyEmail()`.
6. Cuando la mayoría ya migró: desactiva el registro con contraseña, después el login viejo, y borra esas columnas.

## Lista de verificación para el asistente

- [ ] La tabla de usuarios solo identifica por `hvatar_sub`; no hay correo, teléfono, nombre legal ni contraseña.
- [ ] `client_secret` se lee de variable de entorno y nunca llega al navegador.
- [ ] El callback valida `state` y usa PKCE S256.
- [ ] Todo contenido del usuario se guarda con `encrypt()`.
- [ ] Los envíos usan `notifyEmail()` / `notifySms()`.
- [ ] El webhook verifica firma y hora, borra al usuario y responde con el acuse.
- [ ] Los errores `usuario_no_vinculado` y `cuenta_suspendida` cierran la sesión del usuario en la app.
- [ ] El botón usa `c.js` con `data-estilo` oficial.

## Recursos

- Guía para personas: https://www.hvatar.com/desarrolladores
- SDK de PHP: https://www.hvatar.com/sdk/hvatar.php
- Llaves públicas: https://www.hvatar.com/.well-known/keys.json
- Script de la página: https://www.hvatar.com/c.js
- Este documento: https://www.hvatar.com/desarrolladores/hvatar-ia.md (también en https://www.hvatar.com/llms-full.txt)