@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,367 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.githubAppPlugin = void 0;
|
|
30
|
+
const flink_1 = require("@flink-app/flink");
|
|
31
|
+
const GitHubAppSessionRepo_1 = __importDefault(require("./repos/GitHubAppSessionRepo"));
|
|
32
|
+
const GitHubInstallationRepo_1 = __importDefault(require("./repos/GitHubInstallationRepo"));
|
|
33
|
+
const GitHubWebhookEventRepo_1 = __importDefault(require("./repos/GitHubWebhookEventRepo"));
|
|
34
|
+
const GitHubAuthService_1 = require("./services/GitHubAuthService");
|
|
35
|
+
const GitHubAPIClient_1 = require("./services/GitHubAPIClient");
|
|
36
|
+
const WebhookValidator_1 = require("./services/WebhookValidator");
|
|
37
|
+
const error_utils_1 = require("./utils/error-utils");
|
|
38
|
+
const state_utils_1 = require("./utils/state-utils");
|
|
39
|
+
const InstallationCallback = __importStar(require("./handlers/InstallationCallback"));
|
|
40
|
+
const WebhookHandler = __importStar(require("./handlers/WebhookHandler"));
|
|
41
|
+
/**
|
|
42
|
+
* GitHub App Plugin Factory Function
|
|
43
|
+
*
|
|
44
|
+
* Creates a Flink plugin for GitHub App integration with:
|
|
45
|
+
* - Installation management
|
|
46
|
+
* - JWT-based authentication with private key signing
|
|
47
|
+
* - Installation access token management with automatic refresh and caching
|
|
48
|
+
* - Webhook integration with signature validation
|
|
49
|
+
* - GitHub API client wrapper
|
|
50
|
+
*
|
|
51
|
+
* @param options - GitHub App plugin configuration options
|
|
52
|
+
* @returns FlinkPlugin instance
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { githubAppPlugin } from '@flink-app/github-app-plugin';
|
|
57
|
+
*
|
|
58
|
+
* const app = new FlinkApp({
|
|
59
|
+
* plugins: [
|
|
60
|
+
* githubAppPlugin({
|
|
61
|
+
* appId: process.env.GITHUB_APP_ID!,
|
|
62
|
+
* privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
63
|
+
* webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
64
|
+
* clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
65
|
+
* clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
66
|
+
* onInstallationSuccess: async ({ installationId, repositories, account }, ctx) => {
|
|
67
|
+
* const userId = getLoggedInUserId(req); // App-defined function
|
|
68
|
+
* return {
|
|
69
|
+
* userId,
|
|
70
|
+
* redirectUrl: '/dashboard/repos'
|
|
71
|
+
* };
|
|
72
|
+
* },
|
|
73
|
+
* onWebhookEvent: async ({ event, payload, installationId }, ctx) => {
|
|
74
|
+
* if (event === 'push') {
|
|
75
|
+
* // Process push event
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* })
|
|
79
|
+
* ]
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
function githubAppPlugin(options) {
|
|
84
|
+
// Validate required options
|
|
85
|
+
if (!options.appId) {
|
|
86
|
+
throw new Error("GitHub App Plugin: appId is required");
|
|
87
|
+
}
|
|
88
|
+
if (!options.privateKey) {
|
|
89
|
+
throw new Error("GitHub App Plugin: privateKey is required");
|
|
90
|
+
}
|
|
91
|
+
if (!options.webhookSecret) {
|
|
92
|
+
throw new Error("GitHub App Plugin: webhookSecret is required");
|
|
93
|
+
}
|
|
94
|
+
if (!options.clientId) {
|
|
95
|
+
throw new Error("GitHub App Plugin: clientId is required");
|
|
96
|
+
}
|
|
97
|
+
if (!options.clientSecret) {
|
|
98
|
+
throw new Error("GitHub App Plugin: clientSecret is required");
|
|
99
|
+
}
|
|
100
|
+
if (!options.onInstallationSuccess) {
|
|
101
|
+
throw new Error("GitHub App Plugin: onInstallationSuccess callback is required");
|
|
102
|
+
}
|
|
103
|
+
// Determine configuration defaults
|
|
104
|
+
const baseUrl = options.baseUrl || "https://api.github.com";
|
|
105
|
+
const tokenCacheTTL = options.tokenCacheTTL || 3300; // 55 minutes
|
|
106
|
+
const sessionTTL = options.sessionTTL || 600; // 10 minutes
|
|
107
|
+
const registerRoutes = options.registerRoutes !== false; // default true
|
|
108
|
+
const logWebhookEvents = options.logWebhookEvents || false; // default false
|
|
109
|
+
let flinkApp;
|
|
110
|
+
let authService;
|
|
111
|
+
let webhookValidator;
|
|
112
|
+
let sessionRepo;
|
|
113
|
+
let installationRepo;
|
|
114
|
+
let webhookEventRepo;
|
|
115
|
+
/**
|
|
116
|
+
* Plugin initialization
|
|
117
|
+
*/
|
|
118
|
+
async function init(app, db) {
|
|
119
|
+
flink_1.log.info("Initializing GitHub App Plugin...");
|
|
120
|
+
flinkApp = app;
|
|
121
|
+
try {
|
|
122
|
+
if (!db) {
|
|
123
|
+
throw new Error("GitHub App Plugin: Database connection is required");
|
|
124
|
+
}
|
|
125
|
+
// Initialize GitHubAuthService with private key validation
|
|
126
|
+
// This will throw early if the private key is invalid
|
|
127
|
+
try {
|
|
128
|
+
authService = new GitHubAuthService_1.GitHubAuthService(options.appId, options.privateKey, baseUrl, tokenCacheTTL);
|
|
129
|
+
flink_1.log.info("GitHub App Plugin: Successfully validated private key and generated test JWT");
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
flink_1.log.error("GitHub App Plugin: Failed to initialize auth service", error);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
// Initialize WebhookValidator
|
|
136
|
+
webhookValidator = new WebhookValidator_1.WebhookValidator(options.webhookSecret);
|
|
137
|
+
// Initialize repositories
|
|
138
|
+
const sessionsCollectionName = options.sessionsCollectionName || "github_app_sessions";
|
|
139
|
+
const installationsCollectionName = options.installationsCollectionName || "github_installations";
|
|
140
|
+
const webhookEventsCollectionName = options.webhookEventsCollectionName || "github_webhook_events";
|
|
141
|
+
sessionRepo = new GitHubAppSessionRepo_1.default(flinkApp.ctx, db, sessionsCollectionName);
|
|
142
|
+
installationRepo = new GitHubInstallationRepo_1.default(flinkApp.ctx, db, installationsCollectionName);
|
|
143
|
+
// Add repositories to FlinkApp
|
|
144
|
+
flinkApp.addRepo("githubAppSessionRepo", sessionRepo);
|
|
145
|
+
flinkApp.addRepo("githubInstallationRepo", installationRepo);
|
|
146
|
+
// Conditionally initialize webhook event repo if logging is enabled
|
|
147
|
+
if (logWebhookEvents) {
|
|
148
|
+
webhookEventRepo = new GitHubWebhookEventRepo_1.default(flinkApp.ctx, db, webhookEventsCollectionName);
|
|
149
|
+
flinkApp.addRepo("githubWebhookEventRepo", webhookEventRepo);
|
|
150
|
+
flink_1.log.info(`GitHub App Plugin: Webhook event logging enabled (collection: ${webhookEventsCollectionName})`);
|
|
151
|
+
}
|
|
152
|
+
// Create TTL indexes
|
|
153
|
+
// Sessions TTL index for automatic cleanup
|
|
154
|
+
await db.collection(sessionsCollectionName).createIndex({ createdAt: 1 }, { expireAfterSeconds: sessionTTL });
|
|
155
|
+
flink_1.log.info(`GitHub App Plugin: Created TTL index on ${sessionsCollectionName} with ${sessionTTL}s expiration`);
|
|
156
|
+
// Webhook events TTL index if logging enabled
|
|
157
|
+
if (logWebhookEvents && webhookEventsCollectionName) {
|
|
158
|
+
// Optional TTL for webhook events (default: 30 days)
|
|
159
|
+
const webhookEventTTL = 30 * 24 * 60 * 60; // 30 days in seconds
|
|
160
|
+
await db.collection(webhookEventsCollectionName).createIndex({ createdAt: 1 }, { expireAfterSeconds: webhookEventTTL });
|
|
161
|
+
flink_1.log.info(`GitHub App Plugin: Created TTL index on ${webhookEventsCollectionName} with ${webhookEventTTL}s expiration`);
|
|
162
|
+
}
|
|
163
|
+
// Conditionally register handlers (only GitHub-required handlers)
|
|
164
|
+
if (registerRoutes) {
|
|
165
|
+
flinkApp.addHandler(InstallationCallback);
|
|
166
|
+
flinkApp.addHandler(WebhookHandler);
|
|
167
|
+
flink_1.log.info("GitHub App Plugin: Registered handlers (callback and webhook)");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
flink_1.log.info("GitHub App Plugin: Skipped handler registration (routes disabled)");
|
|
171
|
+
}
|
|
172
|
+
flink_1.log.info(`GitHub App Plugin initialized successfully`);
|
|
173
|
+
flink_1.log.info(` - App ID: ${options.appId}`);
|
|
174
|
+
flink_1.log.info(` - Base URL: ${baseUrl}`);
|
|
175
|
+
flink_1.log.info(` - Token Cache TTL: ${tokenCacheTTL}s`);
|
|
176
|
+
flink_1.log.info(` - Session TTL: ${sessionTTL}s`);
|
|
177
|
+
flink_1.log.info(` - Routes Registered: ${registerRoutes}`);
|
|
178
|
+
flink_1.log.info(` - Webhook Logging: ${logWebhookEvents}`);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
flink_1.log.error("Failed to initialize GitHub App Plugin:", error);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Initiates GitHub App installation flow
|
|
187
|
+
*/
|
|
188
|
+
async function initiateInstallation(params) {
|
|
189
|
+
if (!sessionRepo) {
|
|
190
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
191
|
+
}
|
|
192
|
+
// Validate that appSlug is configured
|
|
193
|
+
if (!options.appSlug) {
|
|
194
|
+
throw new Error("GitHub App Plugin: appSlug is required for installation flow. Please set appSlug in plugin options.");
|
|
195
|
+
}
|
|
196
|
+
// Generate cryptographically secure state and session ID
|
|
197
|
+
const state = (0, state_utils_1.generateState)();
|
|
198
|
+
const sessionId = (0, state_utils_1.generateSessionId)();
|
|
199
|
+
// Store session for state validation in callback
|
|
200
|
+
await sessionRepo.create({
|
|
201
|
+
sessionId,
|
|
202
|
+
state,
|
|
203
|
+
userId: params.userId,
|
|
204
|
+
metadata: params.metadata || {},
|
|
205
|
+
createdAt: new Date(),
|
|
206
|
+
});
|
|
207
|
+
// Build GitHub installation URL
|
|
208
|
+
const installationUrl = `https://github.com/apps/${options.appSlug}/installations/new?state=${state}`;
|
|
209
|
+
flink_1.log.info("GitHub App installation initiated", { userId: params.userId, sessionId });
|
|
210
|
+
return {
|
|
211
|
+
redirectUrl: installationUrl,
|
|
212
|
+
state,
|
|
213
|
+
sessionId,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Uninstalls GitHub App for a user
|
|
218
|
+
*/
|
|
219
|
+
async function uninstall(params) {
|
|
220
|
+
if (!installationRepo || !authService) {
|
|
221
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
// Find installation
|
|
225
|
+
const installation = await installationRepo.findByUserAndInstallationId(params.userId, params.installationId);
|
|
226
|
+
if (!installation) {
|
|
227
|
+
return { success: false, error: "installation-not-found" };
|
|
228
|
+
}
|
|
229
|
+
// Verify ownership
|
|
230
|
+
if (installation.userId !== params.userId) {
|
|
231
|
+
flink_1.log.warn("User attempted to uninstall installation they don't own", {
|
|
232
|
+
userId: params.userId,
|
|
233
|
+
installationId: params.installationId,
|
|
234
|
+
ownerId: installation.userId,
|
|
235
|
+
});
|
|
236
|
+
return { success: false, error: "installation-not-owned" };
|
|
237
|
+
}
|
|
238
|
+
// Delete from database
|
|
239
|
+
await installationRepo.deleteByInstallationId(params.installationId);
|
|
240
|
+
// Clear token cache
|
|
241
|
+
authService.deleteInstallationToken(params.installationId);
|
|
242
|
+
flink_1.log.info("GitHub App uninstalled", { userId: params.userId, installationId: params.installationId });
|
|
243
|
+
return { success: true };
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
flink_1.log.error("Failed to uninstall GitHub App", {
|
|
247
|
+
userId: params.userId,
|
|
248
|
+
installationId: params.installationId,
|
|
249
|
+
error: error.message,
|
|
250
|
+
});
|
|
251
|
+
return { success: false, error: "uninstall-failed" };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get GitHub API client for an installation
|
|
256
|
+
*/
|
|
257
|
+
async function getClient(installationId) {
|
|
258
|
+
if (!authService) {
|
|
259
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
260
|
+
}
|
|
261
|
+
return new GitHubAPIClient_1.GitHubAPIClient(installationId, authService, baseUrl);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get installation for a user (returns first installation)
|
|
265
|
+
*/
|
|
266
|
+
async function getInstallation(userId) {
|
|
267
|
+
if (!installationRepo) {
|
|
268
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
269
|
+
}
|
|
270
|
+
const installations = await installationRepo.findByUserId(userId);
|
|
271
|
+
return installations.length > 0 ? installations[0] : null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get all installations for a user
|
|
275
|
+
*/
|
|
276
|
+
async function getInstallations(userId) {
|
|
277
|
+
if (!installationRepo) {
|
|
278
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
279
|
+
}
|
|
280
|
+
return installationRepo.findByUserId(userId);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Delete installation
|
|
284
|
+
*/
|
|
285
|
+
async function deleteInstallation(userId, installationId) {
|
|
286
|
+
if (!installationRepo || !authService) {
|
|
287
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
288
|
+
}
|
|
289
|
+
// Verify user owns the installation
|
|
290
|
+
const installation = await installationRepo.findByUserAndInstallationId(userId, installationId);
|
|
291
|
+
if (!installation) {
|
|
292
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.INSTALLATION_NOT_OWNED, "Installation not found or not owned by user", { userId, installationId });
|
|
293
|
+
}
|
|
294
|
+
// Delete from database
|
|
295
|
+
await installationRepo.deleteByInstallationId(installationId);
|
|
296
|
+
// Clear token cache for this installation
|
|
297
|
+
authService.deleteInstallationToken(installationId);
|
|
298
|
+
flink_1.log.info(`GitHub App Plugin: Deleted installation ${installationId} for user ${userId}`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if user has access to specific repository
|
|
302
|
+
*/
|
|
303
|
+
async function hasRepositoryAccess(userId, owner, repo) {
|
|
304
|
+
if (!installationRepo) {
|
|
305
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
306
|
+
}
|
|
307
|
+
const installations = await installationRepo.findByUserId(userId);
|
|
308
|
+
// Check if any installation has access to the repository
|
|
309
|
+
for (const installation of installations) {
|
|
310
|
+
// Skip suspended installations
|
|
311
|
+
if (installation.suspendedAt) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// Check repositories array
|
|
315
|
+
const hasAccess = installation.repositories.some((r) => r.fullName.toLowerCase() === `${owner}/${repo}`.toLowerCase());
|
|
316
|
+
if (hasAccess) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get installation access token (for advanced usage)
|
|
324
|
+
*/
|
|
325
|
+
async function getInstallationToken(installationId) {
|
|
326
|
+
if (!authService) {
|
|
327
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
328
|
+
}
|
|
329
|
+
return authService.getInstallationToken(installationId);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Clear token cache
|
|
333
|
+
*/
|
|
334
|
+
function clearTokenCache() {
|
|
335
|
+
if (!authService) {
|
|
336
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
337
|
+
}
|
|
338
|
+
authService.clearTokenCache();
|
|
339
|
+
flink_1.log.info("GitHub App Plugin: Cleared token cache");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Plugin context exposed via ctx.plugins.githubApp
|
|
343
|
+
*/
|
|
344
|
+
const pluginCtx = {
|
|
345
|
+
initiateInstallation,
|
|
346
|
+
uninstall,
|
|
347
|
+
getClient,
|
|
348
|
+
getInstallation,
|
|
349
|
+
getInstallations,
|
|
350
|
+
deleteInstallation,
|
|
351
|
+
hasRepositoryAccess,
|
|
352
|
+
getInstallationToken,
|
|
353
|
+
clearTokenCache,
|
|
354
|
+
options: Object.freeze({ ...options }),
|
|
355
|
+
get authService() { return authService; },
|
|
356
|
+
get webhookValidator() { return webhookValidator; },
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
id: "githubApp",
|
|
360
|
+
db: {
|
|
361
|
+
useHostDb: true,
|
|
362
|
+
},
|
|
363
|
+
ctx: pluginCtx,
|
|
364
|
+
init,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
exports.githubAppPlugin = githubAppPlugin;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { GitHubAPIClient } from "./services/GitHubAPIClient";
|
|
2
|
+
import GitHubInstallation from "./schemas/GitHubInstallation";
|
|
3
|
+
import { GitHubAppPluginOptions } from "./GitHubAppPluginOptions";
|
|
4
|
+
import { GitHubAuthService } from "./services/GitHubAuthService";
|
|
5
|
+
import { WebhookValidator } from "./services/WebhookValidator";
|
|
6
|
+
/**
|
|
7
|
+
* Public context interface exposed via ctx.plugins.githubApp
|
|
8
|
+
*
|
|
9
|
+
* Provides methods for interacting with GitHub App installations,
|
|
10
|
+
* managing installation access, and creating authenticated API clients.
|
|
11
|
+
*/
|
|
12
|
+
export interface GitHubAppPluginContext {
|
|
13
|
+
githubApp: {
|
|
14
|
+
/**
|
|
15
|
+
* Initiates GitHub App installation flow
|
|
16
|
+
*
|
|
17
|
+
* Creates a session with CSRF protection and returns the installation URL.
|
|
18
|
+
* Call this from your own handler with your own auth logic.
|
|
19
|
+
*
|
|
20
|
+
* @param params - Installation parameters
|
|
21
|
+
* @param params.userId - Application user ID to link installation to
|
|
22
|
+
* @param params.redirectUrl - Optional URL to redirect to after installation (deprecated, use onInstallationSuccess callback)
|
|
23
|
+
* @param params.metadata - Optional metadata to store in session
|
|
24
|
+
* @returns Installation flow details
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* // In your custom handler
|
|
29
|
+
* export default async function InitiateGitHubInstall({ ctx }: GetHandlerParams) {
|
|
30
|
+
* const userId = ctx.auth.tokenData.userId;
|
|
31
|
+
*
|
|
32
|
+
* const { redirectUrl } = await ctx.plugins.githubApp.initiateInstallation({
|
|
33
|
+
* userId,
|
|
34
|
+
* metadata: { source: 'settings' }
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* return {
|
|
38
|
+
* status: 302,
|
|
39
|
+
* headers: { Location: redirectUrl }
|
|
40
|
+
* };
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
initiateInstallation(params: {
|
|
45
|
+
userId: string;
|
|
46
|
+
redirectUrl?: string;
|
|
47
|
+
metadata?: Record<string, any>;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
redirectUrl: string;
|
|
50
|
+
state: string;
|
|
51
|
+
sessionId: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Uninstalls GitHub App for a user
|
|
55
|
+
*
|
|
56
|
+
* Removes installation from database and clears cached token.
|
|
57
|
+
* Call this from your own handler with your own auth logic.
|
|
58
|
+
*
|
|
59
|
+
* @param params - Uninstall parameters
|
|
60
|
+
* @param params.userId - Application user ID requesting uninstall
|
|
61
|
+
* @param params.installationId - GitHub installation ID to uninstall
|
|
62
|
+
* @returns Uninstall result
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // In your custom handler
|
|
67
|
+
* export default async function UninstallGitHub({ ctx, params }: DeleteHandlerParams) {
|
|
68
|
+
* const userId = ctx.auth.tokenData.userId;
|
|
69
|
+
*
|
|
70
|
+
* const result = await ctx.plugins.githubApp.uninstall({
|
|
71
|
+
* userId,
|
|
72
|
+
* installationId: params.installationId
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* if (!result.success) {
|
|
76
|
+
* return badRequest(result.error);
|
|
77
|
+
* }
|
|
78
|
+
*
|
|
79
|
+
* return { status: 204 };
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
uninstall(params: {
|
|
84
|
+
userId: string;
|
|
85
|
+
installationId: number;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
success: boolean;
|
|
88
|
+
error?: string;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Get GitHub API client for an installation
|
|
92
|
+
*
|
|
93
|
+
* Creates an authenticated API client with automatic token injection.
|
|
94
|
+
* The client handles token caching, refresh, and retry logic.
|
|
95
|
+
*
|
|
96
|
+
* @param installationId - GitHub installation ID
|
|
97
|
+
* @returns GitHub API client instance
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const client = await ctx.plugins.githubApp.getClient(12345);
|
|
102
|
+
* const repos = await client.getRepositories();
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
getClient(installationId: number): Promise<GitHubAPIClient>;
|
|
106
|
+
/**
|
|
107
|
+
* Get installation for a user
|
|
108
|
+
*
|
|
109
|
+
* Returns the first installation found for the user.
|
|
110
|
+
* Use getInstallations() if a user may have multiple installations.
|
|
111
|
+
*
|
|
112
|
+
* @param userId - Application user ID
|
|
113
|
+
* @returns Installation if found, null otherwise
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const installation = await ctx.plugins.githubApp.getInstallation('user-123');
|
|
118
|
+
* if (installation) {
|
|
119
|
+
* console.log('Installed on:', installation.accountLogin);
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
getInstallation(userId: string): Promise<GitHubInstallation | null>;
|
|
124
|
+
/**
|
|
125
|
+
* Get all installations for a user
|
|
126
|
+
*
|
|
127
|
+
* Returns all GitHub App installations linked to the user.
|
|
128
|
+
* Users can install the app on multiple accounts (personal + organizations).
|
|
129
|
+
*
|
|
130
|
+
* @param userId - Application user ID
|
|
131
|
+
* @returns Array of installations
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const installations = await ctx.plugins.githubApp.getInstallations('user-123');
|
|
136
|
+
* installations.forEach(installation => {
|
|
137
|
+
* console.log(`Installed on ${installation.accountLogin} (${installation.accountType})`);
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
getInstallations(userId: string): Promise<GitHubInstallation[]>;
|
|
142
|
+
/**
|
|
143
|
+
* Delete installation
|
|
144
|
+
*
|
|
145
|
+
* Removes an installation from the database and clears its cached token.
|
|
146
|
+
* Verifies the user owns the installation before deletion.
|
|
147
|
+
*
|
|
148
|
+
* @param userId - Application user ID
|
|
149
|
+
* @param installationId - GitHub installation ID
|
|
150
|
+
* @throws Error if user doesn't own installation
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* await ctx.plugins.githubApp.deleteInstallation('user-123', 12345);
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
deleteInstallation(userId: string, installationId: number): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Check if user has access to specific repository
|
|
160
|
+
*
|
|
161
|
+
* Verifies that the user has an installation with access to the
|
|
162
|
+
* specified repository. Useful for authorization checks.
|
|
163
|
+
*
|
|
164
|
+
* @param userId - Application user ID
|
|
165
|
+
* @param owner - Repository owner (username or org)
|
|
166
|
+
* @param repo - Repository name
|
|
167
|
+
* @returns true if user has access, false otherwise
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```typescript
|
|
171
|
+
* const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(
|
|
172
|
+
* 'user-123',
|
|
173
|
+
* 'facebook',
|
|
174
|
+
* 'react'
|
|
175
|
+
* );
|
|
176
|
+
*
|
|
177
|
+
* if (!hasAccess) {
|
|
178
|
+
* return forbidden('You do not have access to this repository');
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
hasRepositoryAccess(userId: string, owner: string, repo: string): Promise<boolean>;
|
|
183
|
+
/**
|
|
184
|
+
* Get installation access token (for advanced usage)
|
|
185
|
+
*
|
|
186
|
+
* Returns a raw installation access token. Tokens are cached
|
|
187
|
+
* and automatically refreshed when expired.
|
|
188
|
+
*
|
|
189
|
+
* Most users should use getClient() instead, which handles
|
|
190
|
+
* token injection automatically.
|
|
191
|
+
*
|
|
192
|
+
* @param installationId - GitHub installation ID
|
|
193
|
+
* @returns Installation access token
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* const token = await ctx.plugins.githubApp.getInstallationToken(12345);
|
|
198
|
+
* // Make custom API call with token
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
getInstallationToken(installationId: number): Promise<string>;
|
|
202
|
+
/**
|
|
203
|
+
* Clear token cache
|
|
204
|
+
*
|
|
205
|
+
* Removes all cached installation tokens. Next API call will
|
|
206
|
+
* fetch fresh tokens from GitHub.
|
|
207
|
+
*
|
|
208
|
+
* Useful for testing, forcing token refresh, or plugin shutdown.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* ctx.plugins.githubApp.clearTokenCache();
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
clearTokenCache(): void;
|
|
216
|
+
/**
|
|
217
|
+
* Plugin configuration (read-only)
|
|
218
|
+
*
|
|
219
|
+
* Provides access to plugin options for custom logic.
|
|
220
|
+
* Object is frozen to prevent modifications.
|
|
221
|
+
*/
|
|
222
|
+
options: Readonly<GitHubAppPluginOptions>;
|
|
223
|
+
/**
|
|
224
|
+
* GitHub Authentication Service
|
|
225
|
+
*
|
|
226
|
+
* @internal
|
|
227
|
+
* Used internally by plugin handlers. Most applications should not need to access this directly.
|
|
228
|
+
*
|
|
229
|
+
* Provides JWT generation and installation token management.
|
|
230
|
+
*/
|
|
231
|
+
authService: GitHubAuthService;
|
|
232
|
+
/**
|
|
233
|
+
* Webhook Validator
|
|
234
|
+
*
|
|
235
|
+
* @internal
|
|
236
|
+
* Used internally by plugin handlers. Most applications should not need to access this directly.
|
|
237
|
+
*
|
|
238
|
+
* Validates webhook signatures and parses webhook payloads.
|
|
239
|
+
*/
|
|
240
|
+
webhookValidator: WebhookValidator;
|
|
241
|
+
};
|
|
242
|
+
}
|