5.00, 'residential' => 2.50, 'seo' => 0.00, // per-request, not per-GB ]; public function __construct( private readonly NodeSelector $selector, private readonly UsageMeter $meter, private readonly GatewayRegistry $registry, ) {} /** * POST /v1/proxy/connect {"type": "residential"} * * Returns a gateway node to connect to for the requested service type. * Requires API key (Bearer token from Blesta service). */ public function connect(Request $request): JsonResponse { $type = strtolower(trim((string) $request->input('type', 'proxy'))); $region = strtolower(trim((string) $request->input('region', ''))); // Map service types to node capabilities $capMap = [ 'mobile' => 'proxy', 'residential' => 'proxy', 'seo' => 'exit', 'vpn' => 'vpn', 'proxy' => 'proxy', 'dns' => 'dns', ]; $capability = $capMap[$type] ?? 'proxy'; $source = 'chain'; // Prefer live paired gateways (real-time stats, verified endpoints) $liveGateway = $this->registry->selectBest($capability); if ($liveGateway) { $source = 'paired'; $apiKey = $request->bearerToken() ?? 'anonymous'; $this->meter->recordRequest($apiKey); return response()->json([ 'node' => $liveGateway['name'] . '.lthn', 'type' => $type, 'capabilities' => $liveGateway['capabilities'] ?? [], 'region' => $liveGateway['region'] ?? 'unknown', 'wireguard_endpoint' => $liveGateway['wireguard_endpoint'] ?? '', 'proxy_endpoint' => $liveGateway['proxy_endpoint'] ?? '', 'load' => $liveGateway['current_load'] ?? 0, 'source' => $source, 'pricing' => [ 'model' => $type === 'seo' ? 'per-request' : 'per-gb', 'rate_usd' => self::PRICING[$type] ?? 0, 'currency' => 'LTHN', ], 'session' => bin2hex(random_bytes(8)), ]); } // Fall back to chain-discovered nodes $node = $this->selector->selectNode($capability); if (! $node) { return response()->json([ 'error' => 'No nodes available for this service type.', 'type' => $type, ], 503); } $apiKey = $request->bearerToken() ?? 'anonymous'; $this->meter->recordRequest($apiKey); $comment = $node['comment'] ?? ''; $caps = []; if (preg_match('/cap=([^;]+)/', $comment, $m)) { $caps = explode(',', $m[1]); } return response()->json([ 'node' => $node['alias'] . '.lthn', 'address' => $node['address'] ?? '', 'type' => $type, 'capabilities' => $caps, 'source' => $source, 'pricing' => [ 'model' => $type === 'seo' ? 'per-request' : 'per-gb', 'rate_usd' => self::PRICING[$type] ?? 0, 'currency' => 'LTHN', ], 'session' => bin2hex(random_bytes(8)), ]); } /** * GET /v1/proxy/usage * * Returns usage for the authenticated API key. */ public function usage(Request $request): JsonResponse { $apiKey = $request->bearerToken() ?? 'anonymous'; $usage = $this->meter->getUsage($apiKey); return response()->json([ 'api_key' => substr($apiKey, 0, 8) . '...', 'usage' => $usage, 'billing' => [ 'mobile_cost' => round($usage['gb'] * self::PRICING['mobile'], 2), 'residential_cost' => round($usage['gb'] * self::PRICING['residential'], 2), 'seo_requests' => $usage['requests'], ], ]); } /** * GET /v1/proxy/billing/{apiKey} * * Billing data for a customer's API key — Blesta queries this via cron. * Returns usage, cost per tier, and whether to invoice. */ public function billing(string $apiKey): JsonResponse { $usage = $this->meter->getUsage($apiKey); $gb = $usage['gb']; $requests = $usage['requests']; return response()->json([ 'api_key' => $apiKey, 'period' => [ 'start' => now()->startOfMonth()->toIso8601String(), 'end' => now()->toIso8601String(), ], 'usage' => $usage, 'charges' => [ 'mobile_proxy' => [ 'gb' => $gb, 'rate' => self::PRICING['mobile'], 'total' => round($gb * self::PRICING['mobile'], 2), ], 'residential_proxy' => [ 'gb' => $gb, 'rate' => self::PRICING['residential'], 'total' => round($gb * self::PRICING['residential'], 2), ], 'seo_traffic' => [ 'requests' => $requests, 'rate_per_1k' => 1.00, 'total' => round($requests / 1000 * 1.00, 2), ], ], 'currency' => 'USD', ]); } /** * GET /v1/proxy/nodes?cap=proxy®ion=eu * * List available nodes by capability. */ public function nodes(Request $request): JsonResponse { $capability = strtolower(trim((string) $request->get('cap', ''))); $nodes = $this->selector->availableNodes($capability); $result = []; foreach ($nodes as $node) { $caps = []; if (preg_match('/cap=([^;]+)/', $node['comment'] ?? '', $m)) { $caps = explode(',', $m[1]); } $result[] = [ 'name' => $node['alias'] . '.lthn', 'capabilities' => $caps, 'status' => 'online', ]; } return response()->json([ 'nodes' => $result, 'count' => count($result), 'filter' => $capability ?: 'all', ]); } /** * GET /v1/proxy/status * * Network availability summary. */ public function status(): JsonResponse { $vpn = $this->selector->availableNodes('vpn'); $proxy = $this->selector->availableNodes('proxy'); $exit = $this->selector->availableNodes('exit'); $dns = $this->selector->availableNodes('dns'); return response()->json([ 'status' => 'operational', 'availability' => [ 'vpn' => ['nodes' => count($vpn), 'status' => count($vpn) > 0 ? 'available' : 'unavailable'], 'proxy' => ['nodes' => count($proxy), 'status' => count($proxy) > 0 ? 'available' : 'unavailable'], 'exit' => ['nodes' => count($exit), 'status' => count($exit) > 0 ? 'available' : 'unavailable'], 'dns' => ['nodes' => count($dns), 'status' => count($dns) > 0 ? 'available' : 'unavailable'], ], 'services' => [ 'mobile_proxy' => ['available' => count($proxy) > 0, 'rate' => '$5.00/GB'], 'residential_proxy' => ['available' => count($proxy) > 0, 'rate' => '$2.50/GB'], 'seo_traffic' => ['available' => count($exit) > 0, 'rate' => 'per-request'], ], ]); } }