1
- import fetch from "node-fetch " ;
1
+ import { createGitHubError } from "./errors.js " ;
2
2
3
- if ( ! process . env . GITHUB_PERSONAL_ACCESS_TOKEN ) {
4
- console . error ( "GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set" ) ;
5
- process . exit ( 1 ) ;
3
+ type RequestOptions = {
4
+ method ?: string ;
5
+ body ?: unknown ;
6
+ headers ?: Record < string , string > ;
7
+ } ;
8
+
9
+ async function parseResponseBody ( response : Response ) : Promise < unknown > {
10
+ const contentType = response . headers . get ( "content-type" ) ;
11
+ if ( contentType ?. includes ( "application/json" ) ) {
12
+ return response . json ( ) ;
13
+ }
14
+ return response . text ( ) ;
6
15
}
7
16
8
- export const GITHUB_PERSONAL_ACCESS_TOKEN = process . env . GITHUB_PERSONAL_ACCESS_TOKEN ;
17
+ export async function githubRequest (
18
+ url : string ,
19
+ options : RequestOptions = { }
20
+ ) : Promise < unknown > {
21
+ const headers = {
22
+ "Accept" : "application/vnd.github.v3+json" ,
23
+ "Content-Type" : "application/json" ,
24
+ ...options . headers ,
25
+ } ;
9
26
10
- interface GitHubRequestOptions {
11
- method ?: string ;
12
- body ?: any ;
13
- }
27
+ if ( process . env . GITHUB_TOKEN ) {
28
+ headers [ "Authorization" ] = `Bearer ${ process . env . GITHUB_TOKEN } ` ;
29
+ }
14
30
15
- export async function githubRequest ( url : string , options : GitHubRequestOptions = { } ) {
16
31
const response = await fetch ( url , {
17
32
method : options . method || "GET" ,
18
- headers : {
19
- Authorization : `token ${ GITHUB_PERSONAL_ACCESS_TOKEN } ` ,
20
- Accept : "application/vnd.github.v3+json" ,
21
- "User-Agent" : "github-mcp-server" ,
22
- ...( options . body ? { "Content-Type" : "application/json" } : { } ) ,
23
- } ,
24
- ...( options . body ? { body : JSON . stringify ( options . body ) } : { } ) ,
33
+ headers,
34
+ body : options . body ? JSON . stringify ( options . body ) : undefined ,
25
35
} ) ;
26
36
37
+ const responseBody = await parseResponseBody ( response ) ;
38
+
27
39
if ( ! response . ok ) {
28
- throw new Error ( `GitHub API error: ${ response . statusText } ` ) ;
40
+ throw createGitHubError ( response . status , responseBody ) ;
29
41
}
30
42
31
- return response . json ( ) ;
43
+ return responseBody ;
32
44
}
33
45
34
- export function buildUrl ( baseUrl : string , params : Record < string , any > = { } ) {
35
- const url = new URL ( baseUrl ) ;
36
- Object . entries ( params ) . forEach ( ( [ key , value ] ) => {
37
- if ( value !== undefined && value !== null ) {
38
- if ( Array . isArray ( value ) ) {
39
- url . searchParams . append ( key , value . join ( "," ) ) ;
40
- } else {
41
- url . searchParams . append ( key , value . toString ( ) ) ;
42
- }
46
+ export function validateBranchName ( branch : string ) : string {
47
+ const sanitized = branch . trim ( ) ;
48
+ if ( ! sanitized ) {
49
+ throw new Error ( "Branch name cannot be empty" ) ;
50
+ }
51
+ if ( sanitized . includes ( ".." ) ) {
52
+ throw new Error ( "Branch name cannot contain '..'" ) ;
53
+ }
54
+ if ( / [ \s ~ ^ : ? * [ \\ \] ] / . test ( sanitized ) ) {
55
+ throw new Error ( "Branch name contains invalid characters" ) ;
56
+ }
57
+ if ( sanitized . startsWith ( "/" ) || sanitized . endsWith ( "/" ) ) {
58
+ throw new Error ( "Branch name cannot start or end with '/'" ) ;
59
+ }
60
+ if ( sanitized . endsWith ( ".lock" ) ) {
61
+ throw new Error ( "Branch name cannot end with '.lock'" ) ;
62
+ }
63
+ return sanitized ;
64
+ }
65
+
66
+ export function validateRepositoryName ( name : string ) : string {
67
+ const sanitized = name . trim ( ) . toLowerCase ( ) ;
68
+ if ( ! sanitized ) {
69
+ throw new Error ( "Repository name cannot be empty" ) ;
70
+ }
71
+ if ( ! / ^ [ a - z 0 - 9 _ . - ] + $ / . test ( sanitized ) ) {
72
+ throw new Error (
73
+ "Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores"
74
+ ) ;
75
+ }
76
+ if ( sanitized . startsWith ( "." ) || sanitized . endsWith ( "." ) ) {
77
+ throw new Error ( "Repository name cannot start or end with a period" ) ;
78
+ }
79
+ return sanitized ;
80
+ }
81
+
82
+ export function validateOwnerName ( owner : string ) : string {
83
+ const sanitized = owner . trim ( ) . toLowerCase ( ) ;
84
+ if ( ! sanitized ) {
85
+ throw new Error ( "Owner name cannot be empty" ) ;
86
+ }
87
+ if ( ! / ^ [ a - z 0 - 9 ] (?: [ a - z 0 - 9 ] | - (? = [ a - z 0 - 9 ] ) ) { 0 , 38 } $ / . test ( sanitized ) ) {
88
+ throw new Error (
89
+ "Owner name must start with a letter or number and can contain up to 39 characters"
90
+ ) ;
91
+ }
92
+ return sanitized ;
93
+ }
94
+
95
+ export async function checkBranchExists (
96
+ owner : string ,
97
+ repo : string ,
98
+ branch : string
99
+ ) : Promise < boolean > {
100
+ try {
101
+ await githubRequest (
102
+ `https://api.github.com/repos/${ owner } /${ repo } /branches/${ branch } `
103
+ ) ;
104
+ return true ;
105
+ } catch ( error ) {
106
+ if ( error && typeof error === "object" && "status" in error && error . status === 404 ) {
107
+ return false ;
43
108
}
44
- } ) ;
45
- return url . toString ( ) ;
109
+ throw error ;
110
+ }
111
+ }
112
+
113
+ export async function checkUserExists ( username : string ) : Promise < boolean > {
114
+ try {
115
+ await githubRequest ( `https://api.github.com/users/${ username } ` ) ;
116
+ return true ;
117
+ } catch ( error ) {
118
+ if ( error && typeof error === "object" && "status" in error && error . status === 404 ) {
119
+ return false ;
120
+ }
121
+ throw error ;
122
+ }
46
123
}
0 commit comments