All files / src/cli/init connection.ts

91.66% Statements 44/48
83.87% Branches 26/31
80% Functions 4/5
93.61% Lines 44/47

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159                  3x         11x 11x     11x 11x   11x   11x               10x 3x 1x         2x 1x         1x           7x               7x 7x               6x 5x 5x           7x                 1x             1x             1x         11x             3x 8x 1x       7x 2x     5x 5x 4x     4x   1x                   3x 13x 13x 11x   11x   2x               3x 5x   5x 5x    
/**
 * Connection testing for init wizard
 */
 
import { ConnectionTestResult } from "./types";
 
/**
 * Test GitLab connection with provided credentials
 */
export async function testConnection(
  instanceUrl: string,
  token: string
): Promise<ConnectionTestResult> {
  // Normalize URL: strip trailing slash and /api/v4 suffix if present
  const baseUrl = instanceUrl.replace(/\/$/, "").replace(/\/api\/v4\/?$/, "");
  const apiUrl = `${baseUrl}/api/v4`;
 
  // 10 second timeout for connection test
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10000);
 
  try {
    // Test /user endpoint to verify token
    const userResponse = await fetch(`${apiUrl}/user`, {
      headers: {
        "PRIVATE-TOKEN": token,
        Accept: "application/json",
      },
      signal: controller.signal,
    });
 
    if (!userResponse.ok) {
      if (userResponse.status === 401) {
        return {
          success: false,
          error: "Invalid token - authentication failed",
        };
      }
      if (userResponse.status === 403) {
        return {
          success: false,
          error: "Token lacks required permissions",
        };
      }
      return {
        success: false,
        error: `GitLab API error: ${userResponse.status} ${userResponse.statusText}`,
      };
    }
 
    const userData = (await userResponse.json()) as {
      username?: string;
      email?: string;
      is_admin?: boolean;
    };
 
    // Get GitLab version (with same timeout)
    let gitlabVersion: string | undefined;
    try {
      const versionResponse = await fetch(`${apiUrl}/version`, {
        headers: {
          "PRIVATE-TOKEN": token,
          Accept: "application/json",
        },
        signal: controller.signal,
      });
 
      if (versionResponse.ok) {
        const versionData = (await versionResponse.json()) as { version?: string };
        gitlabVersion = versionData.version;
      }
    } catch {
      // Version endpoint may not be available, continue without it
    }
 
    return {
      success: true,
      username: userData.username,
      email: userData.email,
      isAdmin: userData.is_admin,
      gitlabVersion,
    };
  } catch (error) {
    // Handle timeout
    Iif (error instanceof Error && error.name === "AbortError") {
      return {
        success: false,
        error: `Connection timeout - ${instanceUrl} did not respond within 10 seconds`,
      };
    }
    // Handle network errors
    Iif (error instanceof TypeError && error.message.includes("fetch")) {
      return {
        success: false,
        error: `Cannot connect to ${instanceUrl} - check URL and network`,
      };
    }
 
    return {
      success: false,
      error: error instanceof Error ? error.message : String(error),
    };
  } finally {
    clearTimeout(timeoutId);
  }
}
 
/**
 * Validate GitLab URL format
 */
export function validateGitLabUrl(url: string): { valid: boolean; error?: string } {
  if (!url) {
    return { valid: false, error: "URL is required" };
  }
 
  // Must start with https:// or http://
  if (!url.startsWith("https://") && !url.startsWith("http://")) {
    return { valid: false, error: "URL must start with https:// or http://" };
  }
 
  try {
    const parsed = new URL(url);
    Iif (!parsed.hostname) {
      return { valid: false, error: "Invalid URL hostname" };
    }
    return { valid: true };
  } catch {
    return { valid: false, error: "Invalid URL format" };
  }
}
 
/**
 * Check if URL is GitLab SaaS (gitlab.com)
 * Uses strict hostname matching to avoid false positives from URLs like:
 * - notgitlab.com (contains "gitlab.com" as substring)
 * - gitlab.company.com (contains "gitlab.com" as substring)
 */
export function isGitLabSaas(url: string): boolean {
  try {
    const parsed = new URL(url);
    const hostname = parsed.hostname.toLowerCase();
    // Strict match: exactly gitlab.com or subdomain of gitlab.com
    return hostname === "gitlab.com" || hostname.endsWith(".gitlab.com");
  } catch {
    return false;
  }
}
 
/**
 * Generate PAT creation URL for GitLab instance
 * Uses least-privilege scopes based on read-only mode
 */
export function getPatCreationUrl(instanceUrl: string, readOnly = false): string {
  const baseUrl = instanceUrl.replace(/\/$/, "");
  // Use minimal scopes for read-only mode, full api for write access
  const scopes = readOnly ? "read_api,read_user" : "api,read_user";
  return `${baseUrl}/-/user_settings/personal_access_tokens?name=gitlab-mcp&scopes=${scopes}`;
}