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