@@ -15,6 +15,64 @@ function base64url(str: string): string {
1515 . replace ( / \/ / g, '_' ) ;
1616}
1717
18+ const PRIVATE_KEY_BEGIN = / - - - - - B E G I N [ A - Z ] * P R I V A T E K E Y - - - - - / ;
19+ const PRIVATE_KEY_END = / - - - - - E N D [ A - Z ] * P R I V A T E K E Y - - - - - / ;
20+
21+ function stripWrappingQuotes ( value : string ) : string {
22+ if ( value . length < 2 ) return value ;
23+
24+ const first = value [ 0 ] ;
25+ const last = value [ value . length - 1 ] ;
26+
27+ if ( ( first === '"' && last === '"' ) || ( first === "'" && last === "'" ) ) {
28+ return value . slice ( 1 , - 1 ) ;
29+ }
30+
31+ return value ;
32+ }
33+
34+ function decodeBase64Pem ( value : string ) : string | null {
35+ const compact = value . replace ( / \s / g, '' ) ;
36+ if ( ! compact || compact . length % 4 !== 0 ) return null ;
37+
38+ try {
39+ const decoded = Buffer . from ( compact , 'base64' ) . toString ( 'utf8' ) . trim ( ) ;
40+ return PRIVATE_KEY_BEGIN . test ( decoded ) && PRIVATE_KEY_END . test ( decoded )
41+ ? decoded
42+ : null ;
43+ } catch {
44+ return null ;
45+ }
46+ }
47+
48+ export function normalizeGitHubAppPrivateKey ( privateKey : string ) : string {
49+ let key = stripWrappingQuotes ( privateKey . trim ( ) )
50+ . replace ( / \\ r \\ n / g, '\n' )
51+ . replace ( / \\ n / g, '\n' )
52+ . replace ( / \\ r / g, '\n' )
53+ . replace ( / \r \n / g, '\n' )
54+ . replace ( / \r / g, '\n' )
55+ . trim ( ) ;
56+
57+ if ( ! PRIVATE_KEY_BEGIN . test ( key ) ) {
58+ key = decodeBase64Pem ( key ) ?? key ;
59+ }
60+
61+ if ( / ^ \/ .* \. p e m $ / i. test ( key ) || / ^ [ A - Z ] : \\ .* \. p e m $ / i. test ( key ) ) {
62+ throw new Error (
63+ 'GITHUB_PRIVATE_KEY contains a file path. Configure the deployed environment with the PEM contents or a base64-encoded PEM, not a local .pem path.'
64+ ) ;
65+ }
66+
67+ if ( ! PRIVATE_KEY_BEGIN . test ( key ) || ! PRIVATE_KEY_END . test ( key ) ) {
68+ throw new Error (
69+ 'GITHUB_PRIVATE_KEY must be an RSA private key PEM. Expected BEGIN/END PRIVATE KEY markers after env normalization.'
70+ ) ;
71+ }
72+
73+ return key ;
74+ }
75+
1876export function generateGitHubAppJWT ( appId : string , privateKey : string ) : string {
1977 // Current time in seconds
2078 const now = Math . floor ( Date . now ( ) / 1000 ) ;
@@ -39,10 +97,7 @@ export function generateGitHubAppJWT(appId: string, privateKey: string): string
3997 // Create signature
4098 const signatureInput = `${ encodedHeader } .${ encodedPayload } ` ;
4199
42- // Parse the private key (handle both raw and escaped formats)
43- const privateKeyContent = privateKey
44- . replace ( / \\ n / g, '\n' )
45- . trim ( ) ;
100+ const privateKeyContent = normalizeGitHubAppPrivateKey ( privateKey ) ;
46101
47102 const sign = crypto . createSign ( 'RSA-SHA256' ) ;
48103 sign . update ( signatureInput ) ;
@@ -57,4 +112,4 @@ export function generateGitHubAppJWT(appId: string, privateKey: string): string
57112
58113 // Combine all parts
59114 return `${ encodedHeader } .${ encodedPayload } .${ encodedSignature } ` ;
60- }
115+ }
0 commit comments