@flink-app/oauth-plugin 0.12.1-alpha.43 → 0.12.1-alpha.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/oauth-plugin",
3
- "version": "0.12.1-alpha.43",
3
+ "version": "0.12.1-alpha.45",
4
4
  "description": "Flink plugin for OAuth 2.0 authentication with GitHub and Google providers",
5
5
  "scripts": {
6
6
  "test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
@@ -19,9 +19,9 @@
19
19
  "mongodb": "^6.15.0"
20
20
  },
21
21
  "devDependencies": {
22
- "@flink-app/flink": "^0.12.1-alpha.40",
23
- "@flink-app/jwt-auth-plugin": "^0.12.1-alpha.43",
24
- "@flink-app/test-utils": "^0.12.1-alpha.40",
22
+ "@flink-app/flink": "^0.12.1-alpha.45",
23
+ "@flink-app/jwt-auth-plugin": "^0.12.1-alpha.45",
24
+ "@flink-app/test-utils": "^0.12.1-alpha.45",
25
25
  "@types/jasmine": "^3.7.1",
26
26
  "@types/jwt-simple": "^0.5.36",
27
27
  "@types/node": "22.13.10",
@@ -34,5 +34,5 @@
34
34
  "tsc-watch": "^4.2.9",
35
35
  "typescript": "5.4.5"
36
36
  },
37
- "gitHead": "e5fc78243a97075ce0272f287f3f89fd44681715"
37
+ "gitHead": "af426a157217c110ac9c7beb48e2e746968bec33"
38
38
  }
@@ -8,10 +8,8 @@
8
8
  * Once Task Groups 1-6 are implemented, these tests should be completed with actual assertions.
9
9
  */
10
10
 
11
- import { createMockJwtAuthPlugin, decodeTestToken } from "./helpers/mockJwtAuthPlugin";
12
- import { createMockGitHubProfile, createMockGoogleProfile, createMockTokenResponse } from "./helpers/mockOAuthProviders";
13
- import { beforeEachTest, afterAllTests } from "./helpers/testDatabase";
14
- import { extractTokenFromUrl } from "./helpers/testHelpers";
11
+ import { createMockJwtAuthPlugin } from "./helpers/mockJwtAuthPlugin";
12
+ import { afterAllTests, beforeEachTest } from "./helpers/testDatabase";
15
13
 
