app/lib/server/githubTokenService.ts (146 lines of code) (raw):
// GitHub token service for creating ephemeral access tokens
export class GitHubTokenService {
private static readonly GITHUB_API_BASE = "https://api.github.com";
/**
* Create an ephemeral token for repository access
* This creates a fine-grained personal access token that can be used by containers
* to clone private repositories
*/
static async createEphemeralToken(
accessToken: string,
repositoryUrl: string,
expirationMinutes: number = 60
): Promise<{ token: string; expiresAt: Date } | null> {
try {
// Extract repository owner and name from URL
const repoMatch = repositoryUrl.match(
/github\.com[\/:]([^\/]+)\/([^\/\.]+)/
);
if (!repoMatch) {
console.error("Invalid GitHub repository URL:", repositoryUrl);
return null;
}
const [, owner, repo] = repoMatch;
console.log(`Creating ephemeral token for ${owner}/${repo}`);
// Calculate expiration time
const expiresAt = new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + expirationMinutes);
// Create a fine-grained personal access token
// Note: This requires the user to have authorized the app with appropriate scopes
const tokenResponse = await fetch(
`${this.GITHUB_API_BASE}/user/installations`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
if (!tokenResponse.ok) {
console.warn("Could not create fine-grained token, using main token");
// Fallback: return the main access token with a warning
return {
token: accessToken,
expiresAt,
};
}
// For now, we'll use the main access token as GitHub's fine-grained
// personal access tokens through API are still in beta
// In a production environment, you might want to implement installation tokens
// or use GitHub Apps for better security
console.log(
`Using main access token as ephemeral token (expires in ${expirationMinutes} minutes)`
);
return {
token: accessToken,
expiresAt,
};
} catch (error) {
console.error("Error creating ephemeral GitHub token:", error);
return null;
}
}
/**
* Validate that a repository is accessible with the given token
*/
static async validateRepositoryAccess(
token: string,
repositoryUrl: string
): Promise<{ canAccess: boolean; isPrivate: boolean }> {
try {
const repoMatch = repositoryUrl.match(
/github\.com[\/:]([^\/]+)\/([^\/\.]+)/
);
if (!repoMatch) {
return { canAccess: false, isPrivate: false };
}
const [, owner, repo] = repoMatch;
const response = await fetch(
`${this.GITHUB_API_BASE}/repos/${owner}/${repo}`,
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
if (response.ok) {
const repoData = await response.json();
return {
canAccess: true,
isPrivate: repoData.private || false,
};
} else if (response.status === 404) {
// Could be private repo without access, or non-existent repo
return { canAccess: false, isPrivate: true }; // Assume private if not found
} else {
return { canAccess: false, isPrivate: false };
}
} catch (error) {
console.error("Error validating repository access:", error);
return { canAccess: false, isPrivate: false };
}
}
/**
* Get repository information including clone URLs
*/
static async getRepositoryInfo(
token: string,
repositoryUrl: string
): Promise<{
clone_url: string;
ssh_url: string;
private: boolean;
full_name: string;
} | null> {
try {
const repoMatch = repositoryUrl.match(
/github\.com[\/:]([^\/]+)\/([^\/\.]+)/
);
if (!repoMatch) {
return null;
}
const [, owner, repo] = repoMatch;
const response = await fetch(
`${this.GITHUB_API_BASE}/repos/${owner}/${repo}`,
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
if (response.ok) {
const repoData = await response.json();
return {
clone_url: repoData.clone_url,
ssh_url: repoData.ssh_url,
private: repoData.private,
full_name: repoData.full_name,
};
}
return null;
} catch (error) {
console.error("Error getting repository info:", error);
return null;
}
}
/**
* Create a GitHub clone URL with embedded token for HTTPS cloning
*/
static createAuthenticatedCloneUrl(
token: string,
repositoryUrl: string
): string {
try {
const url = new URL(repositoryUrl);
if (url.hostname === "github.com") {
// Format: https://TOKEN@github.com/owner/repo.git
return `https://${token}@github.com${url.pathname}${
url.pathname.endsWith(".git") ? "" : ".git"
}`;
}
return repositoryUrl;
} catch (error) {
console.error("Error creating authenticated clone URL:", error);
return repositoryUrl;
}
}
}