rtrim(faceai_env('FACEAI_FRONTEND_URL', 'http://localhost:5173'), '/'), 'backend_internal_url' => rtrim(faceai_env('FACEAI_BACKEND_INTERNAL_URL', 'http://localhost:3001'), '/'), 'shared_secret' => (string) faceai_env('FACEAI_SHARED_SECRET', 'change-me'), 'allow_dev_handoff' => faceai_env('FACEAI_ALLOW_DEV_HANDOFF', '1') === '1', 'identity_cookie' => (string) faceai_env('FACEAI_IDENTITY_COOKIE', 'rus_faceai_identity'), 'return_forward_url' => rtrim((string) faceai_env('FACEAI_RETURN_FORWARD_URL', ''), '/') ); return $config; } function faceai_base64url_encode($value) { return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); } function faceai_base64url_decode($value) { $padding = strlen($value) % 4; if ($padding > 0) { $value .= str_repeat('=', 4 - $padding); } return base64_decode(strtr($value, '-_', '+/')); } function faceai_sign_payload(array $payload, $secret) { $body = faceai_base64url_encode(json_encode($payload)); $signature = hash_hmac('sha256', $body, $secret, true); return $body . '.' . faceai_base64url_encode($signature); } function faceai_verify_payload($token, $secret) { if (!is_string($token) || strpos($token, '.') === false) { throw new RuntimeException('Invalid token format.'); } list($body, $signature) = explode('.', $token, 2); $expected = faceai_base64url_encode(hash_hmac('sha256', $body, $secret, true)); if (!hash_equals($expected, $signature)) { throw new RuntimeException('Invalid token signature.'); } $decoded = faceai_base64url_decode($body); $payload = json_decode($decoded, true); if (!is_array($payload)) { throw new RuntimeException('Invalid token payload.'); } if (isset($payload['expiresAt']) && (int) $payload['expiresAt'] < (int) round(microtime(true) * 1000)) { throw new RuntimeException('Token expired.'); } return $payload; } function faceai_build_url($baseUrl, array $params) { return $baseUrl . (strpos($baseUrl, '?') === false ? '?' : '&') . http_build_query($params); } function faceai_request_value($key, $default = '') { if (!isset($_GET[$key])) { return $default; } if (is_array($_GET[$key])) { return $default; } return trim((string) $_GET[$key]); } function faceai_html($value) { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } function faceai_resolve_identity(array $config) { if (!empty($_COOKIE[$config['identity_cookie']])) { $payload = faceai_verify_payload($_COOKIE[$config['identity_cookie']], $config['shared_secret']); if (($payload['type'] ?? '') !== 'legacy-identity') { throw new RuntimeException('Unexpected identity cookie payload.'); } return array( 'id' => (string) ($payload['userId'] ?? ''), 'displayName' => (string) ($payload['displayName'] ?? ''), 'email' => (string) ($payload['email'] ?? ''), 'membershipStatus' => (string) ($payload['membershipStatus'] ?? 'inactive') ); } if ($config['allow_dev_handoff']) { $userId = faceai_request_value('devUserId'); if ($userId !== '') { return array( 'id' => $userId, 'displayName' => faceai_request_value('devDisplayName', 'Local Test User'), 'email' => faceai_request_value('devEmail', 'local.test@example.invalid'), 'membershipStatus' => faceai_request_value('devMembershipStatus', 'active') ); } } return null; } function faceai_render_message_page($title, $message, array $details = array(), $statusCode = 400) { http_response_code($statusCode); header('Content-Type: text/html; charset=UTF-8'); echo ''; echo '' . faceai_html($title) . ''; echo ''; echo '
'; echo '

' . faceai_html($title) . '

'; echo '

' . faceai_html($message) . '

'; if (!empty($details)) { echo ''; } echo '
'; exit; } function faceai_fetch_json($url) { $context = stream_context_create(array( 'http' => array( 'ignore_errors' => true, 'timeout' => 10 ) )); $response = @file_get_contents($url, false, $context); if ($response === false) { throw new RuntimeException('Unable to fetch remote FaceAI data.'); } $statusCode = 0; if (!empty($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $matches)) { $statusCode = (int) $matches[1]; } $payload = json_decode($response, true); if (!is_array($payload)) { throw new RuntimeException('FaceAI returned invalid JSON.'); } if ($statusCode >= 400) { throw new RuntimeException($payload['error'] ?? ('FaceAI bridge request failed with status ' . $statusCode . '.')); } return $payload; }