# Radio Babasónica Alexa v0.1 — Resultados de implementación

## Resumen

```text
Implementación completada: PARCIAL (backend completo; falta Skill ID real y prueba en Alexa/Echo)
Endpoint público: https://aproposito.ddns.net/radio-babasonica-alexa/public/
Stream: https://aproposito.ddns.net/live/radiobbs/index.m3u8
PHP: 8.2.12
Skill ID configurado: NO
Verificación Alexa: PENDIENTE de activación con Skill ID; firma/certificado/timestamp implementados
```

No se modificó SignalLive, MediaMTX, `mediamtx.yml`, `/live/`, `radiobbs`, FFmpeg, la app C#, WordPress ni la Skill del Mundial. No se reinició ningún servicio.

## Archivos creados

- `.gitignore`
- `README.md`
- `config/config.php`
- `interaction-model/es-MX.json`
- `public/.htaccess`
- `public/index.php`
- `src/AlexaRequestHandler.php`
- `src/AlexaRequestVerifier.php`
- `src/AlexaResponse.php`
- `src/RadioBabasonicaController.php`
- `src/SanitizedLogger.php`
- `src/bootstrap.php`
- `storage/cert-cache/.gitkeep`
- `storage/logs/.gitkeep`
- `tests/run.php`
- `tests/fixtures/LaunchRequest.json`
- `tests/fixtures/PlayRadioIntent.json`
- `tests/fixtures/PauseIntent.json`
- `tests/fixtures/ResumeIntent.json`
- `tests/fixtures/StopIntent.json`
- `tests/fixtures/PlaybackStarted.json`
- `tests/fixtures/PlaybackStopped.json`
- `tests/fixtures/PlaybackFailed.json`
- `tests/fixtures/PlaybackControllerPlay.json`
- `tests/fixtures/PlaybackControllerPause.json`
- `RADIO_BABASONICA_ALEXA_V01_IMPLEMENTATION_RESULTS.md`

El archivo de log `storage/logs/alexa-radio.log` fue generado por las pruebas y está excluido de Git.

## Archivos modificados

Ningún archivo preexistente fue modificado. El documento de instrucciones `RADIO_BABASONICA_ALEXA_V01_CODEX.md` se conservó intacto.

## Archivos inspeccionados de otros proyectos

- `C:\xampp\htdocs\worlcup-api-DISABLED\index.php`: bootstrap, recepción JSON y cabecera de respuesta.
- `C:\xampp\htdocs\worlcup-api-DISABLED\controllers\AlexaController.php`: patrón de enrutamiento Alexa.
- `C:\xampp\htdocs\worlcup-api-DISABLED\services\AlexaResponse.php`: patrón de response envelope.
- `C:\xampp\htdocs\worlcup-api-DISABLED\config.php`: patrón de configuración. Se detectó información sensible y no se copió ni se incluyó aquí.
- `C:\xampp\apache\conf\httpd.conf`: DocumentRoot e inclusión de virtual hosts.
- `C:\xampp\apache\conf\extra\httpd-vhosts.conf`: VirtualHost HTTP y rutas existentes.
- `C:\xampp\apache\conf\extra\httpd-ssl.conf`: VirtualHost HTTPS, DocumentRoot y coexistencia con `/live/`. No se registran secretos de esa configuración.

La implementación del Mundial no valida firma, timestamp ni applicationId y registra el request completo. Por seguridad sólo se reutilizó el patrón conceptual de clases; no se reutilizó su transporte ni su logging.

## Arquitectura resultante

```text
Alexa HTTPS POST
       |
       v
public/index.php
       |
       v
AlexaRequestHandler -----> errores HTTP controlados
       |
       +--> AlexaRequestVerifier
       |      +-- URL de certificado restringida
       |      +-- certificado X.509 y SAN
       |      +-- firma del cuerpo
       |      +-- timestamp <= 150 s
       |      +-- applicationId == ALEXA_SKILL_ID
       |
       +--> RadioBabasonicaController
                +--> AlexaResponse
                +--> SanitizedLogger --> storage/logs/alexa-radio.log
```

## Interaction Model

Ruta: `C:\xampp\htdocs\radio-babasonica-alexa\interaction-model\es-MX.json`

Locale `es-MX`, invocación `radio babasónica`, `PlayRadioIntent`, intents AudioPlayer obligatorios y recomendados, sin slots.

## Pruebas ejecutadas

| Prueba | Resultado |
|---|---|
| LaunchRequest | PASS: Play, REPLACE_ALL, token/URL/offset correctos, habla y termina sesión |
| PlayRadioIntent | PASS: misma acción que LaunchRequest |
| PauseIntent | PASS: AudioPlayer.Stop |
| ResumeIntent | PASS: Play al live edge, offset 0 |
| StopIntent | PASS: AudioPlayer.Stop |
| PlaybackStarted | PASS: sin `session`, silencioso y registrado |
| PlaybackStopped | PASS: sin `session`, silencioso y registrado |
| PlaybackFailed | PASS: sin `session`, silencioso y error sanitizado registrado |
| PlaybackController Play | PASS: Play sin outputSpeech/card/reprompt |
| PlaybackController Pause | PASS: Stop sin outputSpeech/card/reprompt |
| JSON inválido | PASS: HTTP 400 JSON controlado |
| Stream ffprobe | PASS: AAC-LC, 48000 Hz, stereo, sólo audio, ~128 kbps |
| Endpoint HTTPS | PASS: GET HTTP 405; POST JSON inválido HTTP 400; Content-Type JSON; TLS válido |

