@flink-app/github-app-plugin 0.12.1-alpha.45 → 0.13.0
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 +153 -129
- package/dist/GitHubAppPlugin.js +1 -2
- package/dist/services/WebhookValidator.d.ts +0 -2
- package/dist/utils/error-utils.js +4 -4
- package/dist/utils/jwt-utils.js +2 -3
- package/dist/utils/state-utils.js +3 -4
- package/dist/utils/webhook-signature-utils.d.ts +0 -2
- package/dist/utils/webhook-signature-utils.js +1 -2
- package/package.json +35 -40
- package/tsconfig.json +1 -1
- package/dist/handlers/InitiateInstallation.d.ts +0 -32
- package/dist/handlers/InitiateInstallation.js +0 -66
- package/dist/handlers/InstallationCallback.d.ts +0 -36
- package/dist/handlers/InstallationCallback.js +0 -248
- package/dist/handlers/UninstallHandler.d.ts +0 -37
- package/dist/handlers/UninstallHandler.js +0 -153
- package/dist/schemas/InstallationCallbackRequest.d.ts +0 -10
- package/dist/schemas/InstallationCallbackRequest.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Migrate to pnpm and streamlines build process.
|
|
8
|
+
|
|
3
9
|
All notable changes to the GitHub App Plugin will be documented in this file.
|
|
4
10
|
|
|
5
11
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
@@ -10,137 +16,151 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
16
|
### Added
|
|
11
17
|
|
|
12
18
|
#### Core Features
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
19
|
+
|
|
20
|
+
- GitHub App installation flow with CSRF protection using state parameter
|
|
21
|
+
- JWT signing with RSA private key (RS256 algorithm)
|
|
22
|
+
- Automatic detection of PKCS#1 and PKCS#8 private key formats
|
|
23
|
+
- Installation access token management with automatic caching (55-minute TTL)
|
|
24
|
+
- Automatic token refresh when expired
|
|
25
|
+
- Webhook integration with HMAC-SHA256 signature validation
|
|
26
|
+
- Constant-time comparison for security-critical validations
|
|
27
|
+
- GitHub API client wrapper with automatic token injection
|
|
28
|
+
- Repository access verification
|
|
29
|
+
- Standalone plugin architecture (works without JWT Auth Plugin)
|
|
23
30
|
|
|
24
31
|
#### Database
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
32
|
+
|
|
33
|
+
- `GitHubAppSessionRepo` for CSRF session storage with TTL index (default: 10 minutes)
|
|
34
|
+
- `GitHubInstallationRepo` for installation-to-user mappings
|
|
35
|
+
- `GitHubWebhookEventRepo` for optional webhook event logging
|
|
36
|
+
- Configurable collection names for all repositories
|
|
37
|
+
- Automatic TTL index creation for session cleanup
|
|
30
38
|
|
|
31
39
|
#### API Handlers
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
40
|
+
|
|
41
|
+
- `GET /github-app/install` - Initiate GitHub App installation
|
|
42
|
+
- `GET /github-app/callback` - Handle installation callback
|
|
43
|
+
- `POST /github-app/webhook` - Receive and validate webhook events
|
|
44
|
+
- `DELETE /github-app/installation/:id` - Uninstall GitHub App
|
|
45
|
+
- Optional handler registration via `registerRoutes` config option
|
|
37
46
|
|
|
38
47
|
#### Context API
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
48
|
+
|
|
49
|
+
- `getClient(installationId)` - Get GitHub API client for installation
|
|
50
|
+
- `getInstallation(userId)` - Get first installation for user
|
|
51
|
+
- `getInstallations(userId)` - Get all installations for user
|
|
52
|
+
- `deleteInstallation(userId, installationId)` - Delete installation
|
|
53
|
+
- `hasRepositoryAccess(userId, owner, repo)` - Verify repository access
|
|
54
|
+
- `getInstallationToken(installationId)` - Get raw installation token (advanced usage)
|
|
55
|
+
- `clearTokenCache()` - Clear all cached installation tokens
|
|
56
|
+
- `options` - Read-only access to plugin configuration
|
|
47
57
|
|
|
48
58
|
#### GitHub API Client Methods
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
59
|
+
|
|
60
|
+
- `getRepositories()` - List accessible repositories
|
|
61
|
+
- `getRepository(owner, repo)` - Get repository details
|
|
62
|
+
- `getContents(owner, repo, path)` - Get file contents
|
|
63
|
+
- `createIssue(owner, repo, params)` - Create GitHub issue
|
|
64
|
+
- `request(method, endpoint, data)` - Generic API request with retry logic
|
|
54
65
|
|
|
55
66
|
#### Configuration Options
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
67
|
+
|
|
68
|
+
- Required: `appId`, `privateKey`, `webhookSecret`, `clientId`, `clientSecret`
|
|
69
|
+
- Optional: `appSlug`, `baseUrl` (default: https://api.github.com)
|
|
70
|
+
- Callbacks: `onInstallationSuccess` (required), `onInstallationError`, `onWebhookEvent`
|
|
71
|
+
- Database: `sessionsCollectionName`, `installationsCollectionName`, `webhookEventsCollectionName`
|
|
72
|
+
- Cache: `tokenCacheTTL` (default: 3300 seconds), `sessionTTL` (default: 600 seconds)
|
|
73
|
+
- Features: `registerRoutes` (default: true), `logWebhookEvents` (default: false)
|
|
62
74
|
|
|
63
75
|
#### Security Features
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
76
|
+
|
|
77
|
+
- Private key validation on plugin initialization
|
|
78
|
+
- CSRF protection with cryptographically secure state parameter (32 bytes)
|
|
79
|
+
- Webhook signature validation using HMAC-SHA256
|
|
80
|
+
- Constant-time comparison for state and signature validation
|
|
81
|
+
- Installation tokens cached in memory only (never in database)
|
|
82
|
+
- One-time use sessions (deleted after successful callback)
|
|
83
|
+
- Automatic session expiration via MongoDB TTL indexes
|
|
71
84
|
|
|
72
85
|
#### Error Handling
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
86
|
+
|
|
87
|
+
- Kebab-case error codes for frontend translation
|
|
88
|
+
- Comprehensive error codes: `invalid-state`, `session-expired`, `installation-not-found`, `invalid-private-key`, `jwt-signing-failed`, `token-exchange-failed`, `webhook-signature-invalid`, `webhook-payload-invalid`, `repository-not-accessible`, `installation-suspended`, `installation-not-owned`, `api-rate-limit`, `network-error`, `server-error`
|
|
89
|
+
- User-friendly error messages with actionable hints
|
|
90
|
+
- Detailed error logging for debugging
|
|
91
|
+
- Graceful handling of GitHub API failures
|
|
92
|
+
- Retry logic with exponential backoff for rate limits
|
|
79
93
|
|
|
80
94
|
#### Documentation
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
95
|
+
|
|
96
|
+
- Comprehensive README.md with step-by-step guides
|
|
97
|
+
- SECURITY.md covering all security considerations
|
|
98
|
+
- 8 complete usage examples:
|
|
99
|
+
- `basic-installation.ts` - Basic GitHub App installation with standalone auth
|
|
100
|
+
- `webhook-handling.ts` - Process webhook events (push, PR, installation)
|
|
101
|
+
- `repository-access.ts` - Access repositories via API client
|
|
102
|
+
- `create-issue.ts` - Create GitHub issue with permission check
|
|
103
|
+
- `with-jwt-auth.ts` - Optional integration with JWT Auth Plugin
|
|
104
|
+
- `organization-installation.ts` - Organization-level installation
|
|
105
|
+
- `error-handling.ts` - Comprehensive error handling
|
|
106
|
+
- `multi-event-webhook.ts` - Handle multiple webhook event types
|
|
107
|
+
- JSDoc comments on all public interfaces
|
|
108
|
+
- TypeScript type exports for all schemas and interfaces
|
|
94
109
|
|
|
95
110
|
#### Testing
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
111
|
+
|
|
112
|
+
- Project initialization tests
|
|
113
|
+
- Schema and repository tests
|
|
114
|
+
- JWT utilities tests (PKCS#1 and PKCS#8 support)
|
|
115
|
+
- Webhook signature validation tests
|
|
116
|
+
- Token cache tests
|
|
117
|
+
- Error utilities tests
|
|
118
|
+
- Authentication service tests
|
|
119
|
+
- API client tests
|
|
120
|
+
- Handler tests (installation flow, callbacks, webhooks)
|
|
121
|
+
- Plugin initialization tests
|
|
122
|
+
- Integration tests for end-to-end flows
|
|
123
|
+
- Security tests for CSRF and signature validation
|
|
108
124
|
|
|
109
125
|
### Features Highlights
|
|
110
126
|
|
|
111
127
|
#### Standalone Architecture
|
|
112
|
-
|
|
113
|
-
- Plugin
|
|
114
|
-
-
|
|
115
|
-
-
|
|
128
|
+
|
|
129
|
+
- **No dependencies on JWT Auth Plugin** - Works with any authentication system
|
|
130
|
+
- Plugin agnostic about authentication mechanism
|
|
131
|
+
- Developers can use sessions, custom tokens, or any auth system
|
|
132
|
+
- Optional integration with JWT Auth Plugin available
|
|
116
133
|
|
|
117
134
|
#### Token Management
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
135
|
+
|
|
136
|
+
- **Automatic caching** - Reduces GitHub API calls
|
|
137
|
+
- **Smart expiration** - Caches for 55 minutes (tokens expire at 60 minutes)
|
|
138
|
+
- **Automatic refresh** - Seamless token renewal
|
|
139
|
+
- **Memory-only storage** - Enhanced security
|
|
122
140
|
|
|
123
141
|
#### Webhook Security
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
142
|
+
|
|
143
|
+
- **Signature validation** - HMAC-SHA256 with constant-time comparison
|
|
144
|
+
- **Automatic validation** - No manual verification needed
|
|
145
|
+
- **Rejection of invalid webhooks** - Returns 401 for invalid signatures
|
|
127
146
|
|
|
128
147
|
#### Developer Experience
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
148
|
+
|
|
149
|
+
- **TypeScript-first** - Full type safety
|
|
150
|
+
- **Auto-registration** - Handlers, repos, and jobs automatically registered
|
|
151
|
+
- **Comprehensive examples** - 8 working examples covering all use cases
|
|
152
|
+
- **Clear error messages** - Actionable error codes with hints
|
|
153
|
+
- **Flexible configuration** - Sensible defaults, customizable when needed
|
|
134
154
|
|
|
135
155
|
### Known Limitations
|
|
136
156
|
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
157
|
+
- Single GitHub App per Flink application (multi-tenant support deferred to future)
|
|
158
|
+
- No proactive token refresh before expiration (refreshes on-demand when expired)
|
|
159
|
+
- GitHub Enterprise Server support via `baseUrl` option but not explicitly tested
|
|
160
|
+
- No GraphQL API support (REST API only)
|
|
161
|
+
- No advanced rate limit handling with queuing (basic retry logic included)
|
|
162
|
+
- No webhook event replay mechanism
|
|
163
|
+
- No repository permission auditing features
|
|
144
164
|
|
|
145
165
|
### Breaking Changes
|
|
146
166
|
|
|
@@ -157,44 +177,48 @@ None (initial release)
|
|
|
157
177
|
## Roadmap
|
|
158
178
|
|
|
159
179
|
### Planned for v0.2.0
|
|
160
|
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
165
|
-
-
|
|
180
|
+
|
|
181
|
+
- Multi-tenant support (multiple GitHub Apps per Flink application)
|
|
182
|
+
- Proactive token refresh before expiration
|
|
183
|
+
- Enhanced rate limit handling with request queuing
|
|
184
|
+
- Webhook event replay mechanism for failed deliveries
|
|
185
|
+
- GitHub Actions integration
|
|
186
|
+
- GraphQL API support
|
|
166
187
|
|
|
167
188
|
### Planned for v0.3.0
|
|
168
|
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
189
|
+
|
|
190
|
+
- Repository permission auditing
|
|
191
|
+
- Installation analytics and reporting
|
|
192
|
+
- Batch operations for multiple repositories
|
|
193
|
+
- Advanced webhook event filtering
|
|
194
|
+
- Custom event processors registry
|
|
195
|
+
- Webhook event transformation pipeline
|
|
174
196
|
|
|
175
197
|
### Planned for v1.0.0
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
198
|
+
|
|
199
|
+
- Production-ready stability
|
|
200
|
+
- Comprehensive GitHub Enterprise Server testing
|
|
201
|
+
- Performance optimizations
|
|
202
|
+
- Advanced caching strategies
|
|
203
|
+
- Complete test coverage (>90%)
|
|
204
|
+
- Security audit and hardening
|
|
182
205
|
|
|
183
206
|
### Future Considerations
|
|
184
|
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
207
|
+
|
|
208
|
+
- GitHub Packages integration
|
|
209
|
+
- Container Registry support
|
|
210
|
+
- Deployment status integration
|
|
211
|
+
- Check runs and check suites support
|
|
212
|
+
- Team management features
|
|
213
|
+
- Marketplace integration
|
|
190
214
|
|
|
191
215
|
## Support
|
|
192
216
|
|
|
193
|
-
-
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
217
|
+
- **Documentation:** [README.md](./README.md)
|
|
218
|
+
- **Security:** [SECURITY.md](./SECURITY.md)
|
|
219
|
+
- **Examples:** [examples/](./examples/)
|
|
220
|
+
- **Issues:** [GitHub Issues](https://github.com/your-org/flink-framework/issues)
|
|
221
|
+
- **Discussions:** [GitHub Discussions](https://github.com/your-org/flink-framework/discussions)
|
|
198
222
|
|
|
199
223
|
## Contributing
|
|
200
224
|
|
package/dist/GitHubAppPlugin.js
CHANGED
|
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.githubAppPlugin =
|
|
29
|
+
exports.githubAppPlugin = githubAppPlugin;
|
|
30
30
|
const flink_1 = require("@flink-app/flink");
|
|
31
31
|
const GitHubAppSessionRepo_1 = __importDefault(require("./repos/GitHubAppSessionRepo"));
|
|
32
32
|
const GitHubInstallationRepo_1 = __importDefault(require("./repos/GitHubInstallationRepo"));
|
|
@@ -373,4 +373,3 @@ function githubAppPlugin(options) {
|
|
|
373
373
|
init,
|
|
374
374
|
};
|
|
375
375
|
}
|
|
376
|
-
exports.githubAppPlugin = githubAppPlugin;
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* Error codes use kebab-case for frontend translation consistency.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.
|
|
11
|
+
exports.GitHubAppErrorCodes = void 0;
|
|
12
|
+
exports.createGitHubAppError = createGitHubAppError;
|
|
13
|
+
exports.validatePrivateKey = validatePrivateKey;
|
|
14
|
+
exports.handleGitHubAPIError = handleGitHubAPIError;
|
|
12
15
|
/**
|
|
13
16
|
* GitHub App error codes
|
|
14
17
|
*
|
|
@@ -50,7 +53,6 @@ function createGitHubAppError(code, message, details) {
|
|
|
50
53
|
details,
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
|
-
exports.createGitHubAppError = createGitHubAppError;
|
|
54
56
|
/**
|
|
55
57
|
* Validate private key format
|
|
56
58
|
*
|
|
@@ -71,7 +73,6 @@ function validatePrivateKey(privateKey) {
|
|
|
71
73
|
}
|
|
72
74
|
return true;
|
|
73
75
|
}
|
|
74
|
-
exports.validatePrivateKey = validatePrivateKey;
|
|
75
76
|
/**
|
|
76
77
|
* Map GitHub API errors to standardized errors
|
|
77
78
|
*
|
|
@@ -118,4 +119,3 @@ function handleGitHubAPIError(error) {
|
|
|
118
119
|
status: error.status,
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
|
-
exports.handleGitHubAPIError = handleGitHubAPIError;
|
package/dist/utils/jwt-utils.js
CHANGED
|
@@ -9,7 +9,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
12
|
+
exports.detectPEMFormat = detectPEMFormat;
|
|
13
|
+
exports.generateJWT = generateJWT;
|
|
13
14
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
14
15
|
/**
|
|
15
16
|
* Detect PEM format (PKCS#1 or PKCS#8)
|
|
@@ -31,7 +32,6 @@ function detectPEMFormat(privateKey) {
|
|
|
31
32
|
}
|
|
32
33
|
throw new Error("Invalid PEM format. Expected PKCS#1 (BEGIN RSA PRIVATE KEY) or PKCS#8 (BEGIN PRIVATE KEY).");
|
|
33
34
|
}
|
|
34
|
-
exports.detectPEMFormat = detectPEMFormat;
|
|
35
35
|
/**
|
|
36
36
|
* Generate GitHub App JWT
|
|
37
37
|
*
|
|
@@ -64,4 +64,3 @@ function generateJWT(appId, privateKey) {
|
|
|
64
64
|
throw new Error(`Failed to sign JWT: ${error.message}`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
exports.generateJWT = generateJWT;
|
|
@@ -11,7 +11,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.
|
|
14
|
+
exports.generateState = generateState;
|
|
15
|
+
exports.validateState = validateState;
|
|
16
|
+
exports.generateSessionId = generateSessionId;
|
|
15
17
|
const crypto_1 = __importDefault(require("crypto"));
|
|
16
18
|
/**
|
|
17
19
|
* Generate a cryptographically secure state parameter
|
|
@@ -25,7 +27,6 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
25
27
|
function generateState() {
|
|
26
28
|
return crypto_1.default.randomBytes(32).toString("hex");
|
|
27
29
|
}
|
|
28
|
-
exports.generateState = generateState;
|
|
29
30
|
/**
|
|
30
31
|
* Validate state parameter using constant-time comparison
|
|
31
32
|
*
|
|
@@ -59,7 +60,6 @@ function validateState(provided, stored) {
|
|
|
59
60
|
return false;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
exports.validateState = validateState;
|
|
63
63
|
/**
|
|
64
64
|
* Generate a session ID for installation flow tracking
|
|
65
65
|
*
|
|
@@ -71,4 +71,3 @@ exports.validateState = validateState;
|
|
|
71
71
|
function generateSessionId() {
|
|
72
72
|
return crypto_1.default.randomBytes(16).toString("hex");
|
|
73
73
|
}
|
|
74
|
-
exports.generateSessionId = generateSessionId;
|
|
@@ -9,7 +9,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.validateWebhookSignature =
|
|
12
|
+
exports.validateWebhookSignature = validateWebhookSignature;
|
|
13
13
|
const crypto_1 = __importDefault(require("crypto"));
|
|
14
14
|
/**
|
|
15
15
|
* Validate GitHub webhook signature
|
|
@@ -54,4 +54,3 @@ function validateWebhookSignature(payload, signature, secret) {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
exports.validateWebhookSignature = validateWebhookSignature;
|
package/package.json
CHANGED
|
@@ -1,41 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"tsc-watch": "^4.2.9",
|
|
38
|
-
"typescript": "5.4.5"
|
|
39
|
-
},
|
|
40
|
-
"gitHead": "af426a157217c110ac9c7beb48e2e746968bec33"
|
|
41
|
-
}
|
|
2
|
+
"name": "@flink-app/github-app-plugin",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Flink plugin for GitHub App integration with installation management and webhook handling",
|
|
5
|
+
"author": "joel@frost.se",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"mongodb": "^6.15.0"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"jsonwebtoken": "^9.0.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/jasmine": "^3.7.1",
|
|
20
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
21
|
+
"@types/node": "22.13.10",
|
|
22
|
+
"mongodb-memory-server": "^10.2.3",
|
|
23
|
+
"ts-node": "^10.9.2",
|
|
24
|
+
"tsc-watch": "^4.2.9",
|
|
25
|
+
"@flink-app/test-utils": "0.13.0",
|
|
26
|
+
"@flink-app/flink": "0.13.0"
|
|
27
|
+
},
|
|
28
|
+
"gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "jasmine-ts --config=./spec/support/jasmine.json",
|
|
31
|
+
"test:watch": "nodemon --ext ts --exec 'jasmine-ts --config=./spec/support/jasmine.json'",
|
|
32
|
+
"build": "tsc --project tsconfig.dist.json",
|
|
33
|
+
"watch": "tsc-watch --project tsconfig.dist.json",
|
|
34
|
+
"clean": "rimraf dist .flink"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub App Installation Initiation Handler
|
|
3
|
-
*
|
|
4
|
-
* Initiates the GitHub App installation flow by:
|
|
5
|
-
* 1. Generating a cryptographically secure state parameter for CSRF protection
|
|
6
|
-
* 2. Creating an installation session to track the flow
|
|
7
|
-
* 3. Building GitHub's installation URL with state parameter
|
|
8
|
-
* 4. Redirecting the user to GitHub for app installation
|
|
9
|
-
*
|
|
10
|
-
* Route: GET /github-app/install?user_id={optional_user_id}
|
|
11
|
-
*/
|
|
12
|
-
import { GetHandler, RouteProps } from "@flink-app/flink";
|
|
13
|
-
/**
|
|
14
|
-
* Query parameters for the handler
|
|
15
|
-
*/
|
|
16
|
-
interface QueryParams {
|
|
17
|
-
user_id?: string;
|
|
18
|
-
[key: string]: any;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Route configuration
|
|
22
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
23
|
-
*/
|
|
24
|
-
export declare const Route: RouteProps;
|
|
25
|
-
/**
|
|
26
|
-
* GitHub App Installation Initiation Handler
|
|
27
|
-
*
|
|
28
|
-
* Starts the installation flow by generating CSRF state, creating a session,
|
|
29
|
-
* and redirecting to GitHub's app installation page.
|
|
30
|
-
*/
|
|
31
|
-
declare const InitiateInstallation: GetHandler<any, any, any, QueryParams>;
|
|
32
|
-
export default InitiateInstallation;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* GitHub App Installation Initiation Handler
|
|
4
|
-
*
|
|
5
|
-
* Initiates the GitHub App installation flow by:
|
|
6
|
-
* 1. Generating a cryptographically secure state parameter for CSRF protection
|
|
7
|
-
* 2. Creating an installation session to track the flow
|
|
8
|
-
* 3. Building GitHub's installation URL with state parameter
|
|
9
|
-
* 4. Redirecting the user to GitHub for app installation
|
|
10
|
-
*
|
|
11
|
-
* Route: GET /github-app/install?user_id={optional_user_id}
|
|
12
|
-
*/
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.Route = void 0;
|
|
15
|
-
const flink_1 = require("@flink-app/flink");
|
|
16
|
-
const state_utils_1 = require("../utils/state-utils");
|
|
17
|
-
/**
|
|
18
|
-
* Route configuration
|
|
19
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
20
|
-
*/
|
|
21
|
-
exports.Route = {
|
|
22
|
-
path: "/github-app/install",
|
|
23
|
-
method: flink_1.HttpMethod.get,
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* GitHub App Installation Initiation Handler
|
|
27
|
-
*
|
|
28
|
-
* Starts the installation flow by generating CSRF state, creating a session,
|
|
29
|
-
* and redirecting to GitHub's app installation page.
|
|
30
|
-
*/
|
|
31
|
-
const InitiateInstallation = async ({ ctx, req }) => {
|
|
32
|
-
const { user_id } = req.query;
|
|
33
|
-
try {
|
|
34
|
-
// Get plugin options
|
|
35
|
-
const { options } = ctx.plugins.githubApp;
|
|
36
|
-
// Validate that appSlug is configured
|
|
37
|
-
if (!options.appSlug) {
|
|
38
|
-
return (0, flink_1.badRequest)("GitHub App slug is not configured. Please set appSlug in plugin options.");
|
|
39
|
-
}
|
|
40
|
-
// Generate cryptographically secure state and session ID
|
|
41
|
-
const state = (0, state_utils_1.generateState)();
|
|
42
|
-
const sessionId = (0, state_utils_1.generateSessionId)();
|
|
43
|
-
// Store session for state validation in callback
|
|
44
|
-
await ctx.repos.githubAppSessionRepo.create({
|
|
45
|
-
sessionId,
|
|
46
|
-
state,
|
|
47
|
-
userId: user_id,
|
|
48
|
-
createdAt: new Date(),
|
|
49
|
-
});
|
|
50
|
-
// Build GitHub installation URL
|
|
51
|
-
const installationUrl = `https://github.com/apps/${options.appSlug}/installations/new?state=${state}`;
|
|
52
|
-
// Redirect user to GitHub's app installation page
|
|
53
|
-
return {
|
|
54
|
-
status: 302,
|
|
55
|
-
headers: {
|
|
56
|
-
Location: installationUrl,
|
|
57
|
-
},
|
|
58
|
-
data: {},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
// Handle unexpected errors
|
|
63
|
-
return (0, flink_1.internalServerError)(error.message || "Failed to initiate GitHub App installation");
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
exports.default = InitiateInstallation;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub App Installation Callback Handler
|
|
3
|
-
*
|
|
4
|
-
* Handles the callback from GitHub after app installation by:
|
|
5
|
-
* 1. Validating the state parameter to prevent CSRF attacks
|
|
6
|
-
* 2. Fetching installation details from GitHub API
|
|
7
|
-
* 3. Calling the onInstallationSuccess callback to link installation to user
|
|
8
|
-
* 4. Storing the installation in the database
|
|
9
|
-
* 5. Redirecting to the application
|
|
10
|
-
*
|
|
11
|
-
* Route: GET /github-app/callback?installation_id=...&setup_action=...&state=...
|
|
12
|
-
*/
|
|
13
|
-
import { RouteProps } from "@flink-app/flink";
|
|
14
|
-
import { GitHubAppInternalContext } from "../GitHubAppInternalContext";
|
|
15
|
-
/**
|
|
16
|
-
* Route configuration
|
|
17
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
18
|
-
*/
|
|
19
|
-
export declare const Route: RouteProps;
|
|
20
|
-
/**
|
|
21
|
-
* GitHub App Installation Callback Handler
|
|
22
|
-
*
|
|
23
|
-
* Completes the installation flow by validating state, fetching installation
|
|
24
|
-
* details, calling the app's callback, and storing the installation.
|
|
25
|
-
*/
|
|
26
|
-
declare const InstallationCallback: ({ ctx, req }: {
|
|
27
|
-
ctx: GitHubAppInternalContext;
|
|
28
|
-
req: any;
|
|
29
|
-
}) => Promise<import("@flink-app/flink").FlinkResponse<undefined> | {
|
|
30
|
-
status: number;
|
|
31
|
-
headers: {
|
|
32
|
-
Location: string;
|
|
33
|
-
};
|
|
34
|
-
data: {};
|
|
35
|
-
}>;
|
|
36
|
-
export default InstallationCallback;
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* GitHub App Installation Callback Handler
|
|
4
|
-
*
|
|
5
|
-
* Handles the callback from GitHub after app installation by:
|
|
6
|
-
* 1. Validating the state parameter to prevent CSRF attacks
|
|
7
|
-
* 2. Fetching installation details from GitHub API
|
|
8
|
-
* 3. Calling the onInstallationSuccess callback to link installation to user
|
|
9
|
-
* 4. Storing the installation in the database
|
|
10
|
-
* 5. Redirecting to the application
|
|
11
|
-
*
|
|
12
|
-
* Route: GET /github-app/callback?installation_id=...&setup_action=...&state=...
|
|
13
|
-
*/
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.Route = void 0;
|
|
16
|
-
const flink_1 = require("@flink-app/flink");
|
|
17
|
-
const error_utils_1 = require("../utils/error-utils");
|
|
18
|
-
const state_utils_1 = require("../utils/state-utils");
|
|
19
|
-
/**
|
|
20
|
-
* Route configuration
|
|
21
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
22
|
-
*/
|
|
23
|
-
exports.Route = {
|
|
24
|
-
path: "/github-app/callback",
|
|
25
|
-
method: flink_1.HttpMethod.get,
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* GitHub App Installation Callback Handler
|
|
29
|
-
*
|
|
30
|
-
* Completes the installation flow by validating state, fetching installation
|
|
31
|
-
* details, calling the app's callback, and storing the installation.
|
|
32
|
-
*/
|
|
33
|
-
const InstallationCallback = async ({ ctx, req }) => {
|
|
34
|
-
const { installation_id, setup_action, state, code } = req.query;
|
|
35
|
-
try {
|
|
36
|
-
// Validate required parameters
|
|
37
|
-
if (!installation_id || !setup_action || !state) {
|
|
38
|
-
return (0, flink_1.badRequest)("Missing required parameters: installation_id, setup_action, or state");
|
|
39
|
-
}
|
|
40
|
-
// Find installation session by state
|
|
41
|
-
const session = await ctx.repos.githubAppSessionRepo.getOne({ state });
|
|
42
|
-
if (!session) {
|
|
43
|
-
const error = (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SESSION_EXPIRED, "Installation session not found or expired. Please try again.", {
|
|
44
|
-
state: state.substring(0, 10) + "...",
|
|
45
|
-
});
|
|
46
|
-
// Call onInstallationError callback if provided
|
|
47
|
-
const { options } = ctx.plugins.githubApp;
|
|
48
|
-
if (options.onInstallationError) {
|
|
49
|
-
const errorResult = await options.onInstallationError({ error, installationId: installation_id });
|
|
50
|
-
if (errorResult.redirectUrl) {
|
|
51
|
-
return {
|
|
52
|
-
status: 302,
|
|
53
|
-
headers: { Location: errorResult.redirectUrl },
|
|
54
|
-
data: {},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return (0, flink_1.badRequest)(error.message);
|
|
59
|
-
}
|
|
60
|
-
// Validate state parameter using constant-time comparison (CSRF protection)
|
|
61
|
-
if (!(0, state_utils_1.validateState)(state, session.state)) {
|
|
62
|
-
const error = (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
63
|
-
providedState: state.substring(0, 10) + "...",
|
|
64
|
-
});
|
|
65
|
-
// Call onInstallationError callback if provided
|
|
66
|
-
const { options } = ctx.plugins.githubApp;
|
|
67
|
-
if (options.onInstallationError) {
|
|
68
|
-
const errorResult = await options.onInstallationError({ error, installationId: installation_id });
|
|
69
|
-
if (errorResult.redirectUrl) {
|
|
70
|
-
return {
|
|
71
|
-
status: 302,
|
|
72
|
-
headers: { Location: errorResult.redirectUrl },
|
|
73
|
-
data: {},
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return (0, flink_1.badRequest)(error.message);
|
|
78
|
-
}
|
|
79
|
-
// Delete session immediately after validation (one-time use)
|
|
80
|
-
await ctx.repos.githubAppSessionRepo.deleteBySessionId(session.sessionId);
|
|
81
|
-
// Get plugin options and auth service
|
|
82
|
-
const { options, authService } = ctx.plugins.githubApp;
|
|
83
|
-
// Generate GitHub App JWT
|
|
84
|
-
const jwt = authService.generateAppJWT();
|
|
85
|
-
// Fetch installation details from GitHub API
|
|
86
|
-
const installationIdNum = parseInt(installation_id, 10);
|
|
87
|
-
const installationDetails = await fetchInstallationDetails(installationIdNum, jwt, options.baseUrl);
|
|
88
|
-
// Fetch repositories accessible by this installation
|
|
89
|
-
const repositories = await fetchInstallationRepositories(installationIdNum, jwt, options.baseUrl);
|
|
90
|
-
// Call onInstallationSuccess callback to get userId and redirect URL
|
|
91
|
-
let callbackResult;
|
|
92
|
-
try {
|
|
93
|
-
callbackResult = await options.onInstallationSuccess({
|
|
94
|
-
installationId: installationIdNum,
|
|
95
|
-
setupAction: setup_action,
|
|
96
|
-
account: installationDetails.account,
|
|
97
|
-
repositories: repositories,
|
|
98
|
-
permissions: installationDetails.permissions || {},
|
|
99
|
-
events: installationDetails.events || [],
|
|
100
|
-
}, ctx);
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
flink_1.log.error("GitHub App onInstallationSuccess callback failed:", error);
|
|
104
|
-
const callbackError = (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SERVER_ERROR, "Failed to complete installation. Please try again.", {
|
|
105
|
-
originalError: error.message,
|
|
106
|
-
});
|
|
107
|
-
// Call onInstallationError callback if provided
|
|
108
|
-
if (options.onInstallationError) {
|
|
109
|
-
const errorResult = await options.onInstallationError({
|
|
110
|
-
error: callbackError,
|
|
111
|
-
installationId: installation_id,
|
|
112
|
-
});
|
|
113
|
-
if (errorResult.redirectUrl) {
|
|
114
|
-
return {
|
|
115
|
-
status: 302,
|
|
116
|
-
headers: { Location: errorResult.redirectUrl },
|
|
117
|
-
data: {},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return (0, flink_1.internalServerError)("Installation failed. Please try again.");
|
|
122
|
-
}
|
|
123
|
-
// Extract userId and redirectUrl from callback result
|
|
124
|
-
const { userId, redirectUrl } = callbackResult;
|
|
125
|
-
if (!userId) {
|
|
126
|
-
return (0, flink_1.badRequest)("onInstallationSuccess callback must return userId");
|
|
127
|
-
}
|
|
128
|
-
// Store installation in database
|
|
129
|
-
await ctx.repos.githubInstallationRepo.create({
|
|
130
|
-
userId,
|
|
131
|
-
installationId: installationIdNum,
|
|
132
|
-
accountId: installationDetails.account.id,
|
|
133
|
-
accountLogin: installationDetails.account.login,
|
|
134
|
-
accountType: installationDetails.account.type,
|
|
135
|
-
avatarUrl: installationDetails.account.avatar_url,
|
|
136
|
-
repositories: repositories.map((repo) => ({
|
|
137
|
-
id: repo.id,
|
|
138
|
-
name: repo.name,
|
|
139
|
-
fullName: repo.full_name,
|
|
140
|
-
private: repo.private,
|
|
141
|
-
})),
|
|
142
|
-
permissions: installationDetails.permissions || {},
|
|
143
|
-
events: installationDetails.events || [],
|
|
144
|
-
createdAt: new Date(),
|
|
145
|
-
updatedAt: new Date(),
|
|
146
|
-
});
|
|
147
|
-
// Redirect to app using callback's redirectUrl or default to root
|
|
148
|
-
const finalRedirectUrl = redirectUrl || "/";
|
|
149
|
-
return {
|
|
150
|
-
status: 302,
|
|
151
|
-
headers: {
|
|
152
|
-
Location: finalRedirectUrl,
|
|
153
|
-
},
|
|
154
|
-
data: {},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
flink_1.log.error("GitHub App installation callback error:", error);
|
|
159
|
-
// Call onInstallationError callback if provided
|
|
160
|
-
const { options } = ctx.plugins.githubApp;
|
|
161
|
-
if (options.onInstallationError) {
|
|
162
|
-
try {
|
|
163
|
-
const errorResult = await options.onInstallationError({
|
|
164
|
-
error: error,
|
|
165
|
-
installationId: installation_id,
|
|
166
|
-
});
|
|
167
|
-
if (errorResult.redirectUrl) {
|
|
168
|
-
return {
|
|
169
|
-
status: 302,
|
|
170
|
-
headers: { Location: errorResult.redirectUrl },
|
|
171
|
-
data: {},
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
catch (callbackError) {
|
|
176
|
-
flink_1.log.error("onInstallationError callback failed:", callbackError);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return (0, flink_1.internalServerError)(error.message || "Installation callback failed. Please try again.");
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* Fetch installation details from GitHub API
|
|
184
|
-
*
|
|
185
|
-
* @param installationId - GitHub installation ID
|
|
186
|
-
* @param jwt - GitHub App JWT
|
|
187
|
-
* @param baseUrl - GitHub API base URL
|
|
188
|
-
* @returns Installation details
|
|
189
|
-
*/
|
|
190
|
-
async function fetchInstallationDetails(installationId, jwt, baseUrl = "https://api.github.com") {
|
|
191
|
-
const url = `${baseUrl}/app/installations/${installationId}`;
|
|
192
|
-
const response = await fetch(url, {
|
|
193
|
-
method: "GET",
|
|
194
|
-
headers: {
|
|
195
|
-
Authorization: `Bearer ${jwt}`,
|
|
196
|
-
Accept: "application/vnd.github+json",
|
|
197
|
-
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
if (!response.ok) {
|
|
201
|
-
const errorBody = await response.text();
|
|
202
|
-
throw new Error(`Failed to fetch installation details: ${response.statusText} - ${errorBody}`);
|
|
203
|
-
}
|
|
204
|
-
return await response.json();
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Fetch repositories accessible by installation
|
|
208
|
-
*
|
|
209
|
-
* @param installationId - GitHub installation ID
|
|
210
|
-
* @param jwt - GitHub App JWT
|
|
211
|
-
* @param baseUrl - GitHub API base URL
|
|
212
|
-
* @returns Array of repositories
|
|
213
|
-
*/
|
|
214
|
-
async function fetchInstallationRepositories(installationId, jwt, baseUrl = "https://api.github.com") {
|
|
215
|
-
const url = `${baseUrl}/installation/repositories`;
|
|
216
|
-
// First get an installation token
|
|
217
|
-
const tokenUrl = `${baseUrl}/app/installations/${installationId}/access_tokens`;
|
|
218
|
-
const tokenResponse = await fetch(tokenUrl, {
|
|
219
|
-
method: "POST",
|
|
220
|
-
headers: {
|
|
221
|
-
Authorization: `Bearer ${jwt}`,
|
|
222
|
-
Accept: "application/vnd.github+json",
|
|
223
|
-
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
if (!tokenResponse.ok) {
|
|
227
|
-
const errorBody = await tokenResponse.text();
|
|
228
|
-
throw new Error(`Failed to get installation token: ${tokenResponse.statusText} - ${errorBody}`);
|
|
229
|
-
}
|
|
230
|
-
const tokenData = await tokenResponse.json();
|
|
231
|
-
const token = tokenData.token;
|
|
232
|
-
// Now fetch repositories with the installation token
|
|
233
|
-
const reposResponse = await fetch(url, {
|
|
234
|
-
method: "GET",
|
|
235
|
-
headers: {
|
|
236
|
-
Authorization: `Bearer ${token}`,
|
|
237
|
-
Accept: "application/vnd.github+json",
|
|
238
|
-
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
if (!reposResponse.ok) {
|
|
242
|
-
const errorBody = await reposResponse.text();
|
|
243
|
-
throw new Error(`Failed to fetch repositories: ${reposResponse.statusText} - ${errorBody}`);
|
|
244
|
-
}
|
|
245
|
-
const data = await reposResponse.json();
|
|
246
|
-
return data.repositories || [];
|
|
247
|
-
}
|
|
248
|
-
exports.default = InstallationCallback;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub App Uninstall Handler
|
|
3
|
-
*
|
|
4
|
-
* Manually uninstalls a GitHub App installation by:
|
|
5
|
-
* 1. Extracting userId from request (app-defined authentication)
|
|
6
|
-
* 2. Verifying user owns the installation
|
|
7
|
-
* 3. Deleting installation from database
|
|
8
|
-
* 4. Clearing cached installation token
|
|
9
|
-
* 5. Returning 204 No Content
|
|
10
|
-
*
|
|
11
|
-
* Route: DELETE /github-app/installation/:installationId
|
|
12
|
-
*
|
|
13
|
-
* Note: This is an optional handler for manual uninstallation.
|
|
14
|
-
* Apps should also handle the 'installation.deleted' webhook event
|
|
15
|
-
* for automatic cleanup when users uninstall via GitHub UI.
|
|
16
|
-
*/
|
|
17
|
-
import { Handler, RouteProps } from "@flink-app/flink";
|
|
18
|
-
/**
|
|
19
|
-
* Path parameters for the handler
|
|
20
|
-
*/
|
|
21
|
-
interface PathParams {
|
|
22
|
-
installationId: string;
|
|
23
|
-
[key: string]: string;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Route configuration
|
|
27
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
28
|
-
*/
|
|
29
|
-
export declare const Route: RouteProps;
|
|
30
|
-
/**
|
|
31
|
-
* GitHub App Uninstall Handler
|
|
32
|
-
*
|
|
33
|
-
* Allows users to manually uninstall a GitHub App from their account.
|
|
34
|
-
* The application must provide userId extraction logic (via JWT, session, etc.)
|
|
35
|
-
*/
|
|
36
|
-
declare const UninstallHandler: Handler<any, any, any, PathParams>;
|
|
37
|
-
export default UninstallHandler;
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* GitHub App Uninstall Handler
|
|
4
|
-
*
|
|
5
|
-
* Manually uninstalls a GitHub App installation by:
|
|
6
|
-
* 1. Extracting userId from request (app-defined authentication)
|
|
7
|
-
* 2. Verifying user owns the installation
|
|
8
|
-
* 3. Deleting installation from database
|
|
9
|
-
* 4. Clearing cached installation token
|
|
10
|
-
* 5. Returning 204 No Content
|
|
11
|
-
*
|
|
12
|
-
* Route: DELETE /github-app/installation/:installationId
|
|
13
|
-
*
|
|
14
|
-
* Note: This is an optional handler for manual uninstallation.
|
|
15
|
-
* Apps should also handle the 'installation.deleted' webhook event
|
|
16
|
-
* for automatic cleanup when users uninstall via GitHub UI.
|
|
17
|
-
*/
|
|
18
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.Route = void 0;
|
|
20
|
-
const flink_1 = require("@flink-app/flink");
|
|
21
|
-
/**
|
|
22
|
-
* Route configuration
|
|
23
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
24
|
-
*/
|
|
25
|
-
exports.Route = {
|
|
26
|
-
path: "/github-app/installation/:installationId",
|
|
27
|
-
method: flink_1.HttpMethod.delete,
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* GitHub App Uninstall Handler
|
|
31
|
-
*
|
|
32
|
-
* Allows users to manually uninstall a GitHub App from their account.
|
|
33
|
-
* The application must provide userId extraction logic (via JWT, session, etc.)
|
|
34
|
-
*/
|
|
35
|
-
const UninstallHandler = async ({ ctx, req }) => {
|
|
36
|
-
try {
|
|
37
|
-
const { installationId } = req.params;
|
|
38
|
-
const installationIdNum = parseInt(installationId, 10);
|
|
39
|
-
if (isNaN(installationIdNum)) {
|
|
40
|
-
return {
|
|
41
|
-
status: 400,
|
|
42
|
-
data: { error: "Invalid installation ID" },
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
// Extract userId from request
|
|
46
|
-
// This is app-defined and can work with any authentication system
|
|
47
|
-
// Examples:
|
|
48
|
-
// - JWT Auth Plugin: ctx.auth?.tokenData?.userId
|
|
49
|
-
// - Session-based: req.session?.userId
|
|
50
|
-
// - Custom header: req.headers['x-user-id']
|
|
51
|
-
//
|
|
52
|
-
// For now, we'll look for userId in multiple common places
|
|
53
|
-
const userId = extractUserId(req, ctx);
|
|
54
|
-
if (!userId) {
|
|
55
|
-
return {
|
|
56
|
-
status: 401,
|
|
57
|
-
data: { error: "Authentication required" },
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
// Find the installation
|
|
61
|
-
const installation = await ctx.repos.githubInstallationRepo.findByInstallationId(installationIdNum);
|
|
62
|
-
if (!installation) {
|
|
63
|
-
return {
|
|
64
|
-
status: 404,
|
|
65
|
-
data: { error: "Installation not found" },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
// Verify user owns the installation
|
|
69
|
-
if (installation.userId !== userId) {
|
|
70
|
-
flink_1.log.warn("User attempted to delete installation they don't own", {
|
|
71
|
-
userId,
|
|
72
|
-
installationId: installationIdNum,
|
|
73
|
-
ownerId: installation.userId,
|
|
74
|
-
});
|
|
75
|
-
return {
|
|
76
|
-
status: 403,
|
|
77
|
-
data: { error: "You do not have permission to delete this installation" },
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// Delete installation from database
|
|
81
|
-
const deletedCount = await ctx.repos.githubInstallationRepo.deleteByInstallationId(installationIdNum);
|
|
82
|
-
if (deletedCount === 0) {
|
|
83
|
-
flink_1.log.error("Failed to delete installation from database", {
|
|
84
|
-
installationId: installationIdNum,
|
|
85
|
-
});
|
|
86
|
-
return {
|
|
87
|
-
status: 500,
|
|
88
|
-
data: { error: "Failed to delete installation" },
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
// Clear cached installation token
|
|
92
|
-
ctx.plugins.githubApp.authService.deleteInstallationToken(installationIdNum);
|
|
93
|
-
flink_1.log.info("GitHub App installation deleted", {
|
|
94
|
-
userId,
|
|
95
|
-
installationId: installationIdNum,
|
|
96
|
-
});
|
|
97
|
-
// Return 204 No Content
|
|
98
|
-
return {
|
|
99
|
-
status: 204,
|
|
100
|
-
data: {},
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
flink_1.log.error("Error deleting GitHub App installation", {
|
|
105
|
-
error: error.message,
|
|
106
|
-
});
|
|
107
|
-
return {
|
|
108
|
-
status: 500,
|
|
109
|
-
data: { error: "Failed to delete installation" },
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
/**
|
|
114
|
-
* Extract userId from request
|
|
115
|
-
*
|
|
116
|
-
* This helper function attempts to extract userId from various common
|
|
117
|
-
* authentication patterns. Applications can customize this logic based
|
|
118
|
-
* on their authentication system.
|
|
119
|
-
*
|
|
120
|
-
* @param req - Express request object
|
|
121
|
-
* @param ctx - Flink context
|
|
122
|
-
* @returns userId if found, undefined otherwise
|
|
123
|
-
*/
|
|
124
|
-
function extractUserId(req, ctx) {
|
|
125
|
-
// Option 1: JWT Auth Plugin (if available)
|
|
126
|
-
if (ctx.auth?.tokenData?.userId) {
|
|
127
|
-
return ctx.auth.tokenData.userId;
|
|
128
|
-
}
|
|
129
|
-
// Option 2: Custom user property on request (set by custom middleware)
|
|
130
|
-
if (req.user?.id) {
|
|
131
|
-
return req.user.id;
|
|
132
|
-
}
|
|
133
|
-
if (req.user?.userId) {
|
|
134
|
-
return req.user.userId;
|
|
135
|
-
}
|
|
136
|
-
if (req.user?._id) {
|
|
137
|
-
return req.user._id;
|
|
138
|
-
}
|
|
139
|
-
// Option 3: Session-based authentication
|
|
140
|
-
if (req.session?.userId) {
|
|
141
|
-
return req.session.userId;
|
|
142
|
-
}
|
|
143
|
-
// Option 4: Custom header
|
|
144
|
-
if (req.headers["x-user-id"]) {
|
|
145
|
-
return req.headers["x-user-id"];
|
|
146
|
-
}
|
|
147
|
-
// Option 5: Query parameter (less secure, use with caution)
|
|
148
|
-
if (req.query?.user_id) {
|
|
149
|
-
return req.query.user_id;
|
|
150
|
-
}
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
exports.default = UninstallHandler;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Query parameters received from GitHub after app installation.
|
|
3
|
-
*/
|
|
4
|
-
export default interface InstallationCallbackRequest {
|
|
5
|
-
installation_id: string;
|
|
6
|
-
setup_action: 'install' | 'update' | 'request';
|
|
7
|
-
state: string;
|
|
8
|
-
code?: string;
|
|
9
|
-
[key: string]: string | undefined;
|
|
10
|
-
}
|