@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/LICENSE +21 -0
  3. package/README.md +667 -0
  4. package/SECURITY.md +498 -0
  5. package/dist/GitHubAppInternalContext.d.ts +44 -0
  6. package/dist/GitHubAppInternalContext.js +2 -0
  7. package/dist/GitHubAppPlugin.d.ts +45 -0
  8. package/dist/GitHubAppPlugin.js +367 -0
  9. package/dist/GitHubAppPluginContext.d.ts +242 -0
  10. package/dist/GitHubAppPluginContext.js +2 -0
  11. package/dist/GitHubAppPluginOptions.d.ts +369 -0
  12. package/dist/GitHubAppPluginOptions.js +2 -0
  13. package/dist/handlers/InitiateInstallation.d.ts +32 -0
  14. package/dist/handlers/InitiateInstallation.js +66 -0
  15. package/dist/handlers/InstallationCallback.d.ts +42 -0
  16. package/dist/handlers/InstallationCallback.js +248 -0
  17. package/dist/handlers/UninstallHandler.d.ts +37 -0
  18. package/dist/handlers/UninstallHandler.js +153 -0
  19. package/dist/handlers/WebhookHandler.d.ts +54 -0
  20. package/dist/handlers/WebhookHandler.js +157 -0
  21. package/dist/index.d.ts +19 -0
  22. package/dist/index.js +23 -0
  23. package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
  24. package/dist/repos/GitHubAppSessionRepo.js +32 -0
  25. package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
  26. package/dist/repos/GitHubInstallationRepo.js +83 -0
  27. package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
  28. package/dist/repos/GitHubWebhookEventRepo.js +42 -0
  29. package/dist/schemas/GitHubAppSession.d.ts +13 -0
  30. package/dist/schemas/GitHubAppSession.js +2 -0
  31. package/dist/schemas/GitHubInstallation.d.ts +28 -0
  32. package/dist/schemas/GitHubInstallation.js +2 -0
  33. package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
  34. package/dist/schemas/InstallationCallbackRequest.js +2 -0
  35. package/dist/schemas/WebhookEvent.d.ts +16 -0
  36. package/dist/schemas/WebhookEvent.js +2 -0
  37. package/dist/schemas/WebhookPayload.d.ts +35 -0
  38. package/dist/schemas/WebhookPayload.js +2 -0
  39. package/dist/services/GitHubAPIClient.d.ts +143 -0
  40. package/dist/services/GitHubAPIClient.js +167 -0
  41. package/dist/services/GitHubAuthService.d.ts +85 -0
  42. package/dist/services/GitHubAuthService.js +160 -0
  43. package/dist/services/WebhookValidator.d.ts +93 -0
  44. package/dist/services/WebhookValidator.js +123 -0
  45. package/dist/utils/error-utils.d.ts +67 -0
  46. package/dist/utils/error-utils.js +121 -0
  47. package/dist/utils/jwt-utils.d.ts +35 -0
  48. package/dist/utils/jwt-utils.js +67 -0
  49. package/dist/utils/state-utils.d.ts +38 -0
  50. package/dist/utils/state-utils.js +74 -0
  51. package/dist/utils/token-cache-utils.d.ts +47 -0
  52. package/dist/utils/token-cache-utils.js +74 -0
  53. package/dist/utils/webhook-signature-utils.d.ts +22 -0
  54. package/dist/utils/webhook-signature-utils.js +57 -0
  55. package/examples/basic-installation.ts +246 -0
  56. package/examples/create-issue.ts +392 -0
  57. package/examples/error-handling.ts +396 -0
  58. package/examples/multi-event-webhook.ts +367 -0
  59. package/examples/organization-installation.ts +316 -0
  60. package/examples/repository-access.ts +480 -0
  61. package/examples/webhook-handling.ts +343 -0
  62. package/examples/with-jwt-auth.ts +319 -0
  63. package/package.json +41 -0
  64. package/spec/core-utilities.spec.ts +243 -0
  65. package/spec/handlers.spec.ts +216 -0
  66. package/spec/helpers/reporter.ts +41 -0
  67. package/spec/integration-and-security.spec.ts +483 -0
  68. package/spec/plugin-core.spec.ts +258 -0
  69. package/spec/project-setup.spec.ts +56 -0
  70. package/spec/repos-and-schemas.spec.ts +288 -0
  71. package/spec/services.spec.ts +108 -0
  72. package/spec/support/jasmine.json +7 -0
  73. package/src/GitHubAppPlugin.ts +411 -0
  74. package/src/GitHubAppPluginContext.ts +254 -0
  75. package/src/GitHubAppPluginOptions.ts +412 -0
  76. package/src/handlers/InstallationCallback.ts +292 -0
  77. package/src/handlers/WebhookHandler.ts +179 -0
  78. package/src/index.ts +29 -0
  79. package/src/repos/GitHubAppSessionRepo.ts +36 -0
  80. package/src/repos/GitHubInstallationRepo.ts +95 -0
  81. package/src/repos/GitHubWebhookEventRepo.ts +48 -0
  82. package/src/schemas/GitHubAppSession.ts +13 -0
  83. package/src/schemas/GitHubInstallation.ts +28 -0
  84. package/src/schemas/InstallationCallbackRequest.ts +10 -0
  85. package/src/schemas/WebhookEvent.ts +16 -0
  86. package/src/schemas/WebhookPayload.ts +35 -0
  87. package/src/services/GitHubAPIClient.ts +244 -0
  88. package/src/services/GitHubAuthService.ts +188 -0
  89. package/src/services/WebhookValidator.ts +159 -0
  90. package/src/utils/error-utils.ts +148 -0
  91. package/src/utils/jwt-utils.ts +64 -0
  92. package/src/utils/state-utils.ts +72 -0
  93. package/src/utils/token-cache-utils.ts +89 -0
  94. package/src/utils/webhook-signature-utils.ts +57 -0
  95. package/tsconfig.dist.json +4 -0
  96. 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;