Además, todos los PHP pasaron `php -l`; `tests/run.php` terminó con `ALL TESTS PASSED`.

## Respuesta exacta de LaunchRequest

```json
{"version":"1.0","response":{"directives":[{"type":"AudioPlayer.Play","playBehavior":"REPLACE_ALL","audioItem":{"stream":{"token":"radiobbs-live-v1","url":"https://aproposito.ddns.net/live/radiobbs/index.m3u8","offsetInMilliseconds":0}}}],"shouldEndSession":true,"outputSpeech":{"type":"PlainText","text":"Estás escuchando Radio Babasónica."}}}
```

## Respuesta exacta de PauseIntent

```json
{"version":"1.0","response":{"directives":[{"type":"AudioPlayer.Stop"}],"shouldEndSession":true}}
```

## Respuesta exacta de ResumeIntent

```json
{"version":"1.0","response":{"directives":[{"type":"AudioPlayer.Play","playBehavior":"REPLACE_ALL","audioItem":{"stream":{"token":"radiobbs-live-v1","url":"https://aproposito.ddns.net/live/radiobbs/index.m3u8","offsetInMilliseconds":0}}}],"shouldEndSession":true,"outputSpeech":{"type":"PlainText","text":"Estás escuchando Radio Babasónica."}}}
```

## Seguridad

- **Validación de firma:** exige `SignatureCertChainUrl` y `Signature`; limita la URL a HTTPS, puerto 443, host `s3.amazonaws.com` y ruta `/echo.api/`; descarga con validación TLS; valida vigencia del X.509 y SAN exacto `echo-api.amazon.com`; verifica la firma SHA-1 del cuerpo con OpenSSL. El certificado público se almacena como caché por 24 horas; no se almacena ninguna clave privada.
- **Validación timestamp:** rechaza timestamps inválidos o con diferencia mayor de 150 segundos.
- **Validación Skill ID:** compara en tiempo constante el applicationId de `session` o `context.System` con `ALEXA_SKILL_ID`. Si no está configurado, el endpoint rechaza la petición; no existe modo público inseguro.
- **Sanitización logs:** sólo se escriben metadatos funcionales permitidos. El applicationId se abrevia. No se escriben tokens de acceso, cookies, headers, firma, cuerpo crudo, secretos ni stack traces.
- **HTTP:** sólo POST, JSON controlado, `Content-Type: application/json`, errores genéricos y `display_errors=0`.

## Cambios manuales pendientes

1. Crear una Custom Skill en Alexa Developer Console.
2. Seleccionar locale `es-MX`.
3. Usar invocation name `radio babasónica`.
4. Importar `interaction-model/es-MX.json` y construir el modelo.
5. Activar la interfaz AudioPlayer.
6. Configurar endpoint HTTPS `https://aproposito.ddns.net/radio-babasonica-alexa/public/` y la opción de certificado de autoridad confiable.
7. Copiar el Skill ID real a la variable de entorno `ALEXA_SKILL_ID` accesible por Apache. Esto requerirá reiniciar Apache manualmente; no se hizo automáticamente.
8. Probar primero en Alexa Simulator y luego en un Echo físico.

No se necesita una modificación de VirtualHost para exponer la URL elegida: el DocumentRoot HTTPS existente ya es `C:\xampp\htdocs` y permite `.htaccess`.

## Riesgos / pendientes

- La verificación completa no puede probarse con una petición Alexa real hasta configurar el Skill ID y conectar Developer Console.
- Confirmar en Echo físico que Alexa acepta el HLS/fMP4 live y observar cualquier `AudioPlayer.PlaybackFailed`.
- La disponibilidad continua depende del stream y del acceso saliente del servidor a los certificados públicos de Amazon.
- El endpoint público está correctamente cerrado mientras falta el Skill ID; por ello el estado global es **PARCIAL**, listo para la fase manual de Developer Console pero no para una prueba Alexa exitosa todavía.

## Criterios de aceptación

```text
[x] LaunchRequest produce AudioPlayer.Play válido
[x] Stream URL limpia sin cookieCheck
[x] shouldEndSession = true en Play
[x] Pause funciona
[x] Resume vuelve al live edge
[x] Stop funciona
[x] AudioPlayer events no rompen el endpoint
[x] PlaybackController no devuelve propiedades prohibidas
[x] Endpoint acepta requests sin objeto session
[x] Logs sanitizados
[x] No se modificó MediaMTX
[x] No se modificó radiobbs
[x] Interaction Model es-MX generado
[x] URL HTTPS del webhook identificada y probada
[ ] Skill ID real configurado y verificación probada con Alexa
[x] Reporte final generado
```
