Ir al contenido principal
Laravel, thinking fast.

El arreglo es un backend-for-frontend: un endpoint pequeño en el servicio que es dueño de los hechos, al que el navegador habla en su lugar.

Tiene exactamente cuatro tareas:

/**
 * Backend-for-frontend for the inline "AI rewrite" button on live sites.
 *
 * This proxy moves that hop server-side:
 *   1. authenticate the short-lived, per-site editing token (this service
 *      owns that table — the AI service never could);
 *   2. cap cost per site (burst on the route, daily ceiling here);
 *   3. compose the prompt server-side;
 *   4. call the AI service with the workspace credential in the header,
 *      never exposing it to the browser.
 */

Fíjate en lo que no es. No es un reenvío que pasa el cuerpo tal cual. Un proxy que retransmite lo que le den ha movido la credencial y ha conservado todos los demás problemas. Cada una de esas cuatro tareas es una decisión que el navegador ya no toma.

El coste es un salto y un controlador. Compáralo con las cuatro cosas de la sección anterior y no está reñido.

Las credenciales van en cabeceras, nunca en URLs

El token pasó de la cadena de consulta a la cabecera Authorization, y eso merece su propia sección porque es un hábito, no un caso aislado.

Una URL no es un canal privado. Acaba en los registros de acceso del servidor web, en el historial y el autocompletado del navegador, en la cabecera Referer que se envía a terceros cuando la página carga un recurso externo, en la analítica, en los informes de error y en el mensaje de chat donde alguien pega un enlace para enseñarle el fallo a un compañero.

Una cabecera no acaba en ninguno de esos sitios por defecto.

// The editing token is sent in the Authorization header (not the URL),
// so it stays out of logs, history and Referer leakage.
$tokenString = $request->bearerToken();

if (! is_string($tokenString) || $tokenString === '') {
    return new ErrorResponse('Missing editing token.', Response::HTTP_UNAUTHORIZED);
}

La misma regla vale para cualquier otra cosa identificativa: nunca pongas un token, un identificador de sesión ni una carga firmada en una ruta o cadena de consulta que un navegador vaya a mostrar, recordar o reenviar.

Una guarda, una respuesta

La búsqueda es pequeña, y cada detalle es deliberado:

private function resolveSite(?string $tokenString): Site|ErrorResponse
{
    if (! is_string($tokenString) || $tokenString === '') {
        return new ErrorResponse('Missing editing token.', Response::HTTP_UNAUTHORIZED);
    }

    $token = Token::query()
        ->with('site.server.workspace')
        ->where('id', $tokenString)
        ->first();

    if (! $token || ! $token->expires_at || ! $token->expires_at->isFuture()) {
        return new ErrorResponse('Invalid or expired editing token.', Response::HTTP_UNAUTHORIZED);
    }

    $site = $token->site;

    if ($site === null) {
        return new ErrorResponse('Invalid or expired editing token.', Response::HTTP_UNAUTHORIZED);
    }

    return $site;
}

La caducidad se comprueba explícitamente, y una caducidad ausente falla. ! $token->expires_at no es ruido defensivo: una fila sin caducidad sería una credencial permanente, que es lo contrario de para lo que sirve un token efímero. La ausencia de límite se trata como inválida, no como ilimitada.

Tres fallos distintos dan un mensaje idéntico. Token desconocido, token caducado, sitio desvinculado: todos «Invalid or expired editing token.» Distinguirlos le diría a un atacante qué tokens existen —un oráculo pequeño, y gratis de evitar.

Devuelve una unión, no una excepción ni null. Site|ErrorResponse significa que el llamante no puede olvidarse de manejar el fallo; el sistema de tipos hace que el camino de error sea tan real como el de éxito.

Las relaciones se cargan de golpe en la guarda. with('site.server.workspace') porque lo siguiente que necesita todo llamante es la credencial del espacio de trabajo. Autenticar y luego recorrer tres relaciones de forma perezosa es cómo una petición se convierte en cuatro consultas.