16
14
  describe("OAuth Plugin Integration Tests", () => {
17
15
  let mockJwtAuth: any;
@@ -1,1383 +0,0 @@
1
- # Product Requirements Document: GitHub App Plugin
2
-
3
- **Version:** 1.0
4
- **Date:** 2025-10-26
5
- **Status:** Draft
6
- **Package Name:** `@flink-app/github-app-plugin`
7
-
8
- ---
9
-
10
- ## Table of Contents
11
-
12
- 1. [Overview](#overview)
13
- 2. [Background & Motivation](#background--motivation)
14
- 3. [Goals & Non-Goals](#goals--non-goals)
15
- 4. [Architecture](#architecture)
16
- 5. [GitHub App Authentication Flow](#github-app-authentication-flow)
17
- 6. [Plugin API Specification](#plugin-api-specification)
18
- 7. [Configuration Options](#configuration-options)
19
- 8. [Data Models](#data-models)
20
- 9. [HTTP Handlers](#http-handlers)
21
- 10. [Repositories](#repositories)
22
- 11. [Security Considerations](#security-considerations)
23
- 12. [Context API](#context-api)
24
- 13. [Error Handling](#error-handling)
25
- 14. [Integration with OAuth Plugin](#integration-with-oauth-plugin)
26
- 15. [Examples & Use Cases](#examples--use-cases)
27
- 16. [Development Tasks](#development-tasks)
28
- 17. [Testing Requirements](#testing-requirements)
29
- 18. [Documentation Requirements](#documentation-requirements)
30
-
31
- ---
32
-
33
- ## Overview
34
-
35
- The GitHub App Plugin is a Flink plugin that enables GitHub App integration for fine-grained repository access. Unlike OAuth Apps which require all-or-nothing repo access, GitHub Apps allow users to selectively grant access to specific repositories.
36
-
37
- This plugin complements the existing OAuth Plugin by handling repository-level operations while OAuth Plugin continues to handle social login/authentication.
38
-
39
- ### Key Features
40
-
41
- - GitHub App installation flow with repository selection
42
- - JWT-based authentication with private key signing
43
- - Installation access token management with automatic refresh
44
- - Fine-grained repository permissions
45
- - Webhook integration support
46
- - Higher API rate limits (15,000 requests/hour)
47
- - Support for organization and user installations
48
- - Encrypted private key storage
49
- - Repository permission verification
50
-
51
- ---
52
-
53
- ## Background & Motivation
54
-
55
- ### Problem Statement
56
-
57
- The OAuth Plugin uses GitHub OAuth Apps which have limitations:
58
- - Users must grant access to ALL repositories or none
59
- - Lower API rate limits (5,000 requests/hour)
60
- - No webhook integration
61
- - Cannot act as a bot/service account
62
- - Broad permission model
63
-
64
- ### Solution
65
-
66
- Implement a separate GitHub App plugin that:
67
- - Allows users to select specific repositories during installation
68
- - Provides higher rate limits for API operations
69
- - Enables webhook integration for real-time events
70
- - Supports fine-grained permissions per repository
71
- - Can be installed at organization or user level
72
-
73
- ### Why Separate Plugin?
74
-
75
- GitHub Apps use a **non-OAuth authentication flow** that is incompatible with the OAuth 2.0 standard:
76
- - JWT signing with RSA private keys (not client secrets)
77
- - Installation-based token model (not user-based)
78
- - Different endpoints and flows
79
- - Installation ID tracking required
80
-
81
- ---
82
-
83
- ## Goals & Non-Goals
84
-
85
- ### Goals
86
-
87
- ✅ Enable fine-grained repository access for Flink applications
88
- ✅ Follow Flink plugin architecture patterns (matching OAuth Plugin)
89
- ✅ Support both user and organization installations
90
- ✅ Provide webhook endpoint handling
91
- ✅ Implement secure private key storage and JWT signing
92
- ✅ Auto-refresh installation access tokens
93
- ✅ Integrate seamlessly with JWT Auth Plugin for user linking
94
- ✅ Provide comprehensive error handling
95
- ✅ Support multiple GitHub App configurations (multi-tenant)
96
-
97
- ### Non-Goals
98
-
99
- ❌ Replace OAuth Plugin for social login (OAuth Plugin handles this)
100
- ❌ Support other Git providers (GitHub-specific)
101
- ❌ Implement GitHub Actions or CI/CD features
102
- ❌ Provide repository cloning/Git operations (app-level concern)
103
- ❌ Handle GitHub Packages or GitHub Container Registry
104
-
105
- ---
106
-
107
- ## Architecture
108
-
109
- ### Plugin Structure
110
-
111
- Following the OAuth Plugin pattern:
112
-
113
- ```
114
- packages/github-app-plugin/
115
- ├── src/
116
- │ ├── GitHubAppPlugin.ts # Plugin factory function
117
- │ ├── GitHubAppPluginOptions.ts # Configuration interface
118
- │ ├── GitHubAppPluginContext.ts # Public context interface
119
- │ ├── GitHubAppInternalContext.ts # Internal context interface
120
- │ │
121
- │ ├── handlers/
122
- │ │ ├── InitiateInstallation.ts # GET /github-app/install
123
- │ │ ├── InstallationCallback.ts # GET /github-app/callback
124
- │ │ ├── WebhookHandler.ts # POST /github-app/webhook
125
- │ │ └── UninstallHandler.ts # DELETE /github-app/installation/:id
126
- │ │
127
- │ ├── repos/
128
- │ │ ├── GitHubInstallationRepo.ts # Installation storage
129
- │ │ └── GitHubWebhookEventRepo.ts # Webhook event log (optional)
130
- │ │
131
- │ ├── schemas/
132
- │ │ ├── GitHubInstallation.ts # Installation model
133
- │ │ ├── WebhookEvent.ts # Webhook event model
134
- │ │ ├── InstallationCallbackRequest.ts
135
- │ │ └── WebhookPayload.ts
136
- │ │
137
- │ ├── services/
138
- │ │ ├── GitHubAuthService.ts # JWT signing and token management
139
- │ │ ├── GitHubAPIClient.ts # API client wrapper
140
- │ │ └── WebhookValidator.ts # Webhook signature validation
141
- │ │
142
- │ ├── utils/
143
- │ │ ├── jwt-utils.ts # JWT signing with private key
144
- │ │ ├── token-cache-utils.ts # Installation token caching
145
- │ │ ├── encryption-utils.ts # Private key encryption (reuse from oauth)
146
- │ │ ├── webhook-signature-utils.ts # HMAC signature validation
147
- │ │ └── error-utils.ts # Error handling
148
- │ │
149
- │ └── index.ts # Exports
150
-
151
- ├── spec/ # Tests
152
- ├── examples/ # Usage examples
153
- ├── README.md
154
- ├── SECURITY.md
155
- ├── package.json
156
- └── tsconfig.json
157
- ```
158
-
159
- ### Component Responsibilities
160
-
161
- | Component | Responsibility |
162
- |-----------|----------------|
163
- | **GitHubAppPlugin** | Plugin initialization, handler registration, context setup |
164
- | **GitHubAuthService** | JWT generation, token exchange, token caching |
165
- | **GitHubAPIClient** | Wrapper for GitHub API calls with automatic authentication |
166
- | **WebhookValidator** | Validate webhook signatures and parse payloads |
167
- | **Repositories** | MongoDB storage for installations and webhook events |
168
- | **Handlers** | HTTP endpoints for installation flow and webhooks |
169
-
170
- ---
171
-
172
- ## GitHub App Authentication Flow
173
-
174
- ### 1. Installation Flow
175
-
176
- ```
177
- User clicks "Install GitHub App"
178
-
179
- GET /github-app/install?user_id={userId}
180
-
181
- Redirect to: https://github.com/apps/{app_slug}/installations/new
182
-
183
- User selects repositories to grant access
184
-
185
- GitHub redirects to: /github-app/callback?installation_id=123&setup_action=install&state={state}
186
-
187
- Validate state parameter (CSRF protection)
188
-
189
- Call onInstallationSuccess callback
190
-
191
- Store installation_id linked to user_id
192
-
193
- Redirect to app dashboard
194
- ```
195
-
196
- ### 2. API Access Flow
197
-
198
- ```
199
- App needs to access user's repos
200
-
201
- Get installation_id from database
202
-
203
- Check token cache for valid installation token
204
-
205
- If expired or missing:
206
- ├── Generate JWT signed with app private key (RS256)
207
- ├── Exchange JWT for installation access token
208
- └── Cache token (expires in 1 hour)
209
-
210
- Use installation token to call GitHub API
211
-
212
- Return data to application
213
- ```
214
-
215
- ### 3. Webhook Flow
216
-
217
- ```
218
- GitHub sends webhook event
219
-
220
- POST /github-app/webhook
221
-
222
- Validate X-Hub-Signature-256 header
223
-
224
- Parse webhook payload
225
-
226
- Call onWebhookEvent callback with event data
227
-
228
- Application processes event
229
-
230
- Return 200 OK to GitHub
231
- ```
232
-
233
- ---
234
-
235
- ## Plugin API Specification
236
-
237
- ### Factory Function
238
-
239
- ```typescript
240
- import { githubAppPlugin } from '@flink-app/github-app-plugin';
241
-
242
- const plugin = githubAppPlugin({
243
- appId: string;
244
- privateKey: string;
245
- webhookSecret: string;
246
- clientId: string;
247
- clientSecret: string;
248
- appSlug?: string;
249
-
250
- onInstallationSuccess: (params: InstallationSuccessParams, ctx: Context) => Promise<InstallationSuccessResponse>;
251
- onInstallationError?: (params: InstallationErrorParams) => Promise<InstallationErrorResponse>;
252
- onWebhookEvent?: (params: WebhookEventParams, ctx: Context) => Promise<void>;
253
-
254
- installationsCollectionName?: string;
255
- webhookEventsCollectionName?: string;
256
- tokenCacheTTL?: number;
257
- registerRoutes?: boolean;
258
- });
259
- ```
260
-
261
- ### Callback Interfaces
262
-
263
- ```typescript
264
- interface InstallationSuccessParams {
265
- installationId: number;
266
- repositories: Array<{
267
- id: number;
268
- name: string;
269
- full_name: string;
270
- private: boolean;
271
- }>;
272
- account: {
273
- id: number;
274
- login: string;
275
- type: 'User' | 'Organization';
276
- avatar_url: string;
277
- };
278
- permissions: Record<string, string>;
279
- events: string[];
280
- }
281
-
282
- interface InstallationSuccessResponse {
283
- userId: string;
284
- redirectUrl?: string;
285
- }
286
-
287
- interface WebhookEventParams {
288
- event: string;
289
- action?: string;
290
- installationId: number;
291
- payload: any;
292
- }
293
- ```
294
-
295
- ---
296
-
297
- ## Configuration Options
298
-
299
- ### GitHubAppPluginOptions
300
-
301
- | Option | Type | Required | Default | Description |
302
- |--------|------|----------|---------|-------------|
303
- | `appId` | `string` | Yes | - | GitHub App ID |
304
- | `privateKey` | `string` | Yes | - | RSA private key (PEM format) |
305
- | `webhookSecret` | `string` | Yes | - | Webhook secret for signature validation |
306
- | `clientId` | `string` | Yes | - | GitHub App client ID |
307
- | `clientSecret` | `string` | Yes | - | GitHub App client secret |
308
- | `appSlug` | `string` | No | - | GitHub App slug (auto-detected if not provided) |
309
- | `onInstallationSuccess` | `Function` | Yes | - | Callback after successful installation |
310
- | `onInstallationError` | `Function` | No | - | Callback on installation errors |
311
- | `onWebhookEvent` | `Function` | No | - | Callback for webhook events |
312
- | `installationsCollectionName` | `string` | No | `'github_installations'` | MongoDB collection for installations |
313
- | `webhookEventsCollectionName` | `string` | No | `'github_webhook_events'` | MongoDB collection for webhook logs |
314
- | `tokenCacheTTL` | `number` | No | `3300` | Token cache TTL in seconds (55 min) |
315
- | `registerRoutes` | `boolean` | No | `true` | Auto-register HTTP handlers |
316
-
317
- ### Environment Variables
318
-
319
- ```bash
320
- # GitHub App credentials
321
- GITHUB_APP_ID=123456
322
- GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
323
- GITHUB_APP_WEBHOOK_SECRET=your_webhook_secret
324
- GITHUB_APP_CLIENT_ID=Iv1.abc123
325
- GITHUB_APP_CLIENT_SECRET=your_client_secret
326
-
327
- # Optional
328
- GITHUB_APP_SLUG=my-app-slug
329
- ```
330
-
331
- ---
332
-
333
- ## Data Models
334
-
335
- ### GitHubInstallation
336
-
337
- ```typescript
338
- interface GitHubInstallation {
339
- _id?: string;
340
- userId: string; // Link to application user
341
- installationId: number; // GitHub installation ID
342
- accountId: number; // GitHub account ID
343
- accountLogin: string; // GitHub username or org name
344
- accountType: 'User' | 'Organization';
345
- avatarUrl: string;
346
-
347
- repositories: Array<{
348
- id: number;
349
- name: string;
350
- fullName: string;
351
- private: boolean;
352
- }>;
353
-
354
- permissions: Record<string, string>; // e.g., { contents: 'read', issues: 'write' }
355
- events: string[]; // Webhook events subscribed
356
-
357
- suspendedAt?: Date; // If installation suspended
358
- suspendedBy?: {
359
- id: number;
360
- login: string;
361
- };
362
-
363
- createdAt: Date;
364
- updatedAt: Date;
365
- }
366
- ```
367
-
368
- ### WebhookEvent (Optional - for logging)
369
-
370
- ```typescript
371
- interface WebhookEvent {
372
- _id?: string;
373
- installationId: number;
374
- event: string; // e.g., 'push', 'pull_request'
375
- action?: string; // e.g., 'opened', 'closed'
376
- deliveryId: string; // X-GitHub-Delivery header
377
- payload: any; // Full webhook payload
378
- processed: boolean;
379
- processedAt?: Date;
380
- error?: string;
381
- createdAt: Date;
382
- }
383
- ```
384
-
385
- ---
386
-
387
- ## HTTP Handlers
388
-
389
- ### 1. InitiateInstallation Handler
390
-
391
- **Route:** `GET /github-app/install`
392
-
393
- **Query Parameters:**
394
- - `user_id` (required) - Application user ID to link installation
395
-
396
- **Flow:**
397
- 1. Generate secure state parameter for CSRF protection
398
- 2. Store state in session/cache with user_id
399
- 3. Build GitHub App installation URL
400
- 4. Redirect user to GitHub
401
-
402
- **Implementation:**
403
- ```typescript
404
- export const Route: RouteProps = {
405
- path: '/github-app/install',
406
- method: HttpMethod.get,
407
- };
408
-
409
- const InitiateInstallation: GetHandler = async ({ ctx, req }) => {
410
- const { user_id } = req.query;
411
-
412
- // Validate user exists
413
- // Generate state
414
- // Store session
415
- // Redirect to GitHub
416
-
417
- return {
418
- status: 302,
419
- headers: {
420
- Location: `https://github.com/apps/${appSlug}/installations/new?state=${state}`
421
- },
422
- data: {}
423
- };
424
- };
425
- ```
426
-
427
- ### 2. InstallationCallback Handler
428
-
429
- **Route:** `GET /github-app/callback`
430
-
431
- **Query Parameters:**
432
- - `installation_id` - GitHub installation ID
433
- - `setup_action` - 'install', 'update', or 'request'
434
- - `state` - CSRF protection token
435
- - `code` (optional) - Authorization code (if requesting user token)
436
-
437
- **Flow:**
438
- 1. Validate state parameter
439
- 2. Exchange code for user access token (if present)
440
- 3. Fetch installation details from GitHub API
441
- 4. Call `onInstallationSuccess` callback
442
- 5. Store installation in database
443
- 6. Redirect to app
444
-
445
- **Implementation:**
446
- ```typescript
447
- export const Route: RouteProps = {
448
- path: '/github-app/callback',
449
- method: HttpMethod.get,
450
- };
451
-
452
- const InstallationCallback: GetHandler = async ({ ctx, req }) => {
453
- const { installation_id, setup_action, state, code } = req.query;
454
-
455
- // Validate state
456
- // Fetch installation details
457
- // Call onInstallationSuccess
458
- // Store installation
459
- // Redirect
460
- };
461
- ```
462
-
463
- ### 3. WebhookHandler
464
-
465
- **Route:** `POST /github-app/webhook`
466
-
467
- **Headers:**
468
- - `X-GitHub-Event` - Event type
469
- - `X-GitHub-Delivery` - Unique delivery ID
470
- - `X-Hub-Signature-256` - HMAC signature for validation
471
-
472
- **Flow:**
473
- 1. Validate webhook signature
474
- 2. Parse payload
475
- 3. Optionally log webhook event
476
- 4. Call `onWebhookEvent` callback
477
- 5. Return 200 OK
478
-
479
- **Implementation:**
480
- ```typescript
481
- export const Route: RouteProps = {
482
- path: '/github-app/webhook',
483
- method: HttpMethod.post,
484
- };
485
-
486
- const WebhookHandler: PostHandler = async ({ ctx, req }) => {
487
- // Validate signature
488
- // Parse payload
489
- // Call onWebhookEvent
490
- // Return 200
491
- };
492
- ```
493
-
494
- ### 4. UninstallHandler (Optional)
495
-
496
- **Route:** `DELETE /github-app/installation/:installationId`
497
-
498
- **Auth:** Requires JWT authentication
499
-
500
- **Flow:**
501
- 1. Verify user owns installation
502
- 2. Delete from database
503
- 3. Optionally revoke on GitHub (requires API call)
504
-
505
- ---
506
-
507
- ## Repositories
508
-
509
- ### GitHubInstallationRepo
510
-
511
- ```typescript
512
- class GitHubInstallationRepo extends FlinkRepo<any, GitHubInstallation> {
513
- /**
514
- * Find installation by user ID and installation ID
515
- */
516
- async findByUserAndInstallationId(
517
- userId: string,
518
- installationId: number
519
- ): Promise<GitHubInstallation | null>;
520
-
521
- /**
522
- * Find all installations for a user
523
- */
524
- async findByUserId(userId: string): Promise<GitHubInstallation[]>;
525
-
526
- /**
527
- * Find installation by installation ID only
528
- */
529
- async findByInstallationId(installationId: number): Promise<GitHubInstallation | null>;
530
-
531
- /**
532
- * Update repositories for an installation
533
- */
534
- async updateRepositories(
535
- installationId: number,
536
- repositories: Array<{ id: number; name: string; fullName: string; private: boolean }>
537
- ): Promise<void>;
538
-
539
- /**
540
- * Mark installation as suspended
541
- */
542
- async suspend(installationId: number, suspendedBy: { id: number; login: string }): Promise<void>;
543
-
544
- /**
545
- * Delete installation
546
- */
547
- async deleteByInstallationId(installationId: number): Promise<number>;
548
- }
549
- ```
550
-
551
- ### GitHubWebhookEventRepo (Optional)
552
-
553
- ```typescript
554
- class GitHubWebhookEventRepo extends FlinkRepo<any, WebhookEvent> {
555
- /**
556
- * Find unprocessed webhook events
557
- */
558
- async findUnprocessed(): Promise<WebhookEvent[]>;
559
-
560
- /**
561
- * Mark event as processed
562
- */
563
- async markProcessed(eventId: string): Promise<void>;
564
-
565
- /**
566
- * Find events by installation ID
567
- */
568
- async findByInstallationId(installationId: number): Promise<WebhookEvent[]>;
569
- }
570
- ```
571
-
572
- ---
573
-
574
- ## Security Considerations
575
-
576
- ### Private Key Management
577
-
578
- 1. **Storage:**
579
- - Store private key in environment variables
580
- - Encrypt at rest if storing in database
581
- - Use PEM format (PKCS#1 or PKCS#8)
582
-
583
- 2. **Validation:**
584
- - Validate PEM format on plugin initialization
585
- - Verify key can sign JWTs successfully
586
- - Check key permissions (should be 600 or stricter)
587
-
588
- 3. **Best Practices:**
589
- - Never commit private key to version control
590
- - Rotate keys periodically
591
- - Use secret management services in production
592
-
593
- ### JWT Signing
594
-
595
- ```typescript
596
- // RS256 algorithm with private key
597
- const jwt = sign({
598
- iat: Math.floor(Date.now() / 1000),
599
- exp: Math.floor(Date.now() / 1000) + 600, // 10 minutes
600
- iss: appId
601
- }, privateKey, {
602
- algorithm: 'RS256'
603
- });
604
- ```
605
-
606
- ### Webhook Signature Validation
607
-
608
- ```typescript
609
- function validateWebhookSignature(
610
- payload: string,
611
- signature: string,
612
- secret: string
613
- ): boolean {
614
- const hmac = crypto.createHmac('sha256', secret);
615
- const digest = 'sha256=' + hmac.update(payload).digest('hex');
616
- return crypto.timingSafeEqual(
617
- Buffer.from(signature),
618
- Buffer.from(digest)
619
- );
620
- }
621
- ```
622
-
623
- ### CSRF Protection
624
-
625
- - Use state parameter in installation flow (similar to OAuth)
626
- - Store state in MongoDB session with TTL
627
- - Validate state on callback using constant-time comparison
628
-
629
- ### Installation Token Security
630
-
631
- - Cache installation tokens in memory (not database)
632
- - Tokens expire after 1 hour
633
- - Never expose tokens to client
634
- - Use HTTPS for all API calls
635
-
636
- ---
637
-
638
- ## Context API
639
-
640
- ### Plugin Context
641
-
642
- The plugin exposes methods via `ctx.plugins.githubApp`:
643
-
644
- ```typescript
645
- interface GitHubAppPluginContext {
646
- githubApp: {
647
- /**
648
- * Get GitHub API client for an installation
649
- */
650
- getClient(installationId: number): Promise<GitHubAPIClient>;
651
-
652
- /**
653
- * Get installation for a user
654
- */
655
- getInstallation(userId: string): Promise<GitHubInstallation | null>;
656
-
657
- /**
658
- * Get all installations for a user
659
- */
660
- getInstallations(userId: string): Promise<GitHubInstallation[]>;
661
-
662
- /**
663
- * Delete installation
664
- */
665
- deleteInstallation(userId: string, installationId: number): Promise<void>;
666
-
667
- /**
668
- * Check if user has access to specific repository
669
- */
670
- hasRepositoryAccess(
671
- userId: string,
672
- owner: string,
673
- repo: string
674
- ): Promise<boolean>;
675
-
676
- /**
677
- * Get installation access token (for advanced usage)
678
- */
679
- getInstallationToken(installationId: number): Promise<string>;
680
-
681
- /**
682
- * Plugin options (read-only)
683
- */
684
- options: Readonly<GitHubAppPluginOptions>;
685
- };
686
- }
687
- ```
688
-
689
- ### GitHubAPIClient
690
-
691
- Wrapper for GitHub API calls with automatic authentication:
692
-
693
- ```typescript
694
- class GitHubAPIClient {
695
- /**
696
- * Get repositories accessible by this installation
697
- */
698
- async getRepositories(): Promise<Repository[]>;
699
-
700
- /**
701
- * Get repository details
702
- */
703
- async getRepository(owner: string, repo: string): Promise<Repository>;
704
-
705
- /**
706
- * List repository contents
707
- */
708
- async getContents(owner: string, repo: string, path: string): Promise<Content[]>;
709
-
710
- /**
711
- * Create an issue
712
- */
713
- async createIssue(
714
- owner: string,
715
- repo: string,
716
- params: { title: string; body: string }
717
- ): Promise<Issue>;
718
-
719
- /**
720
- * Generic API call
721
- */
722
- async request(
723
- method: 'GET' | 'POST' | 'PUT' | 'DELETE',
724
- endpoint: string,
725
- data?: any
726
- ): Promise<any>;
727
- }
728
- ```
729
-
730
- ---
731
-
732
- ## Error Handling
733
-
734
- ### Error Codes
735
-
736
- ```typescript
737
- export const GitHubAppErrorCodes = {
738
- INVALID_STATE: 'invalid_state',
739
- INSTALLATION_NOT_FOUND: 'installation_not_found',
740
- INVALID_PRIVATE_KEY: 'invalid_private_key',
741
- JWT_SIGNING_FAILED: 'jwt_signing_failed',
742
- TOKEN_EXCHANGE_FAILED: 'token_exchange_failed',
743
- WEBHOOK_SIGNATURE_INVALID: 'webhook_signature_invalid',
744
- REPOSITORY_NOT_ACCESSIBLE: 'repository_not_accessible',
745
- INSTALLATION_SUSPENDED: 'installation_suspended',
746
- API_RATE_LIMIT: 'api_rate_limit',
747
- NETWORK_ERROR: 'network_error',
748
- } as const;
749
- ```
750
-
751
- ### Error Structure
752
-
753
- ```typescript
754
- interface GitHubAppError {
755
- code: string;
756
- message: string;
757
- details?: any;
758
- }
759
- ```
760
-
761
- ### Error Handling in Callbacks
762
-
763
- ```typescript
764
- onInstallationError: async ({ error, installationId }) => {
765
- console.error(`Installation error ${installationId}:`, error);
766
-
767
- return {
768
- redirectUrl: `/dashboard?error=${error.code}`
769
- };
770
- }
771
- ```
772
-
773
- ---
774
-
775
- ## Integration with OAuth Plugin
776
-
777
- ### Complementary Usage
778
-
779
- The GitHub App Plugin works **alongside** the OAuth Plugin:
780
-
781
- ```typescript
782
- const app = new FlinkApp<Context>({
783
- auth: jwtAuthPlugin({ /* ... */ }),
784
-
785
- plugins: [
786
- // OAuth Plugin for social login
787
- oauthPlugin({
788
- providers: {
789
- github: {
790
- scope: ['user:email'], // Just authentication
791
- }
792
- },
793
- storeTokens: false,
794
- onAuthSuccess: async ({ profile }, ctx) => {
795
- // Create user account
796
- // Generate JWT token
797
- }
798
- }),
799
-
800
- // GitHub App Plugin for repo access
801
- githubAppPlugin({
802
- appId: process.env.GITHUB_APP_ID!,
803
- privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
804
- webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
805
- clientId: process.env.GITHUB_APP_CLIENT_ID!,
806
- clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
807
-
808
- onInstallationSuccess: async ({ installationId, repositories }, ctx) => {
809
- // Get current authenticated user
810
- const userId = ctx.auth.tokenData.userId;
811
-
812
- return {
813
- userId,
814
- redirectUrl: '/dashboard/repos'
815
- };
816
- },
817
-
818
- onWebhookEvent: async ({ event, payload }, ctx) => {
819
- // Handle push events, PR events, etc.
820
- if (event === 'push') {
821
- // Process push webhook
822
- }
823
- }
824
- })
825
- ]
826
- });
827
- ```
828
-
829
- ### User Flow
830
-
831
- 1. User signs up/logs in via **OAuth Plugin** (social login)
832
- 2. User navigates to "Connect Repositories" in dashboard
833
- 3. User clicks "Install GitHub App"
834
- 4. **GitHub App Plugin** handles installation and repo selection
835
- 5. Application can now access selected repos via GitHub API
836
- 6. Webhooks keep application in sync with repo events
837
-
838
- ---
839
-
840
- ## Examples & Use Cases
841
-
842
- ### Example 1: Basic Installation
843
-
844
- ```typescript
845
- import { githubAppPlugin } from '@flink-app/github-app-plugin';
846
-
847
- const plugin = githubAppPlugin({
848
- appId: process.env.GITHUB_APP_ID!,
849
- privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
850
- webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
851
- clientId: process.env.GITHUB_APP_CLIENT_ID!,
852
- clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
853
-
854
- onInstallationSuccess: async ({ installationId, repositories, account }, ctx) => {
855
- console.log(`Installed to ${account.login} with ${repositories.length} repos`);
856
-
857
- // Get current user from JWT auth
858
- const userId = ctx.auth.tokenData.userId;
859
-
860
- return {
861
- userId,
862
- redirectUrl: '/dashboard'
863
- };
864
- }
865
- });
866
- ```
867
-
868
- ### Example 2: Accessing Repositories
869
-
870
- ```typescript
871
- // In a handler
872
- const GetUserRepos: GetHandler = async ({ ctx, auth }) => {
873
- const userId = auth.tokenData.userId;
874
-
875
- // Get GitHub App client
876
- const installation = await ctx.plugins.githubApp.getInstallation(userId);
877
-
878
- if (!installation) {
879
- return badRequest('GitHub App not installed');
880
- }
881
-
882
- const client = await ctx.plugins.githubApp.getClient(installation.installationId);
883
- const repos = await client.getRepositories();
884
-
885
- return { status: 200, data: repos };
886
- };
887
- ```
888
-
889
- ### Example 3: Webhook Handling
890
-
891
- ```typescript
892
- githubAppPlugin({
893
- // ... config
894
-
895
- onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
896
- switch (event) {
897
- case 'push':
898
- // Handle push events
899
- await ctx.repos.commitRepo.create({
900
- installationId,
901
- repository: payload.repository.full_name,
902
- commits: payload.commits,
903
- pusher: payload.pusher
904
- });
905
- break;
906
-
907
- case 'pull_request':
908
- if (action === 'opened') {
909
- // Handle new PR
910
- await ctx.repos.prRepo.create({
911
- installationId,
912
- prNumber: payload.pull_request.number,
913
- title: payload.pull_request.title
914
- });
915
- }
916
- break;
917
-
918
- case 'installation':
919
- if (action === 'deleted') {
920
- // Handle uninstallation
921
- await ctx.plugins.githubApp.deleteInstallation(installationId);
922
- }
923
- break;
924
- }
925
- }
926
- });
927
- ```
928
-
929
- ### Example 4: Creating Issues
930
-
931
- ```typescript
932
- const CreateIssue: PostHandler = async ({ ctx, auth, req }) => {
933
- const { owner, repo, title, body } = req.body;
934
- const userId = auth.tokenData.userId;
935
-
936
- // Verify user has access to this repo
937
- const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(
938
- userId,
939
- owner,
940
- repo
941
- );
942
-
943
- if (!hasAccess) {
944
- return forbidden('No access to this repository');
945
- }
946
-
947
- // Get client and create issue
948
- const installation = await ctx.plugins.githubApp.getInstallation(userId);
949
- const client = await ctx.plugins.githubApp.getClient(installation!.installationId);
950
-
951
- const issue = await client.createIssue(owner, repo, { title, body });
952
-
953
- return { status: 201, data: issue };
954
- };
955
- ```
956
-
957
- ---
958
-
959
- ## Development Tasks
960
-
961
- ### Phase 1: Core Infrastructure (Week 1-2)
962
-
963
- **Task 1.1: Project Setup**
964
- - [ ] Create package structure in `packages/github-app-plugin/`
965
- - [ ] Set up TypeScript configuration
966
- - [ ] Configure build scripts
967
- - [ ] Set up Jest/Jasmine for testing
968
- - [ ] Create README.md skeleton
969
-
970
- **Task 1.2: Data Models & Schemas**
971
- - [ ] Implement `GitHubInstallation` interface
972
- - [ ] Implement `WebhookEvent` interface
973
- - [ ] Implement `InstallationCallbackRequest` interface
974
- - [ ] Implement `WebhookPayload` interface
975
- - [ ] Add schema validation (optional)
976
-
977
- **Task 1.3: Utilities**
978
- - [ ] Implement `jwt-utils.ts` (JWT signing with RS256)
979
- - [ ] Implement `token-cache-utils.ts` (in-memory token cache)
980
- - [ ] Implement `webhook-signature-utils.ts` (HMAC validation)
981
- - [ ] Implement `error-utils.ts` (error codes and handlers)
982
- - [ ] Add encryption utilities (reuse from oauth-plugin)
983
-
984
- ### Phase 2: Authentication & Token Management (Week 2-3)
985
-
986
- **Task 2.1: GitHub Auth Service**
987
- - [ ] Implement JWT generation with private key
988
- - [ ] Implement installation token exchange
989
- - [ ] Implement token caching with TTL
990
- - [ ] Implement token refresh logic
991
- - [ ] Add error handling for auth failures
992
-
993
- **Task 2.2: GitHub API Client**
994
- - [ ] Implement base API client class
995
- - [ ] Add automatic token injection
996
- - [ ] Implement common API methods (repos, issues, contents)
997
- - [ ] Add rate limit handling
998
- - [ ] Add retry logic with exponential backoff
999
-
1000
- **Task 2.3: Private Key Validation**
1001
- - [ ] Validate PEM format on initialization
1002
- - [ ] Test JWT signing on startup
1003
- - [ ] Add helpful error messages for invalid keys
1004
- - [ ] Support both PKCS#1 and PKCS#8 formats
1005
-
1006
- ### Phase 3: HTTP Handlers (Week 3-4)
1007
-
1008
- **Task 3.1: Installation Initiation Handler**
1009
- - [ ] Implement `InitiateInstallation.ts`
1010
- - [ ] Add state generation (CSRF protection)
1011
- - [ ] Store session in MongoDB
1012
- - [ ] Build GitHub installation URL
1013
- - [ ] Add error handling
1014
-
1015
- **Task 3.2: Installation Callback Handler**
1016
- - [ ] Implement `InstallationCallback.ts`
1017
- - [ ] Validate state parameter
1018
- - [ ] Fetch installation details from GitHub API
1019
- - [ ] Call `onInstallationSuccess` callback
1020
- - [ ] Store installation in database
1021
- - [ ] Handle errors and edge cases
1022
-
1023
- **Task 3.3: Webhook Handler**
1024
- - [ ] Implement `WebhookHandler.ts`
1025
- - [ ] Validate webhook signature
1026
- - [ ] Parse webhook payload
1027
- - [ ] Call `onWebhookEvent` callback
1028
- - [ ] Add webhook event logging (optional)
1029
- - [ ] Return proper status codes
1030
-
1031
- **Task 3.4: Uninstall Handler (Optional)**
1032
- - [ ] Implement `UninstallHandler.ts`
1033
- - [ ] Verify user ownership
1034
- - [ ] Delete from database
1035
- - [ ] Optionally revoke on GitHub
1036
-
1037
- ### Phase 4: Repositories (Week 4)
1038
-
1039
- **Task 4.1: GitHubInstallationRepo**
1040
- - [ ] Implement base repository extending FlinkRepo
1041
- - [ ] Add `findByUserAndInstallationId` method
1042
- - [ ] Add `findByUserId` method
1043
- - [ ] Add `findByInstallationId` method
1044
- - [ ] Add `updateRepositories` method
1045
- - [ ] Add `suspend` method
1046
- - [ ] Add `deleteByInstallationId` method
1047
-
1048
- **Task 4.2: GitHubWebhookEventRepo (Optional)**
1049
- - [ ] Implement base repository
1050
- - [ ] Add `findUnprocessed` method
1051
- - [ ] Add `markProcessed` method
1052
- - [ ] Add `findByInstallationId` method
1053
- - [ ] Add TTL index for auto-cleanup
1054
-
1055
- ### Phase 5: Plugin Core (Week 5)
1056
-
1057
- **Task 5.1: Plugin Factory**
1058
- - [ ] Implement `githubAppPlugin()` factory function
1059
- - [ ] Add configuration validation
1060
- - [ ] Initialize repositories
1061
- - [ ] Register HTTP handlers
1062
- - [ ] Set up webhook endpoint
1063
- - [ ] Initialize services (auth, API client)
1064
-
1065
- **Task 5.2: Plugin Context**
1066
- - [ ] Implement `GitHubAppPluginContext` interface
1067
- - [ ] Add `getClient()` method
1068
- - [ ] Add `getInstallation()` method
1069
- - [ ] Add `getInstallations()` method
1070
- - [ ] Add `deleteInstallation()` method
1071
- - [ ] Add `hasRepositoryAccess()` method
1072
- - [ ] Add `getInstallationToken()` method
1073
-
1074
- **Task 5.3: Plugin Options**
1075
- - [ ] Implement `GitHubAppPluginOptions` interface
1076
- - [ ] Add validation for required fields
1077
- - [ ] Set default values
1078
- - [ ] Add TypeScript type exports
1079
-
1080
- ### Phase 6: Testing (Week 5-6)
1081
-
1082
- **Task 6.1: Unit Tests**
1083
- - [ ] Test JWT signing utilities
1084
- - [ ] Test webhook signature validation
1085
- - [ ] Test token caching logic
1086
- - [ ] Test error handling utilities
1087
-
1088
- **Task 6.2: Integration Tests**
1089
- - [ ] Test installation flow end-to-end
1090
- - [ ] Test webhook processing
1091
- - [ ] Test API client methods
1092
- - [ ] Test repository operations
1093
- - [ ] Mock GitHub API responses
1094
-
1095
- **Task 6.3: Security Tests**
1096
- - [ ] Test CSRF protection
1097
- - [ ] Test webhook signature validation
1098
- - [ ] Test private key validation
1099
- - [ ] Test token expiration handling
1100
-
1101
- ### Phase 7: Documentation (Week 6)
1102
-
1103
- **Task 7.1: README.md**
1104
- - [ ] Add installation instructions
1105
- - [ ] Document GitHub App setup process
1106
- - [ ] Add configuration examples
1107
- - [ ] Document all callback functions
1108
- - [ ] Add usage examples
1109
- - [ ] Create troubleshooting section
1110
-
1111
- **Task 7.2: SECURITY.md**
1112
- - [ ] Document private key management
1113
- - [ ] Document webhook security
1114
- - [ ] Document CSRF protection
1115
- - [ ] Add security checklist
1116
- - [ ] Document rate limiting
1117
-
1118
- **Task 7.3: Examples**
1119
- - [ ] Create basic installation example
1120
- - [ ] Create webhook handling example
1121
- - [ ] Create API client usage example
1122
- - [ ] Create multi-tenant example
1123
-
1124
- **Task 7.4: API Documentation**
1125
- - [ ] Document all public interfaces
1126
- - [ ] Add JSDoc comments
1127
- - [ ] Generate TypeDoc documentation (optional)
1128
-
1129
- ### Phase 8: Polish & Release (Week 7)
1130
-
1131
- **Task 8.1: Code Review**
1132
- - [ ] Review all code for consistency
1133
- - [ ] Check error handling coverage
1134
- - [ ] Verify security implementations
1135
- - [ ] Check TypeScript types
1136
-
1137
- **Task 8.2: Package Preparation**
1138
- - [ ] Update package.json
1139
- - [ ] Add LICENSE file
1140
- - [ ] Add CHANGELOG.md
1141
- - [ ] Test npm package build
1142
-
1143
- **Task 8.3: Integration Testing**
1144
- - [ ] Test with demo Flink app
1145
- - [ ] Test with OAuth plugin integration
1146
- - [ ] Test webhook delivery
1147
- - [ ] Test rate limiting
1148
-
1149
- **Task 8.4: Release**
1150
- - [ ] Version bump (0.1.0-alpha.1)
1151
- - [ ] Publish to npm
1152
- - [ ] Create GitHub release
1153
- - [ ] Update main Flink documentation
1154
-
1155
- ---
1156
-
1157
- ## Testing Requirements
1158
-
1159
- ### Unit Tests
1160
-
1161
- Required test coverage:
1162
-
1163
- 1. **JWT Utilities**
1164
- - Valid JWT generation
1165
- - Invalid private key handling
1166
- - Token expiration
1167
- - Algorithm validation
1168
-
1169
- 2. **Webhook Validation**
1170
- - Valid signature verification
1171
- - Invalid signature rejection
1172
- - Constant-time comparison
1173
- - Missing signature handling
1174
-
1175
- 3. **Token Cache**
1176
- - Cache hit/miss
1177
- - TTL expiration
1178
- - Cache invalidation
1179
-
1180
- 4. **Error Utilities**
1181
- - Error code mapping
1182
- - User-friendly messages
1183
- - Details sanitization
1184
-
1185
- ### Integration Tests
1186
-
1187
- 1. **Installation Flow**
1188
- - Full installation process
1189
- - State validation
1190
- - Callback handling
1191
- - Database storage
1192
-
1193
- 2. **Webhook Processing**
1194
- - Signature validation
1195
- - Event parsing
1196
- - Callback execution
1197
- - Event logging
1198
-
1199
- 3. **API Client**
1200
- - Token injection
1201
- - API calls
1202
- - Error handling
1203
- - Rate limit handling
1204
-
1205
- ### Security Tests
1206
-
1207
- 1. **CSRF Protection**
1208
- - State parameter validation
1209
- - Replay attack prevention
1210
- - State expiration
1211
-
1212
- 2. **Webhook Security**
1213
- - Signature verification
1214
- - Timing attack resistance
1215
- - Payload validation
1216
-
1217
- 3. **Token Security**
1218
- - Expiration handling
1219
- - Cache security
1220
- - No token leakage
1221
-
1222
- ---
1223
-
1224
- ## Documentation Requirements
1225
-
1226
- ### README.md Sections
1227
-
1228
- 1. **Overview** - What the plugin does
1229
- 2. **Installation** - npm install instructions
1230
- 3. **Prerequisites** - GitHub App setup guide
1231
- 4. **Quick Start** - Minimal working example
1232
- 5. **Configuration** - All options documented
1233
- 6. **GitHub App Setup** - Step-by-step guide
1234
- 7. **Installation Flow** - How users install the app
1235
- 8. **Webhook Setup** - How to configure webhooks
1236
- 9. **Context API** - All available methods
1237
- 10. **Examples** - Real-world usage examples
1238
- 11. **Security** - Security considerations
1239
- 12. **Troubleshooting** - Common issues and solutions
1240
- 13. **API Reference** - Full TypeScript API docs
1241
-
1242
- ### SECURITY.md Sections
1243
-
1244
- 1. **Private Key Management**
1245
- 2. **JWT Signing Security**
1246
- 3. **Webhook Signature Validation**
1247
- 4. **CSRF Protection**
1248
- 5. **Token Caching Security**
1249
- 6. **HTTPS Requirements**
1250
- 7. **Secrets Management**
1251
- 8. **Security Checklist**
1252
- 9. **Reporting Security Issues**
1253
-
1254
- ### Code Examples Required
1255
-
1256
- 1. Basic installation flow
1257
- 2. Webhook event handling
1258
- 3. Repository access
1259
- 4. Creating issues
1260
- 5. File content access
1261
- 6. Multi-tenant setup
1262
- 7. Error handling
1263
- 8. Integration with OAuth plugin
1264
-
1265
- ---
1266
-
1267
- ## Success Criteria
1268
-
1269
- The GitHub App Plugin will be considered complete when:
1270
-
1271
- ✅ Users can install GitHub Apps and select specific repositories
1272
- ✅ Application can access selected repositories via GitHub API
1273
- ✅ Webhooks are processed with signature validation
1274
- ✅ Private keys are securely stored and validated
1275
- ✅ Installation tokens are automatically refreshed
1276
- ✅ Integration with OAuth Plugin works seamlessly
1277
- ✅ Comprehensive error handling is implemented
1278
- ✅ All tests pass with >80% coverage
1279
- ✅ Documentation is complete and accurate
1280
- ✅ Security best practices are followed
1281
-
1282
- ---
1283
-
1284
- ## Future Enhancements (Out of Scope for v1)
1285
-
1286
- - Support for GitHub Enterprise Server
1287
- - Proactive token refresh before expiration
1288
- - Installation token revocation
1289
- - Repository permission auditing
1290
- - Webhook event replay mechanism
1291
- - GitHub Actions integration
1292
- - GraphQL API support
1293
- - Multi-region deployment support
1294
- - Advanced rate limit handling with queuing
1295
- - Webhook event transformation/filtering
1296
-
1297
- ---
1298
-
1299
- ## Questions & Decisions
1300
-
1301
- ### Open Questions
1302
-
1303
- 1. Should we support GitHub Enterprise Server in v1?
1304
- - **Decision:** No, add in v2 if needed
1305
-
1306
- 2. Should webhook events be logged by default?
1307
- - **Decision:** Optional, controlled by config flag
1308
-
1309
- 3. Should we cache installation details or always fetch fresh?
1310
- - **Decision:** Cache with configurable TTL
1311
-
1312
- 4. How to handle installation updates (repo add/remove)?
1313
- - **Decision:** Process via webhooks
1314
-
1315
- 5. Should we support multiple GitHub Apps per Flink app?
1316
- - **Decision:** Yes, via multi-tenant pattern
1317
-
1318
- ### Resolved Decisions
1319
-
1320
- - ✅ Use RS256 algorithm for JWT signing
1321
- - ✅ Cache installation tokens in memory (not database)
1322
- - ✅ Follow OAuth plugin architecture patterns
1323
- - ✅ Use FlinkRepo for database operations
1324
- - ✅ Integrate with JWT Auth Plugin for user linking
1325
- - ✅ Support both user and organization installations
1326
- - ✅ Provide webhook handler out of the box
1327
-
1328
- ---
1329
-
1330
- ## Appendix
1331
-
1332
- ### GitHub App Permissions
1333
-
1334
- Common permission configurations:
1335
-
1336
- **Read-Only Access:**
1337
- ```yaml
1338
- contents: read
1339
- metadata: read
1340
- ```
1341
-
1342
- **Issue Management:**
1343
- ```yaml
1344
- contents: read
1345
- issues: write
1346
- metadata: read
1347
- ```
1348
-
1349
- **Full Repository Access:**
1350
- ```yaml
1351
- contents: write
1352
- issues: write
1353
- pull_requests: write
1354
- metadata: read
1355
- ```
1356
-
1357
- ### GitHub App Events
1358
-
1359
- Common webhook events:
1360
-
1361
- - `push` - Code pushed to repository
1362
- - `pull_request` - PR opened/closed/merged
1363
- - `issues` - Issue opened/closed/commented
1364
- - `installation` - App installed/uninstalled
1365
- - `installation_repositories` - Repos added/removed
1366
- - `member` - Collaborator added/removed
1367
- - `release` - Release published
1368
-
1369
- ### Useful GitHub API Endpoints
1370
-
1371
- ```
1372
- GET /app/installations - List installations
1373
- GET /installation/repositories - List accessible repos
1374
- GET /repos/{owner}/{repo} - Get repository details
1375
- GET /repos/{owner}/{repo}/contents/{path} - Get file contents
1376
- POST /repos/{owner}/{repo}/issues - Create issue
1377
- GET /repos/{owner}/{repo}/issues - List issues
1378
- POST /repos/{owner}/{repo}/pulls - Create pull request
1379
- ```
1380
-
1381
- ---
1382
-
1383
- **End of PRD**