@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,148 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* Standardized GitHub App error structure
|
|
12
|
+
*/
|
|
13
|
+
export interface GitHubAppError {
|
|
14
|
+
/** Error code for programmatic handling (kebab-case) */
|
|
15
|
+
code: string;
|
|
16
|
+
|
|
17
|
+
/** User-friendly error message */
|
|
18
|
+
message: string;
|
|
19
|
+
|
|
20
|
+
/** Additional error details (for logging, not user display) */
|
|
21
|
+
details?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GitHub App error codes
|
|
26
|
+
*
|
|
27
|
+
* All codes use kebab-case for frontend translation.
|
|
28
|
+
*/
|
|
29
|
+
export const GitHubAppErrorCodes = {
|
|
30
|
+
// Installation Flow Errors
|
|
31
|
+
INVALID_STATE: "invalid-state",
|
|
32
|
+
SESSION_EXPIRED: "session-expired",
|
|
33
|
+
INSTALLATION_NOT_FOUND: "installation-not-found",
|
|
34
|
+
|
|
35
|
+
// Authentication Errors
|
|
36
|
+
INVALID_PRIVATE_KEY: "invalid-private-key",
|
|
37
|
+
JWT_SIGNING_FAILED: "jwt-signing-failed",
|
|
38
|
+
TOKEN_EXCHANGE_FAILED: "token-exchange-failed",
|
|
39
|
+
|
|
40
|
+
// Webhook Errors
|
|
41
|
+
WEBHOOK_SIGNATURE_INVALID: "webhook-signature-invalid",
|
|
42
|
+
WEBHOOK_PAYLOAD_INVALID: "webhook-payload-invalid",
|
|
43
|
+
|
|
44
|
+
// Access Errors
|
|
45
|
+
REPOSITORY_NOT_ACCESSIBLE: "repository-not-accessible",
|
|
46
|
+
INSTALLATION_SUSPENDED: "installation-suspended",
|
|
47
|
+
INSTALLATION_NOT_OWNED: "installation-not-owned",
|
|
48
|
+
|
|
49
|
+
// API Errors
|
|
50
|
+
API_RATE_LIMIT: "api-rate-limit",
|
|
51
|
+
NETWORK_ERROR: "network-error",
|
|
52
|
+
SERVER_ERROR: "server-error",
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a standardized GitHub App error
|
|
57
|
+
*
|
|
58
|
+
* @param code - Error code from GitHubAppErrorCodes
|
|
59
|
+
* @param message - User-friendly error message
|
|
60
|
+
* @param details - Additional error details for logging
|
|
61
|
+
* @returns Standardized GitHubAppError object
|
|
62
|
+
*/
|
|
63
|
+
export function createGitHubAppError(code: string, message: string, details?: any): GitHubAppError {
|
|
64
|
+
return {
|
|
65
|
+
code,
|
|
66
|
+
message,
|
|
67
|
+
details,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate private key format
|
|
73
|
+
*
|
|
74
|
+
* @param privateKey - Private key to validate
|
|
75
|
+
* @returns true if valid
|
|
76
|
+
* @throws GitHubAppError if invalid
|
|
77
|
+
*/
|
|
78
|
+
export function validatePrivateKey(privateKey: string): boolean {
|
|
79
|
+
if (!privateKey || typeof privateKey !== "string") {
|
|
80
|
+
throw createGitHubAppError(GitHubAppErrorCodes.INVALID_PRIVATE_KEY, "Private key is required and must be a string", {
|
|
81
|
+
provided: typeof privateKey,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!privateKey.includes("BEGIN") || !privateKey.includes("PRIVATE KEY")) {
|
|
86
|
+
throw createGitHubAppError(GitHubAppErrorCodes.INVALID_PRIVATE_KEY, "Private key must be in PEM format (PKCS#1 or PKCS#8)", {
|
|
87
|
+
hint: "Expected format: -----BEGIN RSA PRIVATE KEY----- or -----BEGIN PRIVATE KEY-----",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Map GitHub API errors to standardized errors
|
|
96
|
+
*
|
|
97
|
+
* Converts GitHub API error responses into user-friendly
|
|
98
|
+
* standardized errors while sanitizing sensitive data.
|
|
99
|
+
*
|
|
100
|
+
* @param error - Error from GitHub API or internal error
|
|
101
|
+
* @returns Standardized GitHubAppError
|
|
102
|
+
*/
|
|
103
|
+
export function handleGitHubAPIError(error: any): GitHubAppError {
|
|
104
|
+
// Handle network errors
|
|
105
|
+
if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
|
|
106
|
+
return createGitHubAppError(GitHubAppErrorCodes.NETWORK_ERROR, "Unable to connect to GitHub API. Please check your connection and try again.", {
|
|
107
|
+
networkError: error.code,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle rate limit errors
|
|
112
|
+
if (error.status === 403 && error.message?.includes("rate limit")) {
|
|
113
|
+
return createGitHubAppError(GitHubAppErrorCodes.API_RATE_LIMIT, "GitHub API rate limit exceeded. Please try again later.", {
|
|
114
|
+
status: error.status,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle installation suspended
|
|
119
|
+
if (error.status === 403 && error.message?.includes("suspended")) {
|
|
120
|
+
return createGitHubAppError(
|
|
121
|
+
GitHubAppErrorCodes.INSTALLATION_SUSPENDED,
|
|
122
|
+
"This GitHub App installation has been suspended. Please contact the repository owner.",
|
|
123
|
+
{
|
|
124
|
+
status: error.status,
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle installation not found
|
|
130
|
+
if (error.status === 404) {
|
|
131
|
+
return createGitHubAppError(GitHubAppErrorCodes.INSTALLATION_NOT_FOUND, "GitHub App installation not found.", {
|
|
132
|
+
status: error.status,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle unauthorized (invalid JWT or token)
|
|
137
|
+
if (error.status === 401) {
|
|
138
|
+
return createGitHubAppError(GitHubAppErrorCodes.TOKEN_EXCHANGE_FAILED, "Failed to authenticate with GitHub. Please try reinstalling the app.", {
|
|
139
|
+
status: error.status,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Generic error fallback
|
|
144
|
+
return createGitHubAppError(GitHubAppErrorCodes.SERVER_ERROR, "An unexpected error occurred with GitHub API. Please try again.", {
|
|
145
|
+
originalError: error.message || "Unknown error",
|
|
146
|
+
status: error.status,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT Utilities for GitHub App Authentication
|
|
3
|
+
*
|
|
4
|
+
* Provides JWT generation with RS256 algorithm for GitHub App authentication.
|
|
5
|
+
* Supports both PKCS#1 and PKCS#8 private key formats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import jwt from "jsonwebtoken";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect PEM format (PKCS#1 or PKCS#8)
|
|
12
|
+
*
|
|
13
|
+
* GitHub Apps can use either PKCS#1 (BEGIN RSA PRIVATE KEY) or
|
|
14
|
+
* PKCS#8 (BEGIN PRIVATE KEY) format. This function auto-detects
|
|
15
|
+
* the format for proper handling.
|
|
16
|
+
*
|
|
17
|
+
* @param privateKey - PEM-encoded private key
|
|
18
|
+
* @returns Format type: 'pkcs1' or 'pkcs8'
|
|
19
|
+
* @throws Error if format is not recognized
|
|
20
|
+
*/
|
|
21
|
+
export function detectPEMFormat(privateKey: string): "pkcs1" | "pkcs8" {
|
|
22
|
+
if (privateKey.includes("BEGIN RSA PRIVATE KEY")) {
|
|
23
|
+
return "pkcs1";
|
|
24
|
+
} else if (privateKey.includes("BEGIN PRIVATE KEY")) {
|
|
25
|
+
return "pkcs8";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error("Invalid PEM format. Expected PKCS#1 (BEGIN RSA PRIVATE KEY) or PKCS#8 (BEGIN PRIVATE KEY).");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate GitHub App JWT
|
|
33
|
+
*
|
|
34
|
+
* Creates a JWT signed with the GitHub App's private key using RS256 algorithm.
|
|
35
|
+
* The JWT is valid for 10 minutes (600 seconds) as per GitHub's requirements.
|
|
36
|
+
*
|
|
37
|
+
* JWT Payload:
|
|
38
|
+
* - iat: Issued at timestamp (now)
|
|
39
|
+
* - exp: Expiration timestamp (now + 600 seconds)
|
|
40
|
+
* - iss: GitHub App ID
|
|
41
|
+
*
|
|
42
|
+
* @param appId - GitHub App ID
|
|
43
|
+
* @param privateKey - PEM-encoded RSA private key (PKCS#1 or PKCS#8)
|
|
44
|
+
* @returns Signed JWT token
|
|
45
|
+
* @throws Error if key is invalid or signing fails
|
|
46
|
+
*/
|
|
47
|
+
export function generateJWT(appId: string, privateKey: string): string {
|
|
48
|
+
// Validate private key format
|
|
49
|
+
detectPEMFormat(privateKey);
|
|
50
|
+
|
|
51
|
+
const now = Math.floor(Date.now() / 1000);
|
|
52
|
+
|
|
53
|
+
const payload = {
|
|
54
|
+
iat: now,
|
|
55
|
+
exp: now + 600, // 10 minutes from now
|
|
56
|
+
iss: appId,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return jwt.sign(payload, privateKey, { algorithm: "RS256" });
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
throw new Error(`Failed to sign JWT: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Parameter Utilities for CSRF Protection
|
|
3
|
+
*
|
|
4
|
+
* Provides cryptographically secure state parameter generation
|
|
5
|
+
* and constant-time comparison for GitHub App installation CSRF protection.
|
|
6
|
+
*
|
|
7
|
+
* Reuses pattern from OAuth Plugin for consistency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a cryptographically secure state parameter
|
|
14
|
+
*
|
|
15
|
+
* Creates a 32-byte random state parameter for CSRF protection
|
|
16
|
+
* in GitHub App installation flows. This state is stored in the session
|
|
17
|
+
* and must match when GitHub redirects back after installation.
|
|
18
|
+
*
|
|
19
|
+
* @returns Hex-encoded 32-byte random string (64 characters)
|
|
20
|
+
*/
|
|
21
|
+
export function generateState(): string {
|
|
22
|
+
return crypto.randomBytes(32).toString("hex");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate state parameter using constant-time comparison
|
|
27
|
+
*
|
|
28
|
+
* Compares the provided state with the stored state using a
|
|
29
|
+
* constant-time algorithm to prevent timing attacks.
|
|
30
|
+
*
|
|
31
|
+
* @param provided - State parameter from GitHub callback
|
|
32
|
+
* @param stored - State parameter stored in session
|
|
33
|
+
* @returns true if states match, false otherwise
|
|
34
|
+
*/
|
|
35
|
+
export function validateState(provided: string, stored: string): boolean {
|
|
36
|
+
if (!provided || !stored) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ensure both strings are the same length to prevent timing attacks
|
|
41
|
+
if (provided.length !== stored.length) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Use Node.js crypto.timingSafeEqual for constant-time comparison
|
|
47
|
+
const providedBuffer = Buffer.from(provided, "utf8");
|
|
48
|
+
const storedBuffer = Buffer.from(stored, "utf8");
|
|
49
|
+
|
|
50
|
+
// Both buffers must be same length for timingSafeEqual
|
|
51
|
+
if (providedBuffer.length !== storedBuffer.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return crypto.timingSafeEqual(providedBuffer, storedBuffer);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// If comparison fails for any reason, return false
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a session ID for installation flow tracking
|
|
64
|
+
*
|
|
65
|
+
* Creates a unique session identifier for correlating installation
|
|
66
|
+
* initiation with callback.
|
|
67
|
+
*
|
|
68
|
+
* @returns Hex-encoded 16-byte random string (32 characters)
|
|
69
|
+
*/
|
|
70
|
+
export function generateSessionId(): string {
|
|
71
|
+
return crypto.randomBytes(16).toString("hex");
|
|
72
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Cache Utilities
|
|
3
|
+
*
|
|
4
|
+
* In-memory cache for GitHub App installation access tokens.
|
|
5
|
+
* Tokens are cached with TTL to reduce GitHub API calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cache entry structure
|
|
10
|
+
*/
|
|
11
|
+
interface CacheEntry {
|
|
12
|
+
token: string;
|
|
13
|
+
expiresAt: number; // Unix timestamp in milliseconds
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* In-memory token cache
|
|
18
|
+
*
|
|
19
|
+
* Caches GitHub App installation access tokens with automatic expiration.
|
|
20
|
+
* Tokens are stored in memory only (never in database) for security.
|
|
21
|
+
*/
|
|
22
|
+
export class TokenCache {
|
|
23
|
+
private cache: Map<number, CacheEntry>;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this.cache = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get cached token for an installation
|
|
31
|
+
*
|
|
32
|
+
* Returns the cached token if it exists and hasn't expired.
|
|
33
|
+
* Returns null if token is not cached or has expired.
|
|
34
|
+
*
|
|
35
|
+
* @param installationId - GitHub installation ID
|
|
36
|
+
* @returns Cached token or null if not found/expired
|
|
37
|
+
*/
|
|
38
|
+
getToken(installationId: number): string | null {
|
|
39
|
+
const entry = this.cache.get(installationId);
|
|
40
|
+
|
|
41
|
+
if (!entry) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if token has expired
|
|
46
|
+
if (Date.now() >= entry.expiresAt) {
|
|
47
|
+
// Remove expired token from cache
|
|
48
|
+
this.cache.delete(installationId);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return entry.token;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Store token in cache with TTL
|
|
57
|
+
*
|
|
58
|
+
* @param installationId - GitHub installation ID
|
|
59
|
+
* @param token - Installation access token
|
|
60
|
+
* @param ttl - Time to live in seconds (default: 3300 = 55 minutes)
|
|
61
|
+
*/
|
|
62
|
+
setToken(installationId: number, token: string, ttl: number = 3300): void {
|
|
63
|
+
const expiresAt = Date.now() + ttl * 1000; // Convert seconds to milliseconds
|
|
64
|
+
|
|
65
|
+
this.cache.set(installationId, {
|
|
66
|
+
token,
|
|
67
|
+
expiresAt,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clear all cached tokens
|
|
73
|
+
*
|
|
74
|
+
* Removes all tokens from the cache. Useful for testing,
|
|
75
|
+
* plugin shutdown, or when forcing token refresh.
|
|
76
|
+
*/
|
|
77
|
+
clearCache(): void {
|
|
78
|
+
this.cache.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Remove specific installation token from cache
|
|
83
|
+
*
|
|
84
|
+
* @param installationId - GitHub installation ID
|
|
85
|
+
*/
|
|
86
|
+
deleteToken(installationId: number): void {
|
|
87
|
+
this.cache.delete(installationId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Signature Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Validates GitHub webhook signatures using HMAC-SHA256 with
|
|
5
|
+
* constant-time comparison to prevent timing attacks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate GitHub webhook signature
|
|
12
|
+
*
|
|
13
|
+
* GitHub signs webhook payloads with HMAC-SHA256 using the webhook secret.
|
|
14
|
+
* The signature is sent in the X-Hub-Signature-256 header as "sha256=<hex>".
|
|
15
|
+
*
|
|
16
|
+
* This function uses constant-time comparison to prevent timing attacks.
|
|
17
|
+
*
|
|
18
|
+
* @param payload - Raw request body (string or Buffer)
|
|
19
|
+
* @param signature - X-Hub-Signature-256 header value
|
|
20
|
+
* @param secret - Webhook secret configured in GitHub App
|
|
21
|
+
* @returns true if signature is valid, false otherwise
|
|
22
|
+
*/
|
|
23
|
+
export function validateWebhookSignature(payload: string | Buffer, signature: string, secret: string): boolean {
|
|
24
|
+
if (!payload || !signature || !secret) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Ensure signature has correct format (sha256=<hex>)
|
|
29
|
+
if (!signature.startsWith("sha256=")) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Extract hex signature (remove "sha256=" prefix)
|
|
35
|
+
const providedSignature = signature.substring(7);
|
|
36
|
+
|
|
37
|
+
// Compute expected signature
|
|
38
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
39
|
+
const payloadBuffer = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
|
|
40
|
+
hmac.update(payloadBuffer);
|
|
41
|
+
const expectedSignature = hmac.digest("hex");
|
|
42
|
+
|
|
43
|
+
// Use constant-time comparison to prevent timing attacks
|
|
44
|
+
const providedBuffer = Buffer.from(providedSignature, "hex");
|
|
45
|
+
const expectedBuffer = Buffer.from(expectedSignature, "hex");
|
|
46
|
+
|
|
47
|
+
// Both buffers must be same length for timingSafeEqual
|
|
48
|
+
if (providedBuffer.length !== expectedBuffer.length) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return crypto.timingSafeEqual(providedBuffer, expectedBuffer);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// If comparison fails for any reason, return false
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"module": "commonjs",
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": false,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"experimentalDecorators": true,
|
|
18
|
+
"checkJs": true,
|
|
19
|
+
"outDir": "dist",
|
|
20
|
+
"typeRoots": ["./node_modules/@types"]
|
|
21
|
+
},
|
|
22
|
+
"include": ["./src/*", "./spec/*"],
|
|
23
|
+
"exclude": ["./node_modules/*"]
|
|
24
|
+
}
|