'application/json', 'apikey' => aivastark_widget_anon_key(), ); if ( $bearer ) { $headers['Authorization'] = 'Bearer ' . $bearer; } $res = wp_remote_post( aivastark_widget_functions_url() . '/' . $fn, array( 'timeout' => 15, 'headers' => $headers, 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $res ) ) { return $res; } $code = (int) wp_remote_retrieve_response_code( $res ); $data = json_decode( wp_remote_retrieve_body( $res ), true ); if ( ! is_array( $data ) ) { return new WP_Error( 'aivastark_bad_response', 'Unexpected response from Aivastark.' ); } if ( $code >= 400 || empty( $data['ok'] ) ) { $err = isset( $data['error'] ) ? $data['error'] : ( 'http_' . $code ); return new WP_Error( 'aivastark_api_error', (string) $err, array( 'status' => $code, 'data' => $data ) ); } return $data; } /** * Exchange an authorization code + PKCE verifier for a token pair. */ public static function exchange( $code, $code_verifier, $site_url ) { return self::post( 'wordpress-token', array( 'action' => 'exchange', 'code' => $code, 'code_verifier' => $code_verifier, 'site_url' => $site_url, ) ); } /** * Rotate the token pair using the stored refresh token. Persists the new * pair on success. */ public static function refresh() { $tokens = Aivastark_Widget_Options::get_tokens(); if ( empty( $tokens['refresh_token'] ) ) { return new WP_Error( 'aivastark_no_refresh', 'Not connected.' ); } $data = self::post( 'wordpress-token', array( 'action' => 'refresh', 'refresh_token' => $tokens['refresh_token'], ) ); if ( ! is_wp_error( $data ) ) { Aivastark_Widget_Options::update_tokens_from_refresh( $data ); } return $data; } /** * Revoke the current install (disconnect). */ public static function revoke() { $tokens = Aivastark_Widget_Options::get_tokens(); if ( empty( $tokens['access_token'] ) ) { return true; } return self::post( 'wordpress-token', array( 'action' => 'revoke', 'access_token' => $tokens['access_token'], ) ); } /** * Ensure a non-expired access token, refreshing first if it's within the * buffer. Returns the usable access token string, or a WP_Error. */ public static function ensure_fresh_token() { if ( ! Aivastark_Widget_Options::is_connected() ) { return new WP_Error( 'aivastark_not_connected', 'Not connected.' ); } $expires_at = (int) Aivastark_Widget_Options::get( 'token_expires_at', 0 ); if ( $expires_at && ( $expires_at - time() ) <= self::REFRESH_BUFFER ) { $refreshed = self::refresh(); if ( is_wp_error( $refreshed ) ) { return $refreshed; } } $tokens = Aivastark_Widget_Options::get_tokens(); return $tokens['access_token']; } /** * Mint a one-time SSO ticket (magic-link token_hash) for the embedded * customization iframe. Refreshes the access token first when near expiry, * and retries once if the backend reports the token just expired. * * @return array|WP_Error { token_hash, project_id } */ public static function sso_ticket() { $access = self::ensure_fresh_token(); if ( is_wp_error( $access ) ) { return $access; } $data = self::post( 'wordpress-sso', array(), $access ); // Race: token expired between the freshness check and the call → rotate // once and retry. if ( is_wp_error( $data ) && 'token_expired' === $data->get_error_message() ) { $refreshed = self::refresh(); if ( is_wp_error( $refreshed ) ) { return $refreshed; } $tokens = Aivastark_Widget_Options::get_tokens(); $data = self::post( 'wordpress-sso', array(), $tokens['access_token'] ); } return $data; } }