@flink-app/github-app-plugin 0.12.1-alpha.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/LICENSE +21 -0
- package/README.md +667 -0
- package/SECURITY.md +498 -0
- package/dist/GitHubAppInternalContext.d.ts +44 -0
- package/dist/GitHubAppInternalContext.js +2 -0
- package/dist/GitHubAppPlugin.d.ts +45 -0
- package/dist/GitHubAppPlugin.js +367 -0
- package/dist/GitHubAppPluginContext.d.ts +242 -0
- package/dist/GitHubAppPluginContext.js +2 -0
- package/dist/GitHubAppPluginOptions.d.ts +369 -0
- package/dist/GitHubAppPluginOptions.js +2 -0
- package/dist/handlers/InitiateInstallation.d.ts +32 -0
- package/dist/handlers/InitiateInstallation.js +66 -0
- package/dist/handlers/InstallationCallback.d.ts +42 -0
- package/dist/handlers/InstallationCallback.js +248 -0
- package/dist/handlers/UninstallHandler.d.ts +37 -0
- package/dist/handlers/UninstallHandler.js +153 -0
- package/dist/handlers/WebhookHandler.d.ts +54 -0
- package/dist/handlers/WebhookHandler.js +157 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +23 -0
- package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
- package/dist/repos/GitHubAppSessionRepo.js +32 -0
- package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
- package/dist/repos/GitHubInstallationRepo.js +83 -0
- package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
- package/dist/repos/GitHubWebhookEventRepo.js +42 -0
- package/dist/schemas/GitHubAppSession.d.ts +13 -0
- package/dist/schemas/GitHubAppSession.js +2 -0
- package/dist/schemas/GitHubInstallation.d.ts +28 -0
- package/dist/schemas/GitHubInstallation.js +2 -0
- package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
- package/dist/schemas/InstallationCallbackRequest.js +2 -0
- package/dist/schemas/WebhookEvent.d.ts +16 -0
- package/dist/schemas/WebhookEvent.js +2 -0
- package/dist/schemas/WebhookPayload.d.ts +35 -0
- package/dist/schemas/WebhookPayload.js +2 -0
- package/dist/services/GitHubAPIClient.d.ts +143 -0
- package/dist/services/GitHubAPIClient.js +167 -0
- package/dist/services/GitHubAuthService.d.ts +85 -0
- package/dist/services/GitHubAuthService.js +160 -0
- package/dist/services/WebhookValidator.d.ts +93 -0
- package/dist/services/WebhookValidator.js +123 -0
- package/dist/utils/error-utils.d.ts +67 -0
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/jwt-utils.d.ts +35 -0
- package/dist/utils/jwt-utils.js +67 -0
- package/dist/utils/state-utils.d.ts +38 -0
- package/dist/utils/state-utils.js +74 -0
- package/dist/utils/token-cache-utils.d.ts +47 -0
- package/dist/utils/token-cache-utils.js +74 -0
- package/dist/utils/webhook-signature-utils.d.ts +22 -0
- package/dist/utils/webhook-signature-utils.js +57 -0
- package/examples/basic-installation.ts +246 -0
- package/examples/create-issue.ts +392 -0
- package/examples/error-handling.ts +396 -0
- package/examples/multi-event-webhook.ts +367 -0
- package/examples/organization-installation.ts +316 -0
- package/examples/repository-access.ts +480 -0
- package/examples/webhook-handling.ts +343 -0
- package/examples/with-jwt-auth.ts +319 -0
- package/package.json +41 -0
- package/spec/core-utilities.spec.ts +243 -0
- package/spec/handlers.spec.ts +216 -0
- package/spec/helpers/reporter.ts +41 -0
- package/spec/integration-and-security.spec.ts +483 -0
- package/spec/plugin-core.spec.ts +258 -0
- package/spec/project-setup.spec.ts +56 -0
- package/spec/repos-and-schemas.spec.ts +288 -0
- package/spec/services.spec.ts +108 -0
- package/spec/support/jasmine.json +7 -0
- package/src/GitHubAppPlugin.ts +411 -0
- package/src/GitHubAppPluginContext.ts +254 -0
- package/src/GitHubAppPluginOptions.ts +412 -0
- package/src/handlers/InstallationCallback.ts +292 -0
- package/src/handlers/WebhookHandler.ts +179 -0
- package/src/index.ts +29 -0
- package/src/repos/GitHubAppSessionRepo.ts +36 -0
- package/src/repos/GitHubInstallationRepo.ts +95 -0
- package/src/repos/GitHubWebhookEventRepo.ts +48 -0
- package/src/schemas/GitHubAppSession.ts +13 -0
- package/src/schemas/GitHubInstallation.ts +28 -0
- package/src/schemas/InstallationCallbackRequest.ts +10 -0
- package/src/schemas/WebhookEvent.ts +16 -0
- package/src/schemas/WebhookPayload.ts +35 -0
- package/src/services/GitHubAPIClient.ts +244 -0
- package/src/services/GitHubAuthService.ts +188 -0
- package/src/services/WebhookValidator.ts +159 -0
- package/src/utils/error-utils.ts +148 -0
- package/src/utils/jwt-utils.ts +64 -0
- package/src/utils/state-utils.ts +72 -0
- package/src/utils/token-cache-utils.ts +89 -0
- package/src/utils/webhook-signature-utils.ts +57 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub API Client
|
|
4
|
+
*
|
|
5
|
+
* Wrapper for GitHub API calls with automatic token injection,
|
|
6
|
+
* retry logic, and error handling.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.GitHubAPIClient = void 0;
|
|
10
|
+
const error_utils_1 = require("../utils/error-utils");
|
|
11
|
+
/**
|
|
12
|
+
* GitHub API Client
|
|
13
|
+
*
|
|
14
|
+
* Provides wrapper methods for common GitHub API operations with:
|
|
15
|
+
* - Automatic token injection
|
|
16
|
+
* - Retry logic with exponential backoff for rate limits
|
|
17
|
+
* - Error handling and transformation
|
|
18
|
+
*/
|
|
19
|
+
class GitHubAPIClient {
|
|
20
|
+
/**
|
|
21
|
+
* Create GitHub API Client
|
|
22
|
+
*
|
|
23
|
+
* @param installationId - GitHub installation ID
|
|
24
|
+
* @param authService - GitHub Auth Service instance
|
|
25
|
+
* @param baseUrl - GitHub API base URL (default: https://api.github.com)
|
|
26
|
+
*/
|
|
27
|
+
constructor(installationId, authService, baseUrl = "https://api.github.com") {
|
|
28
|
+
this.maxRetries = 3;
|
|
29
|
+
this.retryDelayMs = 1000;
|
|
30
|
+
this.installationId = installationId;
|
|
31
|
+
this.authService = authService;
|
|
32
|
+
this.baseUrl = baseUrl;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generic API request method with automatic token injection
|
|
36
|
+
*
|
|
37
|
+
* @param method - HTTP method (GET, POST, PUT, DELETE)
|
|
38
|
+
* @param endpoint - API endpoint (e.g., "/repos/owner/repo")
|
|
39
|
+
* @param data - Request body data (for POST, PUT)
|
|
40
|
+
* @param retryCount - Current retry attempt (internal use)
|
|
41
|
+
* @returns Response data
|
|
42
|
+
* @throws Error if request fails after retries
|
|
43
|
+
*/
|
|
44
|
+
async request(method, endpoint, data, retryCount = 0) {
|
|
45
|
+
try {
|
|
46
|
+
// Get installation token (cached or fresh)
|
|
47
|
+
const token = await this.authService.getInstallationToken(this.installationId);
|
|
48
|
+
// Build full URL
|
|
49
|
+
const url = endpoint.startsWith("http") ? endpoint : `${this.baseUrl}${endpoint}`;
|
|
50
|
+
// Make request
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
method,
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
Accept: "application/vnd.github+json",
|
|
56
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
57
|
+
...(data ? { "Content-Type": "application/json" } : {}),
|
|
58
|
+
},
|
|
59
|
+
...(data ? { body: JSON.stringify(data) } : {}),
|
|
60
|
+
});
|
|
61
|
+
// Handle rate limiting with retry
|
|
62
|
+
if (response.status === 403) {
|
|
63
|
+
const rateLimitRemaining = response.headers.get("x-ratelimit-remaining");
|
|
64
|
+
if (rateLimitRemaining === "0" && retryCount < this.maxRetries) {
|
|
65
|
+
// Calculate exponential backoff delay
|
|
66
|
+
const delay = this.retryDelayMs * Math.pow(2, retryCount);
|
|
67
|
+
await this.sleep(delay);
|
|
68
|
+
return this.request(method, endpoint, data, retryCount + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Handle non-OK responses
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const errorBody = await response.text();
|
|
74
|
+
const error = new Error(`GitHub API request failed: ${response.statusText}`);
|
|
75
|
+
error.status = response.status;
|
|
76
|
+
error.message = errorBody;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
// Parse and return response
|
|
80
|
+
const responseData = await response.json();
|
|
81
|
+
return responseData;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// If we can retry, do so
|
|
85
|
+
if (this.shouldRetry(error) && retryCount < this.maxRetries) {
|
|
86
|
+
const delay = this.retryDelayMs * Math.pow(2, retryCount);
|
|
87
|
+
await this.sleep(delay);
|
|
88
|
+
return this.request(method, endpoint, data, retryCount + 1);
|
|
89
|
+
}
|
|
90
|
+
// Transform error to standardized format
|
|
91
|
+
throw (0, error_utils_1.handleGitHubAPIError)(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get repositories accessible by this installation
|
|
96
|
+
*
|
|
97
|
+
* @returns Array of repositories
|
|
98
|
+
*/
|
|
99
|
+
async getRepositories() {
|
|
100
|
+
const response = await this.request("GET", "/installation/repositories");
|
|
101
|
+
return response.repositories;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get repository details
|
|
105
|
+
*
|
|
106
|
+
* @param owner - Repository owner (user or org)
|
|
107
|
+
* @param repo - Repository name
|
|
108
|
+
* @returns Repository details
|
|
109
|
+
*/
|
|
110
|
+
async getRepository(owner, repo) {
|
|
111
|
+
return this.request("GET", `/repos/${owner}/${repo}`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get repository contents
|
|
115
|
+
*
|
|
116
|
+
* @param owner - Repository owner
|
|
117
|
+
* @param repo - Repository name
|
|
118
|
+
* @param path - File or directory path
|
|
119
|
+
* @returns Content array (for directories) or single content (for files)
|
|
120
|
+
*/
|
|
121
|
+
async getContents(owner, repo, path) {
|
|
122
|
+
return this.request("GET", `/repos/${owner}/${repo}/contents/${path}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Create an issue
|
|
126
|
+
*
|
|
127
|
+
* @param owner - Repository owner
|
|
128
|
+
* @param repo - Repository name
|
|
129
|
+
* @param params - Issue creation parameters
|
|
130
|
+
* @returns Created issue
|
|
131
|
+
*/
|
|
132
|
+
async createIssue(owner, repo, params) {
|
|
133
|
+
return this.request("POST", `/repos/${owner}/${repo}/issues`, params);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Sleep utility for retry delays
|
|
137
|
+
*
|
|
138
|
+
* @param ms - Milliseconds to sleep
|
|
139
|
+
* @private
|
|
140
|
+
*/
|
|
141
|
+
sleep(ms) {
|
|
142
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Determine if error should trigger retry
|
|
146
|
+
*
|
|
147
|
+
* @param error - Error to check
|
|
148
|
+
* @returns true if should retry
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
shouldRetry(error) {
|
|
152
|
+
// Retry on network errors
|
|
153
|
+
if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// Retry on rate limit (403)
|
|
157
|
+
if (error.status === 403) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// Retry on server errors (500, 502, 503, 504)
|
|
161
|
+
if (error.status >= 500 && error.status < 600) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.GitHubAPIClient = GitHubAPIClient;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Authentication Service
|
|
3
|
+
*
|
|
4
|
+
* Handles GitHub App authentication including JWT generation and
|
|
5
|
+
* installation token exchange with automatic caching.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* GitHub Authentication Service
|
|
9
|
+
*
|
|
10
|
+
* Manages GitHub App authentication flow:
|
|
11
|
+
* 1. Generate GitHub App JWT using private key
|
|
12
|
+
* 2. Exchange JWT for installation access token
|
|
13
|
+
* 3. Cache installation tokens with automatic expiration
|
|
14
|
+
* 4. Automatically refresh expired tokens
|
|
15
|
+
*/
|
|
16
|
+
export declare class GitHubAuthService {
|
|
17
|
+
private appId;
|
|
18
|
+
private privateKey;
|
|
19
|
+
private baseUrl;
|
|
20
|
+
private tokenCache;
|
|
21
|
+
private tokenCacheTTL;
|
|
22
|
+
/**
|
|
23
|
+
* Create GitHub Auth Service
|
|
24
|
+
*
|
|
25
|
+
* @param appId - GitHub App ID
|
|
26
|
+
* @param privateKeyBase64 - Base64 encoded RSA private key (will be decoded to PEM format, PKCS#1 or PKCS#8)
|
|
27
|
+
* @param baseUrl - GitHub API base URL (default: https://api.github.com)
|
|
28
|
+
* @param tokenCacheTTL - Token cache TTL in seconds (default: 3300 = 55 minutes)
|
|
29
|
+
* @throws Error if private key is invalid or cannot be decoded
|
|
30
|
+
*/
|
|
31
|
+
constructor(appId: string, privateKeyBase64: string, baseUrl?: string, tokenCacheTTL?: number);
|
|
32
|
+
/**
|
|
33
|
+
* Generate GitHub App JWT
|
|
34
|
+
*
|
|
35
|
+
* Creates a JWT signed with the app's private key. The JWT is valid
|
|
36
|
+
* for 10 minutes and is used to authenticate as the GitHub App itself.
|
|
37
|
+
*
|
|
38
|
+
* @returns Signed JWT token
|
|
39
|
+
* @throws Error if JWT signing fails
|
|
40
|
+
*/
|
|
41
|
+
generateAppJWT(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Get installation access token
|
|
44
|
+
*
|
|
45
|
+
* Retrieves an installation access token for the specified installation.
|
|
46
|
+
* Tokens are automatically cached and refreshed when expired.
|
|
47
|
+
*
|
|
48
|
+
* Flow:
|
|
49
|
+
* 1. Check cache for valid token
|
|
50
|
+
* 2. If not cached or expired, generate GitHub App JWT
|
|
51
|
+
* 3. Exchange JWT for installation token via GitHub API
|
|
52
|
+
* 4. Cache the new token
|
|
53
|
+
* 5. Return token
|
|
54
|
+
*
|
|
55
|
+
* @param installationId - GitHub installation ID
|
|
56
|
+
* @returns Installation access token
|
|
57
|
+
* @throws Error if token exchange fails
|
|
58
|
+
*/
|
|
59
|
+
getInstallationToken(installationId: number): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Exchange GitHub App JWT for installation access token
|
|
62
|
+
*
|
|
63
|
+
* Calls GitHub API to exchange the app JWT for an installation-specific
|
|
64
|
+
* access token that can be used to make API calls on behalf of the installation.
|
|
65
|
+
*
|
|
66
|
+
* @param installationId - GitHub installation ID
|
|
67
|
+
* @param jwt - GitHub App JWT
|
|
68
|
+
* @returns Installation token response
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
private exchangeJWTForInstallationToken;
|
|
72
|
+
/**
|
|
73
|
+
* Clear token cache
|
|
74
|
+
*
|
|
75
|
+
* Removes all cached installation tokens. Useful for testing,
|
|
76
|
+
* forcing token refresh, or plugin shutdown.
|
|
77
|
+
*/
|
|
78
|
+
clearTokenCache(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Delete specific installation token from cache
|
|
81
|
+
*
|
|
82
|
+
* @param installationId - GitHub installation ID
|
|
83
|
+
*/
|
|
84
|
+
deleteInstallationToken(installationId: number): void;
|
|
85
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Authentication Service
|
|
4
|
+
*
|
|
5
|
+
* Handles GitHub App authentication including JWT generation and
|
|
6
|
+
* installation token exchange with automatic caching.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.GitHubAuthService = void 0;
|
|
10
|
+
const jwt_utils_1 = require("../utils/jwt-utils");
|
|
11
|
+
const token_cache_utils_1 = require("../utils/token-cache-utils");
|
|
12
|
+
const error_utils_1 = require("../utils/error-utils");
|
|
13
|
+
/**
|
|
14
|
+
* GitHub Authentication Service
|
|
15
|
+
*
|
|
16
|
+
* Manages GitHub App authentication flow:
|
|
17
|
+
* 1. Generate GitHub App JWT using private key
|
|
18
|
+
* 2. Exchange JWT for installation access token
|
|
19
|
+
* 3. Cache installation tokens with automatic expiration
|
|
20
|
+
* 4. Automatically refresh expired tokens
|
|
21
|
+
*/
|
|
22
|
+
class GitHubAuthService {
|
|
23
|
+
/**
|
|
24
|
+
* Create GitHub Auth Service
|
|
25
|
+
*
|
|
26
|
+
* @param appId - GitHub App ID
|
|
27
|
+
* @param privateKeyBase64 - Base64 encoded RSA private key (will be decoded to PEM format, PKCS#1 or PKCS#8)
|
|
28
|
+
* @param baseUrl - GitHub API base URL (default: https://api.github.com)
|
|
29
|
+
* @param tokenCacheTTL - Token cache TTL in seconds (default: 3300 = 55 minutes)
|
|
30
|
+
* @throws Error if private key is invalid or cannot be decoded
|
|
31
|
+
*/
|
|
32
|
+
constructor(appId, privateKeyBase64, baseUrl = "https://api.github.com", tokenCacheTTL = 3300) {
|
|
33
|
+
// Decode base64 private key to PEM format
|
|
34
|
+
let privateKey;
|
|
35
|
+
try {
|
|
36
|
+
privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8');
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.INVALID_PRIVATE_KEY, `Failed to decode base64 private key: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
// Validate private key format after decoding
|
|
42
|
+
(0, error_utils_1.validatePrivateKey)(privateKey);
|
|
43
|
+
this.appId = appId;
|
|
44
|
+
this.privateKey = privateKey;
|
|
45
|
+
this.baseUrl = baseUrl;
|
|
46
|
+
this.tokenCache = new token_cache_utils_1.TokenCache();
|
|
47
|
+
this.tokenCacheTTL = tokenCacheTTL;
|
|
48
|
+
// Test JWT signing on startup to catch key errors early
|
|
49
|
+
try {
|
|
50
|
+
this.generateAppJWT();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.JWT_SIGNING_FAILED, "Failed to sign JWT with provided private key. Please verify the key format.", {
|
|
54
|
+
error: error.message,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate GitHub App JWT
|
|
60
|
+
*
|
|
61
|
+
* Creates a JWT signed with the app's private key. The JWT is valid
|
|
62
|
+
* for 10 minutes and is used to authenticate as the GitHub App itself.
|
|
63
|
+
*
|
|
64
|
+
* @returns Signed JWT token
|
|
65
|
+
* @throws Error if JWT signing fails
|
|
66
|
+
*/
|
|
67
|
+
generateAppJWT() {
|
|
68
|
+
try {
|
|
69
|
+
return (0, jwt_utils_1.generateJWT)(this.appId, this.privateKey);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.JWT_SIGNING_FAILED, "Failed to generate GitHub App JWT", { error: error.message });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get installation access token
|
|
77
|
+
*
|
|
78
|
+
* Retrieves an installation access token for the specified installation.
|
|
79
|
+
* Tokens are automatically cached and refreshed when expired.
|
|
80
|
+
*
|
|
81
|
+
* Flow:
|
|
82
|
+
* 1. Check cache for valid token
|
|
83
|
+
* 2. If not cached or expired, generate GitHub App JWT
|
|
84
|
+
* 3. Exchange JWT for installation token via GitHub API
|
|
85
|
+
* 4. Cache the new token
|
|
86
|
+
* 5. Return token
|
|
87
|
+
*
|
|
88
|
+
* @param installationId - GitHub installation ID
|
|
89
|
+
* @returns Installation access token
|
|
90
|
+
* @throws Error if token exchange fails
|
|
91
|
+
*/
|
|
92
|
+
async getInstallationToken(installationId) {
|
|
93
|
+
// Check cache first
|
|
94
|
+
const cachedToken = this.tokenCache.getToken(installationId);
|
|
95
|
+
if (cachedToken) {
|
|
96
|
+
return cachedToken;
|
|
97
|
+
}
|
|
98
|
+
// Generate GitHub App JWT
|
|
99
|
+
const jwt = this.generateAppJWT();
|
|
100
|
+
// Exchange JWT for installation token
|
|
101
|
+
try {
|
|
102
|
+
const response = await this.exchangeJWTForInstallationToken(installationId, jwt);
|
|
103
|
+
// Cache the token
|
|
104
|
+
this.tokenCache.setToken(installationId, response.token, this.tokenCacheTTL);
|
|
105
|
+
return response.token;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw (0, error_utils_1.handleGitHubAPIError)(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Exchange GitHub App JWT for installation access token
|
|
113
|
+
*
|
|
114
|
+
* Calls GitHub API to exchange the app JWT for an installation-specific
|
|
115
|
+
* access token that can be used to make API calls on behalf of the installation.
|
|
116
|
+
*
|
|
117
|
+
* @param installationId - GitHub installation ID
|
|
118
|
+
* @param jwt - GitHub App JWT
|
|
119
|
+
* @returns Installation token response
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
async exchangeJWTForInstallationToken(installationId, jwt) {
|
|
123
|
+
const url = `${this.baseUrl}/app/installations/${installationId}/access_tokens`;
|
|
124
|
+
const response = await fetch(url, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${jwt}`,
|
|
128
|
+
Accept: "application/vnd.github+json",
|
|
129
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const errorBody = await response.text();
|
|
134
|
+
const error = new Error(`GitHub API request failed: ${response.statusText}`);
|
|
135
|
+
error.status = response.status;
|
|
136
|
+
error.message = errorBody;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
const data = await response.json();
|
|
140
|
+
return data;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear token cache
|
|
144
|
+
*
|
|
145
|
+
* Removes all cached installation tokens. Useful for testing,
|
|
146
|
+
* forcing token refresh, or plugin shutdown.
|
|
147
|
+
*/
|
|
148
|
+
clearTokenCache() {
|
|
149
|
+
this.tokenCache.clearCache();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Delete specific installation token from cache
|
|
153
|
+
*
|
|
154
|
+
* @param installationId - GitHub installation ID
|
|
155
|
+
*/
|
|
156
|
+
deleteInstallationToken(installationId) {
|
|
157
|
+
this.tokenCache.deleteToken(installationId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.GitHubAuthService = GitHubAuthService;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Validator Service
|
|
3
|
+
*
|
|
4
|
+
* Validates GitHub webhook signatures and parses webhook payloads.
|
|
5
|
+
*/
|
|
6
|
+
/// <reference types="node" />
|
|
7
|
+
/// <reference types="node" />
|
|
8
|
+
/**
|
|
9
|
+
* Parsed webhook payload data
|
|
10
|
+
*/
|
|
11
|
+
export interface WebhookPayload {
|
|
12
|
+
action?: string;
|
|
13
|
+
installation?: {
|
|
14
|
+
id: number;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
};
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Webhook validation result
|
|
21
|
+
*/
|
|
22
|
+
export interface WebhookValidationResult {
|
|
23
|
+
isValid: boolean;
|
|
24
|
+
payload?: WebhookPayload;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Webhook Validator
|
|
29
|
+
*
|
|
30
|
+
* Handles webhook signature validation and payload parsing for GitHub webhooks.
|
|
31
|
+
* Uses constant-time comparison to prevent timing attacks.
|
|
32
|
+
*/
|
|
33
|
+
export declare class WebhookValidator {
|
|
34
|
+
private secret;
|
|
35
|
+
/**
|
|
36
|
+
* Create Webhook Validator
|
|
37
|
+
*
|
|
38
|
+
* @param secret - Webhook secret configured in GitHub App
|
|
39
|
+
*/
|
|
40
|
+
constructor(secret: string);
|
|
41
|
+
/**
|
|
42
|
+
* Validate webhook signature
|
|
43
|
+
*
|
|
44
|
+
* Verifies the X-Hub-Signature-256 header using HMAC-SHA256 with
|
|
45
|
+
* constant-time comparison to prevent timing attacks.
|
|
46
|
+
*
|
|
47
|
+
* @param rawBody - Raw request body (string or Buffer)
|
|
48
|
+
* @param signature - X-Hub-Signature-256 header value
|
|
49
|
+
* @returns true if signature is valid, false otherwise
|
|
50
|
+
*/
|
|
51
|
+
validateSignature(rawBody: string | Buffer, signature: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Parse webhook payload
|
|
54
|
+
*
|
|
55
|
+
* Parses the JSON payload from a webhook request.
|
|
56
|
+
* Extracts common fields like action and installationId.
|
|
57
|
+
*
|
|
58
|
+
* @param body - Raw request body (string)
|
|
59
|
+
* @returns Parsed webhook payload
|
|
60
|
+
* @throws Error if payload is malformed JSON
|
|
61
|
+
*/
|
|
62
|
+
parsePayload(body: string): WebhookPayload;
|
|
63
|
+
/**
|
|
64
|
+
* Extract event type from payload
|
|
65
|
+
*
|
|
66
|
+
* Gets the event type from the parsed payload.
|
|
67
|
+
* For installation webhooks, this is typically found in the 'action' field.
|
|
68
|
+
*
|
|
69
|
+
* @param payload - Parsed webhook payload
|
|
70
|
+
* @returns Event type (e.g., 'created', 'deleted')
|
|
71
|
+
*/
|
|
72
|
+
extractEventType(payload: WebhookPayload): string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Extract installation ID from payload
|
|
75
|
+
*
|
|
76
|
+
* Gets the installation ID from the parsed payload.
|
|
77
|
+
*
|
|
78
|
+
* @param payload - Parsed webhook payload
|
|
79
|
+
* @returns Installation ID, or undefined if not present
|
|
80
|
+
*/
|
|
81
|
+
extractInstallationId(payload: WebhookPayload): number | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Validate and parse webhook in one step
|
|
84
|
+
*
|
|
85
|
+
* Convenience method that validates signature and parses payload
|
|
86
|
+
* in a single call.
|
|
87
|
+
*
|
|
88
|
+
* @param rawBody - Raw request body
|
|
89
|
+
* @param signature - X-Hub-Signature-256 header
|
|
90
|
+
* @returns Validation result with parsed payload if valid
|
|
91
|
+
*/
|
|
92
|
+
validateAndParse(rawBody: string | Buffer, signature: string): WebhookValidationResult;
|
|
93
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhook Validator Service
|
|
4
|
+
*
|
|
5
|
+
* Validates GitHub webhook signatures and parses webhook payloads.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.WebhookValidator = void 0;
|
|
9
|
+
const webhook_signature_utils_1 = require("../utils/webhook-signature-utils");
|
|
10
|
+
const error_utils_1 = require("../utils/error-utils");
|
|
11
|
+
/**
|
|
12
|
+
* Webhook Validator
|
|
13
|
+
*
|
|
14
|
+
* Handles webhook signature validation and payload parsing for GitHub webhooks.
|
|
15
|
+
* Uses constant-time comparison to prevent timing attacks.
|
|
16
|
+
*/
|
|
17
|
+
class WebhookValidator {
|
|
18
|
+
/**
|
|
19
|
+
* Create Webhook Validator
|
|
20
|
+
*
|
|
21
|
+
* @param secret - Webhook secret configured in GitHub App
|
|
22
|
+
*/
|
|
23
|
+
constructor(secret) {
|
|
24
|
+
if (!secret || typeof secret !== "string") {
|
|
25
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.WEBHOOK_SIGNATURE_INVALID, "Webhook secret is required and must be a string");
|
|
26
|
+
}
|
|
27
|
+
this.secret = secret;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validate webhook signature
|
|
31
|
+
*
|
|
32
|
+
* Verifies the X-Hub-Signature-256 header using HMAC-SHA256 with
|
|
33
|
+
* constant-time comparison to prevent timing attacks.
|
|
34
|
+
*
|
|
35
|
+
* @param rawBody - Raw request body (string or Buffer)
|
|
36
|
+
* @param signature - X-Hub-Signature-256 header value
|
|
37
|
+
* @returns true if signature is valid, false otherwise
|
|
38
|
+
*/
|
|
39
|
+
validateSignature(rawBody, signature) {
|
|
40
|
+
if (!rawBody || !signature) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return (0, webhook_signature_utils_1.validateWebhookSignature)(rawBody, signature, this.secret);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse webhook payload
|
|
47
|
+
*
|
|
48
|
+
* Parses the JSON payload from a webhook request.
|
|
49
|
+
* Extracts common fields like action and installationId.
|
|
50
|
+
*
|
|
51
|
+
* @param body - Raw request body (string)
|
|
52
|
+
* @returns Parsed webhook payload
|
|
53
|
+
* @throws Error if payload is malformed JSON
|
|
54
|
+
*/
|
|
55
|
+
parsePayload(body) {
|
|
56
|
+
try {
|
|
57
|
+
const payload = JSON.parse(body);
|
|
58
|
+
return payload;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.WEBHOOK_PAYLOAD_INVALID, "Failed to parse webhook payload as JSON", { error: error.message });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extract event type from payload
|
|
66
|
+
*
|
|
67
|
+
* Gets the event type from the parsed payload.
|
|
68
|
+
* For installation webhooks, this is typically found in the 'action' field.
|
|
69
|
+
*
|
|
70
|
+
* @param payload - Parsed webhook payload
|
|
71
|
+
* @returns Event type (e.g., 'created', 'deleted')
|
|
72
|
+
*/
|
|
73
|
+
extractEventType(payload) {
|
|
74
|
+
return payload.action;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract installation ID from payload
|
|
78
|
+
*
|
|
79
|
+
* Gets the installation ID from the parsed payload.
|
|
80
|
+
*
|
|
81
|
+
* @param payload - Parsed webhook payload
|
|
82
|
+
* @returns Installation ID, or undefined if not present
|
|
83
|
+
*/
|
|
84
|
+
extractInstallationId(payload) {
|
|
85
|
+
return payload.installation?.id;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate and parse webhook in one step
|
|
89
|
+
*
|
|
90
|
+
* Convenience method that validates signature and parses payload
|
|
91
|
+
* in a single call.
|
|
92
|
+
*
|
|
93
|
+
* @param rawBody - Raw request body
|
|
94
|
+
* @param signature - X-Hub-Signature-256 header
|
|
95
|
+
* @returns Validation result with parsed payload if valid
|
|
96
|
+
*/
|
|
97
|
+
validateAndParse(rawBody, signature) {
|
|
98
|
+
// Validate signature first
|
|
99
|
+
const isValid = this.validateSignature(rawBody, signature);
|
|
100
|
+
if (!isValid) {
|
|
101
|
+
return {
|
|
102
|
+
isValid: false,
|
|
103
|
+
error: "Invalid webhook signature",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Parse payload
|
|
107
|
+
try {
|
|
108
|
+
const bodyString = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
109
|
+
const payload = this.parsePayload(bodyString);
|
|
110
|
+
return {
|
|
111
|
+
isValid: true,
|
|
112
|
+
payload,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
isValid: false,
|
|
118
|
+
error: error.message || "Failed to parse webhook payload",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.WebhookValidator = WebhookValidator;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub App Error Interface and Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized error handling for GitHub App flows including
|
|
5
|
+
* user-friendly messages and error code mapping.
|
|
6
|
+
*
|
|
7
|
+
* Error codes use kebab-case for frontend translation consistency.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Standardized GitHub App error structure
|
|
11
|
+
*/
|
|
12
|
+
export interface GitHubAppError {
|
|
13
|
+
/** Error code for programmatic handling (kebab-case) */
|
|
14
|
+
code: string;
|
|
15
|
+
/** User-friendly error message */
|
|
16
|
+
message: string;
|
|
17
|
+
/** Additional error details (for logging, not user display) */
|
|
18
|
+
details?: any;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* GitHub App error codes
|
|
22
|
+
*
|
|
23
|
+
* All codes use kebab-case for frontend translation.
|
|
24
|
+
*/
|
|
25
|
+
export declare const GitHubAppErrorCodes: {
|
|
26
|
+
readonly INVALID_STATE: "invalid-state";
|
|
27
|
+
readonly SESSION_EXPIRED: "session-expired";
|
|
28
|
+
readonly INSTALLATION_NOT_FOUND: "installation-not-found";
|
|
29
|
+
readonly INVALID_PRIVATE_KEY: "invalid-private-key";
|
|
30
|
+
readonly JWT_SIGNING_FAILED: "jwt-signing-failed";
|
|
31
|
+
readonly TOKEN_EXCHANGE_FAILED: "token-exchange-failed";
|
|
32
|
+
readonly WEBHOOK_SIGNATURE_INVALID: "webhook-signature-invalid";
|
|
33
|
+
readonly WEBHOOK_PAYLOAD_INVALID: "webhook-payload-invalid";
|
|
34
|
+
readonly REPOSITORY_NOT_ACCESSIBLE: "repository-not-accessible";
|
|
35
|
+
readonly INSTALLATION_SUSPENDED: "installation-suspended";
|
|
36
|
+
readonly INSTALLATION_NOT_OWNED: "installation-not-owned";
|
|
37
|
+
readonly API_RATE_LIMIT: "api-rate-limit";
|
|
38
|
+
readonly NETWORK_ERROR: "network-error";
|
|
39
|
+
readonly SERVER_ERROR: "server-error";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Create a standardized GitHub App error
|
|
43
|
+
*
|
|
44
|
+
* @param code - Error code from GitHubAppErrorCodes
|
|
45
|
+
* @param message - User-friendly error message
|
|
46
|
+
* @param details - Additional error details for logging
|
|
47
|
+
* @returns Standardized GitHubAppError object
|
|
48
|
+
*/
|
|
49
|
+
export declare function createGitHubAppError(code: string, message: string, details?: any): GitHubAppError;
|
|
50
|
+
/**
|
|
51
|
+
* Validate private key format
|
|
52
|
+
*
|
|
53
|
+
* @param privateKey - Private key to validate
|
|
54
|
+
* @returns true if valid
|
|
55
|
+
* @throws GitHubAppError if invalid
|
|
56
|
+
*/
|
|
57
|
+
export declare function validatePrivateKey(privateKey: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Map GitHub API errors to standardized errors
|
|
60
|
+
*
|
|
61
|
+
* Converts GitHub API error responses into user-friendly
|
|
62
|
+
* standardized errors while sanitizing sensitive data.
|
|
63
|
+
*
|
|
64
|
+
* @param error - Error from GitHub API or internal error
|
|
65
|
+
* @returns Standardized GitHubAppError
|
|
66
|
+
*/
|
|
67
|
+
export declare function handleGitHubAPIError(error: any): GitHubAppError;
|