{
  "android": {
    "sdk_info": {
      "name": "Ríos de Fuego Central SDK",
      "platform": "Android",
      "version": "1.2.12",
      "package": "riosdefuego.central.rdf_sdk",
      "description": "SDK Android para vinculación remota de cuentas, gestión de sesiones y seguridad de usuarios en el ecosistema Ríos de Fuego."
    },
    "setup": {
      "maven_repository": "https://www.riosdefuego.com/repo",
      "dependency": "implementation(\"riosdefuego.central:rdf-sdk:1.2.12\")",
      "requirements": {
        "min_sdk": 24,
        "kotlin_version": "2.0.0+",
        "compose": "required"
      }
    },
    "core_functions": {
      "initialize": {
        "signature": "RDFAuth.initialize(context: Context, appId: String, apiKey: String)",
        "description": "Configura el SDK con las credenciales de la aplicación. Debe llamarse en el onCreate de la Activity o Application.",
        "security": "Obtiene ANDROID_ID como identificador del dispositivo, recupera la sesión cifrada y valida automáticamente en el servidor cualquier vínculo persistido.",
        "advertising": "Registra una apertura real por proceso y, para aplicaciones gratuitas, muestra automáticamente un banner cada cinco inicializaciones."
      },
      "startAuth": {
        "signature": "suspend RDFAuth.startAuth(userIdentifier: String): Result<AuthInitResponse>",
        "description": "Inicia una solicitud de vinculación remota para un email o nombre de usuario RDF. El dispositivo y su descripción se adjuntan automáticamente.",
        "notes": [
          "Limpia cualquier sesión local anterior antes de comenzar.",
          "La solicitud debe aprobarse desde Ríos de Fuego Central.",
          "Las aplicaciones gratuitas reciben show_ads=true para mostrar el banner correspondiente."
        ]
      },
      "checkStatus": {
        "signature": "suspend RDFAuth.checkStatus(sessionToken: String): Result<AuthStatusResponse>",
        "description": "Consulta el estado pending, approved o denied de una vinculación. Cuando se aprueba, conserva los datos básicos del usuario.",
        "notes": [
          "Una aprobación válida incluye jwt y user.",
          "La pantalla RDFLoginScreen realiza esta consulta automáticamente cada tres segundos."
        ]
      },
      "getAuthToken": {
        "signature": "RDFAuth.getAuthToken(): String?",
        "description": "Retorna el JWT actual si el usuario está vinculado; de lo contrario, retorna null."
      },
      "getUserData": {
        "signature": "RDFAuth.getUserData(): RDFUserData?",
        "description": "Retorna un objeto con uuid, user_name y avatar_url del usuario vinculado.",
        "persistence": "Los datos se mantienen tras cerrar la app gracias al almacenamiento seguro."
      },
      "getAd": {
        "signature": "suspend RDFAuth.getAd(): Result<AdResponse>",
        "description": "Obtiene el banner publicitario asignado sin consumir saldo. Si pertenece a una campaña incluye impression_token y confirmation_required=true; target_url apunta al redirect HTTPS controlado de RDF y nunca expone directamente el destino del anunciante."
      },
      "confirmAdView": {
        "signature": "suspend RDFAuth.confirmAdView(impressionToken: String): Result<AdImpressionResponse>",
        "description": "Confirma idempotentemente que la imagen publicitaria cargó dentro del componente visible. RDFLoginScreen y el diálogo de inicio la invocan automáticamente; una repetición no genera otro cobro."
      },
      "getLinkedUsers": {
        "signature": "suspend RDFAuth.getLinkedUsers(): Result<List<RDFUserData>>",
        "description": "Devuelve los usuarios con vínculo activo en la aplicación.",
        "requirement": "Requiere una sesión válida; el SDK inyecta automáticamente Bearer y X-Device-ID."
      },
      "reportUser": {
        "signature": "suspend RDFAuth.reportUser(reason: String): Result<Boolean>",
        "description": "Envía una denuncia para aumentar los warnings de un usuario en la base de datos central.",
        "requirement": "Requiere una sesión y un vínculo activos. El motivo es obligatorio y admite hasta 500 caracteres. Cada llamada registra una advertencia real."
      },
      "validateSession": {
        "signature": "suspend RDFAuth.validateSession(): Result<Boolean>",
        "description": "Verifica si el vínculo entre el usuario y la app sigue activo en el servidor.",
        "behavior": "Si el vínculo fue revocado o una petición protegida que envió el JWT recibe 401/403, cierra la sesión local automáticamente. Los errores de publicidad y de otros endpoints públicos no modifican la sesión. Desde 1.2.12 también se ejecuta al restaurar una sesión y al volver la aplicación al primer plano."
      },
      "sessionState": {
        "signature": "RDFAuth.sessionState: StateFlow<RDFSessionState>",
        "description": "Expone CHECKING, AUTHENTICATED o UNAUTHENTICATED para que la interfaz espere la validación inicial y reaccione inmediatamente cuando el servidor confirme una desvinculación.",
        "network_behavior": "Un error transitorio de conectividad conserva la sesión local y permite reintentar; únicamente una revocación confirmada o un rechazo protegido elimina las credenciales."
      },
      "updateFcmToken": {
        "signature": "RDFAuth.updateFcmToken(token: String)",
        "description": "Actualiza y conserva de forma cifrada el token FCM que se enviará al iniciar futuras solicitudes de vinculación."
      },
      "setAuthToken": {
        "signature": "RDFAuth.setAuthToken(token: String?, persist: Boolean = true)",
        "description": "Establece manualmente el JWT en memoria y permite decidir si se conserva de forma cifrada. Normalmente RDFLoginScreen gestiona esta operación."
      },
      "logout": {
        "signature": "RDFAuth.logout()",
        "description": "Cierra la sesión local y limpia todos los datos persistidos (token y perfil de usuario)."
      }
    },
    "ui_components": {
      "RDFLoginScreen": {
        "type": "Composable",
        "params": "onLoginSuccess: (token: String) -> Unit, viewModel: RDFLoginViewModel = viewModel()",
        "description": "Pantalla completa que solicita el identificador, inicia la vinculación, consulta su estado y muestra la aprobación o los errores con una interfaz coherente con Ríos de Fuego Central."
      },
      "RDFAuthButton": {
        "type": "Composable",
        "params": "onClick: () -> Unit, modifier: Modifier = Modifier, text: String = \"Iniciar sesión con Ríos de Fuego\"",
        "description": "Botón reutilizable con el estilo visual oficial de Ríos de Fuego."
      },
      "RDFAdBanner": {
        "type": "Composable",
        "params": "imageUrl: String, targetUrl: String, modifier: Modifier = Modifier, displayKey: String = imageUrl, onDisplayed: () -> Unit = {}",
        "description": "Renderiza el banner 1024x500, abre su destino al tocarlo y notifica una sola vez cuando Coil terminó de cargarlo y la Activity está en estado RESUMED."
      },
      "RDF_SDKTheme": {
        "type": "Composable Theme",
        "description": "Tema Material 3 con modos claro y oscuro, paleta, tipografía, superficies y formas coherentes con Ríos de Fuego Central."
      }
    },
    "internal_security": {
      "interceptor": "OkHttp inyecta X-Device-ID en todas las solicitudes y Authorization: Bearer {JWT} únicamente en endpoints protegidos. No registra tokens, encabezados ni cuerpos sensibles en Logcat.",
      "device_binding": "El JWT se genera vinculado al ANDROID_ID enviado por el SDK y el servidor exige el mismo X-Device-ID para validarlo.",
      "storage": "EncryptedSharedPreferences protege JWT, perfil y token FCM mediante claves AES-256 administradas por Android Keystore.",
      "revocation": "Los vínculos se revalidan al restaurar la sesión y cada vez que la aplicación vuelve al primer plano. Una revocación confirmada y las respuestas HTTP 401/403 de peticiones protegidas autenticadas eliminan la sesión local y emiten UNAUTHENTICATED. Las respuestas públicas y los fallos transitorios de red nunca cierran la sesión.",
      "advertising_fraud_controls": "El servidor deriva huellas HMAC del dispositivo, red y agente sin almacenar sus valores crudos; limita solicitudes, entregas y confirmaciones por ventanas y devuelve el banner propio de RDF cuando no habilita inventario pago.",
      "error_handling": "Los errores HTTP y del API se traducen a mensajes aptos para el usuario sin exponer detalles internos."
    },
    "data_models": {
      "RDFUserData": ["uuid", "user_name", "avatar_url"],
      "AuthInitResponse": ["success", "session_token", "show_ads", "message"],
      "AuthStatusResponse": ["status", "jwt", "user", "error"],
      "AdResponse": ["success", "show_ad", "ad.banner_url", "ad.target_url", "ad.impression_token", "ad.confirmation_required"],
      "AdImpressionResponse": ["success", "confirmed", "billed", "already_confirmed", "reason"]
    },
    "usage_examples": {
      "compose_login": "RDFLoginScreen(\n    onLoginSuccess = { token ->\n        // El SDK ya conservó el token según la opción elegida.\n        onAuthenticated(token)\n    }\n)",
      "validate_session": "val sessionState by RDFAuth.sessionState.collectAsStateWithLifecycle()\nwhen (sessionState) {\n    RDFSessionState.CHECKING -> MostrarEspera()\n    RDFSessionState.AUTHENTICATED -> MostrarContenido()\n    RDFSessionState.UNAUTHENTICATED -> MostrarLogin()\n}"
    }
  },
  "python": {
    "sdk_info": {
      "name": "Ríos de Fuego Central SDK",
      "platform": "Python",
      "version": "1.2.11",
      "package": "rdf-sdk",
      "module": "rdf_sdk",
      "description": "SDK Python para autenticación web con Ríos de Fuego Central. Incluye cliente HTTP puro, integración opcional con Flask, frontend HTML/CSS/JS y flujo de login por usuario."
    },
    "setup": {
      "static_pypi_repository": "https://www.riosdefuego.com/pypi",
      "dependency": "pip install --index-url https://www.riosdefuego.com/pypi/simple rdf-sdk==1.2.11",
      "dependency_with_extras": "pip install --index-url https://www.riosdefuego.com/pypi/simple --extra-index-url https://pypi.org/simple \"rdf-sdk[flask]==1.2.11\"",
      "requirements": {
        "python": ">=3.9",
        "build_backend": "setuptools.build_meta",
        "runtime_dependencies": [],
        "optional_dependencies": {
          "flask": ["Flask>=2.3"],
          "dev": ["Flask>=2.3", "pytest>=8.0"]
        }
      }
    },
    "core_functions": {
      "initialize": {
        "signature": "RDFAuth.initialize(app_id: str, api_key: str, **kwargs) -> RDFAuth",
        "description": "Configura el SDK con las credenciales de la aplicación.",
        "security": "Mantiene el JWT en memoria dentro de la instancia RDFAuth. En integraciones web, el blueprint Flask puede persistir token y usuario en flask.session.",
        "kwargs": {
          "base_url": "URL base del servidor. Por defecto https://www.riosdefuego.com/. Las URL remotas deben usar HTTPS; HTTP solo se admite para localhost o direcciones loopback de desarrollo.",
          "device_id": "Identificador del dispositivo o servidor web. Si no se informa, usa platform.node().",
          "timeout": "Timeout HTTP en segundos. Por defecto 10.0.",
          "secret_key": "Clave de sesión Flask aceptada por compatibilidad. Se recomienda usar app.secret_key o create_rdf_blueprint(..., secret_key=...)."
        }
      },
      "start_auth": {
        "signature": "RDFAuth.start_auth(user_identifier: str, *, device_info: str | None = None) -> AuthInitResponse",
        "description": "Limpia la sesión local anterior e inicia el flujo por email o usuario. Envía device_id y una descripción legible del servidor/dispositivo."
      },
      "check_status": {
        "signature": "RDFAuth.check_status(session_token: str, *, persist_result: bool = True) -> AuthStatusResponse",
        "description": "Consulta el estado. El cliente puro conserva JWT/usuario al aprobarse; Flask usa persist_result=false para aislar usuarios concurrentes y guarda el resultado en flask.session."
      },
      "get_auth_token": {
        "signature": "RDFAuth.get_auth_token() -> str | None",
        "description": "Retorna el JWT actual si el usuario está autenticado; de lo contrario, retorna None."
      },
      "get_user_data": {
        "signature": "RDFAuth.get_user_data() -> RDFUserData | None",
        "description": "Retorna un objeto con uuid, user_name y avatar_url del usuario autenticado.",
        "persistence": "Los datos se mantienen en memoria en la instancia RDFAuth. El blueprint Flask puede guardarlos en flask.session."
      },
      "report_user": {
        "signature": "RDFAuth.report_user(reason: str, token: str | None = None) -> bool",
        "description": "Envía una denuncia para aumentar los warnings del usuario en la base de datos central.",
        "requirement": "Requiere una sesión activa o un token explícito. Envía Authorization: Bearer {JWT}."
      },
      "validate_session": {
        "signature": "RDFAuth.validate_session(token: str | None = None) -> bool",
        "description": "Verifica si el vínculo entre el usuario y la app sigue activo en el servidor.",
        "behavior": "Si el vínculo del JWT vigente fue revocado o expiró, cierra la sesión local de la instancia. Al validar un token explícito diferente, su fallo nunca elimina la sesión propia."
      },
      "get_ad": {
        "signature": "RDFAuth.get_ad(*, device_id: str | None = None) -> AdResponse",
        "description": "Obtiene el anuncio actual sin consumir saldo. En aplicaciones premium devuelve show_ad=false y ad=None. El blueprint Flask aporta automáticamente una identidad publicitaria aleatoria por sesión para no mezclar usuarios del mismo servidor. target_url usa el redirect controlado de RDF."
      },
      "confirm_ad_view": {
        "signature": "RDFAuth.confirm_ad_view(impression_token: str, *, device_id: str | None = None) -> dict[str, Any]",
        "description": "Confirma idempotentemente una imagen cargada. Los componentes Flask incluidos realizan esta llamada automáticamente."
      },
      "get_linked_users": {
        "signature": "RDFAuth.get_linked_users(token: str | None = None) -> list[RDFUserData]",
        "description": "Obtiene los usuarios vinculados a la aplicación y normaliza sus avatares.",
        "requirement": "Requiere el token explícito o una sesión activa en la instancia."
      },
      "logout": {
        "signature": "RDFAuth.logout() -> None",
        "description": "Cierra la sesión local y limpia el token y perfil de usuario mantenidos por la instancia."
      }
    },
    "ui_components": {
      "create_rdf_blueprint": {
        "type": "Flask Blueprint",
        "signature": "create_rdf_blueprint(auth: RDFAuth, *, name: str = \"rdf_sdk\", url_prefix: str = \"/rdf\", secret_key: str | None = None, login_success_url: str = \"/\", on_login: Callable | None = None)",
        "description": "Blueprint Flask con login, estado, publicidad, usuarios vinculados, validación, denuncia y logout. Protege los POST con CSRF, respeta la persistencia elegida y redirige a login_success_url tras la aprobación."
      },
      "rdf_asset_version": {
        "type": "Python helper",
        "signature": "rdf_asset_version() -> str",
        "description": "Genera una clave de caché a partir de rdf-login.css, rdf-login.js y rdf-launch-ad.js para servir siempre la versión correcta de los assets empaquetados."
      },
      "RDFLoginPage": {
        "type": "HTML template",
        "route": "GET /rdf/login",
        "description": "Pantalla web de login incluida en el paquete Python. Usa los assets rdf-login.css y rdf-login.js. El diálogo de bienvenida y su publicidad solo aparecen después de que la vinculación sea aprobada."
      },
      "RDFLaunchAd": {
        "type": "JavaScript component",
        "asset": "rdf-launch-ad.js",
        "description": "Componente opcional e independiente para páginas autenticadas. Cuenta aperturas en localStorage por clave de aplicación y, con una frecuencia configurable cuyo valor recomendado es cinco, solicita y muestra la publicidad en un diálogo con el saludo 'Que tengas un bonito día'. El diálogo no se cierra automáticamente."
      }
    },
    "internal_security": {
      "headers": "El cliente inyecta X-Device-ID en todas las solicitudes y el Bearer explícito de cada petición protegida, evitando cruces de token entre solicitudes Flask concurrentes.",
      "device_binding": "sdk/auth/init envía device_id además de device_info; el servidor liga el JWT al mismo X-Device-ID.",
      "session": "RDFAuth conserva token y usuario en memoria; Flask guarda rdf_auth_token y rdf_user en su sesión firmada y solo la hace permanente si el usuario lo solicita.",
      "csrf": "Las rutas POST del blueprint exigen X-RDF-CSRF con comparación constante. El token se obtiene desde la página de login o GET /rdf/session.",
      "advertising": "El login muestra su bienvenida publicitaria únicamente después de una vinculación aprobada. El asset opcional rdf-launch-ad.js mantiene en localStorage un contador por aplicación y solicita otro diálogo cada cinco aperturas autenticadas. Obtener el banner no consume saldo: los componentes confirman por POST, una sola vez, cuando la imagen carga dentro del diálogo. El servidor aplica ventanas antifraude por aplicación, dispositivo, red y anuncio usando huellas HMAC, nunca los identificadores crudos.",
      "error_handling": "RDFClientError normaliza errores HTTP/API; Flask devuelve mensajes seguros y conserva los estados contractuales 402, 409 y 500 sin exponer payloads técnicos. Los errores de publicidad o endpoints públicos no eliminan el JWT vigente."
    },
    "flask_routes": {
      "GET /rdf/login": "Renderiza la pantalla web de login.",
      "POST /rdf/auth/start": "Inicia autenticación por identificador de usuario.",
      "POST /rdf/auth/status": "Consulta el estado y persiste jwt/user si fue aprobado. Requiere CSRF.",
      "GET /rdf/session": "Devuelve la sesión RDF actual y el token CSRF.",
      "POST /rdf/ads/current": "Obtiene del servidor el anuncio actual. El componente que la invoca decide cuándo mostrarlo. Requiere CSRF.",
      "POST /rdf/ads/confirm-view": "Confirma una visualización mediante el token opaco recibido con el anuncio. Requiere CSRF y es idempotente.",
      "GET /rdf/users/linked": "Devuelve los usuarios vinculados para la sesión activa.",
      "POST /rdf/session/validate": "Valida si el vínculo sigue activo.",
      "POST /rdf/user/report": "Reporta al usuario autenticado.",
      "POST /rdf/logout": "Cierra la sesión Flask y limpia la instancia RDFAuth."
    },
    "data_models": {
      "RDFUserData": ["uuid", "user_name", "avatar_url"],
      "AuthInitResponse": ["success", "session_token", "show_ads", "message", "expires_in"],
      "AuthStatusResponse": ["success", "status", "jwt", "user", "error"],
      "AdResponse": ["success", "show_ad", "ad.banner_url", "ad.target_url", "ad.impression_token", "ad.confirmation_required"]
    },
    "usage_examples": {
      "flask": "import os\nfrom flask import Flask\nfrom rdf_sdk import RDFAuth\nfrom rdf_sdk.flask_ext import create_rdf_blueprint\n\napp = Flask(__name__)\napp.secret_key = os.environ[\"FLASK_SECRET_KEY\"]\nrdf_auth = RDFAuth.initialize(\n    app_id=os.environ[\"RDF_APP_ID\"],\n    api_key=os.environ[\"RDF_API_KEY\"],\n)\napp.register_blueprint(create_rdf_blueprint(rdf_auth))",
      "client": "import os\nfrom rdf_sdk import RDFAuth\n\nauth = RDFAuth.initialize(\n    os.environ[\"RDF_APP_ID\"],\n    os.environ[\"RDF_API_KEY\"],\n)\nsession = auth.start_auth(\"usuario@example.com\")\nstatus = auth.check_status(session.session_token)\nif status.status == \"approved\":\n    continuar_con_usuario(status.user)",
      "launch_ad_template": "<link rel=\"stylesheet\" href=\"{{ url_for('rdf_sdk.static', filename='rdf-login.css') }}?v={{ rdf_asset_version }}\">\n<div id=\"rdf-launch-ad-root\" data-rdf-ad-endpoint=\"{{ url_for('rdf_sdk.current_ad') }}\" data-rdf-csrf-token=\"{{ csrf_token }}\" data-rdf-app-key=\"YOUR_APP_ID\" data-rdf-frequency=\"5\"></div>\n<script src=\"{{ url_for('rdf_sdk.static', filename='rdf-launch-ad.js') }}?v={{ rdf_asset_version }}\" defer></script>"
    },
    "packaged_assets": [
      "rdf_sdk/frontend/templates/*.html",
      "rdf_sdk/frontend/static/*.css",
      "rdf_sdk/frontend/static/*.js",
      "rdf_sdk/frontend/static/assets/*.png"
    ]
  },
  "release_1_2_12": {
    "released_at": "2026-09-30",
    "contract_version": "1.0.0",
    "android": {
      "coordinate": "riosdefuego.central:rdf-sdk:1.2.12",
      "aar_sha256": "d62fb108a610b8a54d5db19d75296730b7201c7e0d234c88dff033993a57b3d5",
      "changes": [
        "Expone RDFSessionState como StateFlow para interfaces reactivas.",
        "Revalida automáticamente el vínculo al restaurar la sesión y al volver al primer plano.",
        "Cierra inmediatamente la interfaz autenticada cuando el servidor confirma una desvinculación.",
        "Conserva la sesión ante fallos transitorios de conectividad y evita validaciones simultáneas."
      ]
    },
    "migration": [
      "Cambiar la dependencia Android de 1.2.11 a 1.2.12.",
      "Observar RDFAuth.sessionState desde la raíz de navegación.",
      "Mostrar una espera para CHECKING y dirigir UNAUTHENTICATED al login.",
      "El SDK Python permanece en 1.2.11; no requiere cambios."
    ],
    "compatibility": {
      "server": "RDF SDK API Contract 1.0.0",
      "android_min_sdk": 24,
      "previous_supported": "1.2.11"
    }
  },
  "release_1_2_11": {
    "released_at": "2026-09-29",
    "contract_version": "1.0.0",
    "android": {
      "coordinate": "riosdefuego.central:rdf-sdk:1.2.11",
      "changes": [
        "Aísla la sesión frente a errores 401/403 de publicidad y otros endpoints públicos.",
        "Corrige los mensajes de ERR-1001 y ERR-1002.",
        "Conserva confirmación idempotente de impresiones y redirect HTTPS controlado."
      ]
    },
    "python": {
      "package": "rdf-sdk==1.2.11",
      "wheel_sha256": "e927f82f06be63d30aa334449fcda83d5131612b1a33a397adf1cc2e14af87d8",
      "changes": [
        "Exige HTTPS para servidores remotos y permite HTTP solo en loopback.",
        "Aísla la sesión vigente de errores públicos y de tokens explícitos ajenos.",
        "Alinea los estados HTTP del blueprint Flask con el contrato 1.0.0.",
        "Incluye pruebas del cliente puro, Flask, CSRF y publicidad."
      ]
    },
    "migration": {
      "android": [
        "Cambiar la dependencia de 1.2.10 a 1.2.11.",
        "No es necesario modificar RDFAuth.initialize ni RDFLoginScreen.",
        "Eliminar cualquier manejo externo que cierre sesión por un error publicitario."
      ],
      "python": [
        "Cambiar la versión fijada de rdf-sdk a 1.2.11.",
        "Mantener FLASK_SECRET_KEY, RDF_APP_ID y RDF_API_KEY fuera del código fuente.",
        "Si se pasa un token explícito a validate_session, tratar el resultado como propio de esa solicitud y no como estado global.",
        "No usar HTTP para servidores remotos; configurar HTTPS antes de actualizar."
      ]
    },
    "compatibility": {
      "server": "RDF SDK API Contract 1.0.0",
      "android_min_sdk": 24,
      "python": ">=3.9",
      "previous_supported": {
        "android": "1.2.10",
        "python": "1.2.10"
      }
    }
  },
  "webhooks": {
    "version": "1.0",
    "description": "Los eventos enviados por RDF se firman con un secreto exclusivo de la aplicación, disponible en el panel del desarrollador y destinado únicamente al backend receptor.",
    "headers": {
      "X-RDF-Webhook-Id": "Identificador único del evento. Debe almacenarse para rechazar reenvíos.",
      "X-RDF-Webhook-Timestamp": "Marca Unix utilizada para rechazar eventos antiguos; ventana recomendada de 300 segundos.",
      "X-RDF-Webhook-Signature": "v1=<hexadecimal HMAC-SHA256>"
    },
    "signed_message": "timestamp + '.' + event_id + '.' + raw_request_body",
    "verification_order": [
      "Leer el cuerpo HTTP sin modificarlo.",
      "Validar que el timestamp difiera menos de 300 segundos respecto del reloj del servidor.",
      "Calcular HMAC-SHA256 con el secreto de webhook y comparar en tiempo constante.",
      "Rechazar un X-RDF-Webhook-Id procesado anteriormente.",
      "Procesar el evento y registrar su identificador de forma atómica."
    ],
    "php_example": "$raw = file_get_contents('php://input');\n$id = $_SERVER['HTTP_X_RDF_WEBHOOK_ID'] ?? '';\n$timestamp = $_SERVER['HTTP_X_RDF_WEBHOOK_TIMESTAMP'] ?? '';\n$received = $_SERVER['HTTP_X_RDF_WEBHOOK_SIGNATURE'] ?? '';\nif (!ctype_digit($timestamp) || abs(time() - (int)$timestamp) > 300) { http_response_code(401); exit; }\n$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $id . '.' . $raw, getenv('RDF_WEBHOOK_SECRET'));\nif ($id === '' || !hash_equals($expected, $received)) { http_response_code(401); exit; }"
  }
}
