@manojkmfsi/monodog 1.1.17 → 1.1.19

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.
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ /**
3
+ * Repository Permission Routes
4
+ * Handles checking and managing repository permissions
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const express_1 = require("express");
8
+ const auth_middleware_1 = require("../middleware/auth-middleware");
9
+ const permission_service_1 = require("../services/permission-service");
10
+ const logger_1 = require("../middleware/logger");
11
+ const router = (0, express_1.Router)();
12
+ /**
13
+ * Get user's permission for a specific repository
14
+ * GET /permissions/:owner/:repo
15
+ */
16
+ router.get('/:owner/:repo', auth_middleware_1.authenticationMiddleware, async (req, res) => {
17
+ try {
18
+ const session = (0, auth_middleware_1.getSessionFromRequest)(req);
19
+ if (!session) {
20
+ res.status(401).json({
21
+ error: 'Unauthorized',
22
+ message: 'No active session',
23
+ });
24
+ return;
25
+ }
26
+ const { owner, repo } = req.params;
27
+ const forceRefresh = req.query.refresh === 'true';
28
+ if (!owner || !repo) {
29
+ res.status(400).json({
30
+ error: 'Bad request',
31
+ message: 'Owner and repo parameters are required',
32
+ });
33
+ return;
34
+ }
35
+ logger_1.AppLogger.debug(`Checking permission for ${session.user.login} in ${owner}/${repo}`);
36
+ // Get permission from cache or GitHub API
37
+ const cachedPermission = await (0, permission_service_1.getUserRepositoryPermission)(session.accessToken, session.user.id, session.user.login, owner, repo, forceRefresh);
38
+ const response = {
39
+ permission: cachedPermission.permission,
40
+ role: cachedPermission.role,
41
+ canAdmin: cachedPermission.permission === 'admin',
42
+ canMaintain: (0, permission_service_1.canPerformAction)(cachedPermission.permission, 'maintain'),
43
+ canWrite: (0, permission_service_1.canPerformAction)(cachedPermission.permission, 'write'),
44
+ canRead: (0, permission_service_1.canPerformAction)(cachedPermission.permission, 'read'),
45
+ denied: cachedPermission.permission === 'none',
46
+ };
47
+ res.json({
48
+ success: true,
49
+ owner,
50
+ repo,
51
+ user: session.user.login,
52
+ ...response,
53
+ });
54
+ }
55
+ catch (error) {
56
+ logger_1.AppLogger.error(`Failed to check permission: ${error}`);
57
+ res.status(500).json({
58
+ success: false,
59
+ error: 'Internal server error',
60
+ message: 'Failed to check repository permission',
61
+ });
62
+ }
63
+ });
64
+ /**
65
+ * Check if user can perform a specific action
66
+ * POST /permissions/:owner/:repo/can-action
67
+ */
68
+ router.post('/:owner/:repo/can-action', auth_middleware_1.authenticationMiddleware, async (req, res) => {
69
+ try {
70
+ const session = (0, auth_middleware_1.getSessionFromRequest)(req);
71
+ if (!session) {
72
+ res.status(401).json({
73
+ error: 'Unauthorized',
74
+ message: 'No active session',
75
+ });
76
+ return;
77
+ }
78
+ const { owner, repo } = req.params;
79
+ const { action } = req.body;
80
+ if (!owner || !repo) {
81
+ res.status(400).json({
82
+ error: 'Bad request',
83
+ message: 'Owner and repo parameters are required',
84
+ });
85
+ return;
86
+ }
87
+ if (!action || !['read', 'write', 'maintain', 'admin'].includes(action)) {
88
+ res.status(400).json({
89
+ error: 'Bad request',
90
+ message: 'Valid action is required (read, write, maintain, or admin)',
91
+ });
92
+ return;
93
+ }
94
+ logger_1.AppLogger.debug(`Checking if ${session.user.login} can perform '${action}' in ${owner}/${repo}`);
95
+ // Get permission
96
+ const cachedPermission = await (0, permission_service_1.getUserRepositoryPermission)(session.accessToken, session.user.id, session.user.login, owner, repo);
97
+ // Check if user can perform action
98
+ const can = (0, permission_service_1.canPerformAction)(cachedPermission.permission, action);
99
+ res.json({
100
+ success: true,
101
+ owner,
102
+ repo,
103
+ user: session.user.login,
104
+ action,
105
+ can,
106
+ permission: cachedPermission.permission,
107
+ role: cachedPermission.role,
108
+ });
109
+ }
110
+ catch (error) {
111
+ logger_1.AppLogger.error(`Failed to check action permission: ${error}`);
112
+ res.status(500).json({
113
+ success: false,
114
+ error: 'Internal server error',
115
+ message: 'Failed to check action permission',
116
+ });
117
+ }
118
+ });
119
+ /**
120
+ * Invalidate permission cache for a repository
121
+ * POST /permissions/:owner/:repo/invalidate
122
+ */
123
+ router.post('/:owner/:repo/invalidate', auth_middleware_1.authenticationMiddleware, (req, res) => {
124
+ try {
125
+ const session = (0, auth_middleware_1.getSessionFromRequest)(req);
126
+ if (!session) {
127
+ res.status(401).json({
128
+ error: 'Unauthorized',
129
+ message: 'No active session',
130
+ });
131
+ return;
132
+ }
133
+ const { owner, repo } = req.params;
134
+ if (!owner || !repo) {
135
+ res.status(400).json({
136
+ error: 'Bad request',
137
+ message: 'Owner and repo parameters are required',
138
+ });
139
+ return;
140
+ }
141
+ logger_1.AppLogger.debug(`Invalidating permission cache for ${session.user.login} in ${owner}/${repo}`);
142
+ // Invalidate cache
143
+ (0, permission_service_1.invalidatePermissionCache)(session.user.id, owner, repo);
144
+ res.json({
145
+ success: true,
146
+ message: 'Permission cache invalidated',
147
+ owner,
148
+ repo,
149
+ user: session.user.login,
150
+ });
151
+ }
152
+ catch (error) {
153
+ logger_1.AppLogger.error(`Failed to invalidate cache: ${error}`);
154
+ res.status(500).json({
155
+ success: false,
156
+ error: 'Internal server error',
157
+ message: 'Failed to invalidate permission cache',
158
+ });
159
+ }
160
+ });
161
+ exports.default = router;
package/dist/serve.js CHANGED
@@ -8,11 +8,16 @@
8
8
  * 1. Start the API server for the dashboard.
9
9
  * 2. Start serving the dashboard frontend.
10
10
  */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ const dotenv_1 = __importDefault(require("dotenv"));
16
+ const path_1 = __importDefault(require("path"));
12
17
  const index_1 = require("./index");
13
18
  const utilities_1 = require("./utils/utilities");
14
- let logLevel = '';
15
- let nodeEnv = 'production';
19
+ let logLevel = process.env.LOG_LEVEL || 'info';
20
+ let nodeEnv = process.env.NODE_ENV || 'production';
16
21
  const args = process.argv;
17
22
  if (args.includes('--dev')) {
18
23
  nodeEnv = 'development';
@@ -24,6 +29,7 @@ if (args.includes('--debug')) {
24
29
  else if (args.includes('--info')) {
25
30
  logLevel = 'info';
26
31
  }
32
+ dotenv_1.default.config({ path: path_1.default.resolve(process.cwd(), '.env') });
27
33
  process.env.LOG_LEVEL = logLevel;
28
34
  process.env.NODE_ENV = nodeEnv;
29
35
  const rootPath = (0, utilities_1.findMonorepoRoot)();
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub API Service
4
+ * Handles all GitHub API interactions including OAuth and permission checks
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.exchangeCodeForToken = exchangeCodeForToken;
11
+ exports.getAuthenticatedUser = getAuthenticatedUser;
12
+ exports.getUserEmail = getUserEmail;
13
+ exports.getRepositoryPermission = getRepositoryPermission;
14
+ exports.mapPermissionToRole = mapPermissionToRole;
15
+ exports.hasPermission = hasPermission;
16
+ exports.validateToken = validateToken;
17
+ exports.generateAuthorizationUrl = generateAuthorizationUrl;
18
+ const https_1 = __importDefault(require("https"));
19
+ const logger_1 = require("../middleware/logger");
20
+ const GITHUB_API_BASE = 'https://api.github.com';
21
+ const GITHUB_OAUTH_BASE = 'https://github.com';
22
+ /**
23
+ * Make an HTTPS request to GitHub API
24
+ */
25
+ function makeGitHubRequest(options, data) {
26
+ return new Promise((resolve, reject) => {
27
+ const request = https_1.default.request(options, (response) => {
28
+ let body = '';
29
+ response.on('data', (chunk) => {
30
+ body += chunk;
31
+ });
32
+ response.on('end', () => {
33
+ try {
34
+ if (response.statusCode && response.statusCode >= 400) {
35
+ reject(new Error(`GitHub API error: ${response.statusCode} - ${body}`));
36
+ }
37
+ else {
38
+ const result = body ? JSON.parse(body) : {};
39
+ resolve(result);
40
+ }
41
+ }
42
+ catch (error) {
43
+ reject(new Error(`Failed to parse GitHub API response: ${error}`));
44
+ }
45
+ });
46
+ });
47
+ request.on('error', (error) => {
48
+ logger_1.AppLogger.error(`GitHub API request failed: ${error.message}`);
49
+ reject(error);
50
+ });
51
+ request.setTimeout(10000, () => {
52
+ request.destroy();
53
+ reject(new Error('GitHub API request timeout'));
54
+ });
55
+ if (data) {
56
+ request.write(data);
57
+ }
58
+ request.end();
59
+ });
60
+ }
61
+ /**
62
+ * Exchange OAuth code for access token
63
+ */
64
+ async function exchangeCodeForToken(code, clientId, clientSecret, redirectUri) {
65
+ const payload = JSON.stringify({
66
+ client_id: clientId,
67
+ client_secret: clientSecret,
68
+ code,
69
+ redirect_uri: redirectUri,
70
+ });
71
+ const options = {
72
+ hostname: 'github.com',
73
+ path: '/login/oauth/access_token',
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ 'Content-Length': String(Buffer.byteLength(payload)),
78
+ Accept: 'application/json',
79
+ },
80
+ };
81
+ try {
82
+ const response = await makeGitHubRequest(options, payload);
83
+ if (response.error) {
84
+ throw new Error(`OAuth exchange failed: ${response.error}`);
85
+ }
86
+ logger_1.AppLogger.debug('Successfully exchanged OAuth code for access token');
87
+ return response;
88
+ }
89
+ catch (error) {
90
+ logger_1.AppLogger.error(`Failed to exchange OAuth code: ${error}`);
91
+ throw error;
92
+ }
93
+ }
94
+ /**
95
+ * Get authenticated user information
96
+ */
97
+ async function getAuthenticatedUser(accessToken) {
98
+ const options = {
99
+ hostname: 'api.github.com',
100
+ path: '/user',
101
+ method: 'GET',
102
+ headers: {
103
+ Authorization: `Bearer ${accessToken}`,
104
+ 'User-Agent': 'MonoDog',
105
+ Accept: 'application/vnd.github+json',
106
+ },
107
+ };
108
+ try {
109
+ const user = await makeGitHubRequest(options);
110
+ logger_1.AppLogger.debug(`Retrieved user info: ${user.login}`);
111
+ return user;
112
+ }
113
+ catch (error) {
114
+ logger_1.AppLogger.error(`Failed to get user info: ${error}`);
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * Get user's email from GitHub (with proper scopes)
120
+ */
121
+ async function getUserEmail(accessToken) {
122
+ const options = {
123
+ hostname: 'api.github.com',
124
+ path: '/user/emails',
125
+ method: 'GET',
126
+ headers: {
127
+ Authorization: `Bearer ${accessToken}`,
128
+ 'User-Agent': 'MonoDog',
129
+ Accept: 'application/vnd.github+json',
130
+ },
131
+ };
132
+ try {
133
+ const emails = await makeGitHubRequest(options);
134
+ const primaryEmail = emails.find((e) => e.primary && e.verified);
135
+ return primaryEmail?.email || null;
136
+ }
137
+ catch (error) {
138
+ logger_1.AppLogger.warn(`Failed to get user email: ${error}`);
139
+ return null;
140
+ }
141
+ }
142
+ /**
143
+ * Get user's permission for a specific repository
144
+ * Returns the user's permission level in the target repository
145
+ */
146
+ async function getRepositoryPermission(accessToken, owner, repo, username) {
147
+ const options = {
148
+ hostname: 'api.github.com',
149
+ path: `/repos/${owner}/${repo}/collaborators/${username}/permission`,
150
+ method: 'GET',
151
+ headers: {
152
+ Authorization: `Bearer ${accessToken}`,
153
+ 'User-Agent': 'MonoDog',
154
+ Accept: 'application/vnd.github+json',
155
+ },
156
+ };
157
+ try {
158
+ const response = await makeGitHubRequest(options);
159
+ logger_1.AppLogger.debug(`Retrieved permission for ${username} in ${owner}/${repo}: ${response.permission}`);
160
+ return response;
161
+ }
162
+ catch (error) {
163
+ logger_1.AppLogger.warn(`Failed to get repository permission for ${username} in ${owner}/${repo}: ${error}`);
164
+ // If error (likely 404 or no access), return 'none' permission
165
+ return { permission: 'none' };
166
+ }
167
+ }
168
+ /**
169
+ * Map GitHub permission to MonoDog role
170
+ */
171
+ function mapPermissionToRole(permission) {
172
+ switch (permission) {
173
+ case 'admin':
174
+ return 'Admin';
175
+ case 'maintain':
176
+ return 'Maintainer';
177
+ case 'write':
178
+ case 'read':
179
+ return 'Collaborator';
180
+ case 'none':
181
+ default:
182
+ return 'Denied';
183
+ }
184
+ }
185
+ /**
186
+ * Check if user has required permission level
187
+ */
188
+ function hasPermission(userPermission, requiredPermission) {
189
+ const permissionHierarchy = {
190
+ admin: 4,
191
+ maintain: 3,
192
+ write: 2,
193
+ read: 1,
194
+ none: 0,
195
+ };
196
+ return (permissionHierarchy[userPermission] >= permissionHierarchy[requiredPermission]);
197
+ }
198
+ /**
199
+ * Validate OAuth token is still valid
200
+ */
201
+ async function validateToken(accessToken) {
202
+ try {
203
+ await getAuthenticatedUser(accessToken);
204
+ return true;
205
+ }
206
+ catch (error) {
207
+ logger_1.AppLogger.warn(`Token validation failed: ${error}`);
208
+ return false;
209
+ }
210
+ }
211
+ /**
212
+ * Generate OAuth authorization URL
213
+ */
214
+ function generateAuthorizationUrl(clientId, redirectUri, state, scopes = ['read:user', 'user:email', 'repo']) {
215
+ const params = new URLSearchParams({
216
+ client_id: clientId,
217
+ redirect_uri: redirectUri,
218
+ state,
219
+ scope: scopes.join(','),
220
+ allow_signup: 'true',
221
+ });
222
+ return `${GITHUB_OAUTH_BASE}/login/oauth/authorize?${params.toString()}`;
223
+ }
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ /**
3
+ * Permission Service
4
+ * Manages repository permission caching and validation
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.startCacheCleanup = startCacheCleanup;
8
+ exports.getUserRepositoryPermission = getUserRepositoryPermission;
9
+ exports.invalidatePermissionCache = invalidatePermissionCache;
10
+ exports.invalidateUserCache = invalidateUserCache;
11
+ exports.clearAllCache = clearAllCache;
12
+ exports.getCacheStats = getCacheStats;
13
+ exports.canPerformAction = canPerformAction;
14
+ const github_oauth_service_1 = require("./github-oauth-service");
15
+ const logger_1 = require("../middleware/logger");
16
+ // Cache storage: key = `${userId}:${owner}/${repo}`
17
+ const permissionCache = new Map();
18
+ // Configuration
19
+ const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
20
+ const MAX_CACHE_SIZE = 10000;
21
+ const CLEANUP_INTERVAL = 60 * 1000; // 1 minute
22
+ /**
23
+ * Start periodic cleanup of expired cache entries
24
+ */
25
+ function startCacheCleanup() {
26
+ setInterval(() => {
27
+ const now = Date.now();
28
+ let cleanedCount = 0;
29
+ for (const [key, entry] of permissionCache.entries()) {
30
+ if (now > entry.cachedAt + entry.ttl) {
31
+ permissionCache.delete(key);
32
+ cleanedCount++;
33
+ }
34
+ }
35
+ if (cleanedCount > 0) {
36
+ logger_1.AppLogger.debug(`Cleaned ${cleanedCount} expired permission cache entries`);
37
+ }
38
+ }, CLEANUP_INTERVAL);
39
+ }
40
+ /**
41
+ * Generate cache key
42
+ */
43
+ function getCacheKey(userId, owner, repo) {
44
+ return `${userId}:${owner}/${repo}`;
45
+ }
46
+ /**
47
+ * Get cached permission if still valid
48
+ */
49
+ function getCachedPermission(userId, owner, repo) {
50
+ const key = getCacheKey(userId, owner, repo);
51
+ const cached = permissionCache.get(key);
52
+ if (!cached) {
53
+ return null;
54
+ }
55
+ const now = Date.now();
56
+ if (now > cached.cachedAt + cached.ttl) {
57
+ // Cache expired
58
+ permissionCache.delete(key);
59
+ logger_1.AppLogger.debug(`Cache expired for ${key}`);
60
+ return null;
61
+ }
62
+ return cached;
63
+ }
64
+ /**
65
+ * Set permission in cache
66
+ */
67
+ function setCachedPermission(userId, username, owner, repo, permission, ttl = DEFAULT_TTL) {
68
+ const role = (0, github_oauth_service_1.mapPermissionToRole)(permission);
69
+ const cached = {
70
+ userId,
71
+ username,
72
+ owner,
73
+ repo,
74
+ permission,
75
+ role,
76
+ cachedAt: Date.now(),
77
+ ttl,
78
+ };
79
+ const key = getCacheKey(userId, owner, repo);
80
+ // Check cache size and evict oldest if needed
81
+ if (permissionCache.size >= MAX_CACHE_SIZE) {
82
+ let oldestKey = '';
83
+ let oldestTime = Date.now();
84
+ for (const [k, entry] of permissionCache.entries()) {
85
+ if (entry.cachedAt < oldestTime) {
86
+ oldestTime = entry.cachedAt;
87
+ oldestKey = k;
88
+ }
89
+ }
90
+ if (oldestKey) {
91
+ permissionCache.delete(oldestKey);
92
+ logger_1.AppLogger.debug(`Evicted oldest cache entry: ${oldestKey}`);
93
+ }
94
+ }
95
+ permissionCache.set(key, cached);
96
+ logger_1.AppLogger.debug(`Cached permission for ${key}: ${permission}`);
97
+ return cached;
98
+ }
99
+ /**
100
+ * Get user's permission for a repository
101
+ * Checks cache first, then queries GitHub API if needed
102
+ */
103
+ async function getUserRepositoryPermission(accessToken, userId, username, owner, repo, forceRefresh = false) {
104
+ // Check cache if not forcing refresh
105
+ if (!forceRefresh) {
106
+ const cached = getCachedPermission(userId, owner, repo);
107
+ if (cached) {
108
+ return cached;
109
+ }
110
+ }
111
+ // Query GitHub API
112
+ logger_1.AppLogger.debug(`Querying GitHub API for ${username}'s permission in ${owner}/${repo}`);
113
+ try {
114
+ const response = await (0, github_oauth_service_1.getRepositoryPermission)(accessToken, owner, repo, username);
115
+ return setCachedPermission(userId, username, owner, repo, response.permission);
116
+ }
117
+ catch (error) {
118
+ logger_1.AppLogger.error(`Failed to get repository permission: ${error}`);
119
+ // Return 'none' permission on error
120
+ return setCachedPermission(userId, username, owner, repo, 'none');
121
+ }
122
+ }
123
+ /**
124
+ * Invalidate permission cache for a user-repository pair
125
+ */
126
+ function invalidatePermissionCache(userId, owner, repo) {
127
+ const key = getCacheKey(userId, owner, repo);
128
+ permissionCache.delete(key);
129
+ logger_1.AppLogger.debug(`Invalidated cache for ${key}`);
130
+ }
131
+ /**
132
+ * Invalidate all permissions for a user
133
+ */
134
+ function invalidateUserCache(userId) {
135
+ let invalidatedCount = 0;
136
+ for (const key of permissionCache.keys()) {
137
+ if (key.startsWith(`${userId}:`)) {
138
+ permissionCache.delete(key);
139
+ invalidatedCount++;
140
+ }
141
+ }
142
+ logger_1.AppLogger.debug(`Invalidated ${invalidatedCount} cache entries for user ${userId}`);
143
+ }
144
+ /**
145
+ * Clear all permission cache (useful for testing)
146
+ */
147
+ function clearAllCache() {
148
+ const size = permissionCache.size;
149
+ permissionCache.clear();
150
+ logger_1.AppLogger.info(`Cleared all permission cache (${size} entries)`);
151
+ }
152
+ /**
153
+ * Get cache statistics
154
+ */
155
+ function getCacheStats() {
156
+ return {
157
+ size: permissionCache.size,
158
+ capacity: MAX_CACHE_SIZE,
159
+ utilizationPercent: (permissionCache.size / MAX_CACHE_SIZE) * 100,
160
+ };
161
+ }
162
+ /**
163
+ * Check if user can perform an action based on permission
164
+ */
165
+ function canPerformAction(permission, requiredAction) {
166
+ const actionPermissionMap = {
167
+ read: ['read', 'write', 'maintain', 'admin'],
168
+ write: ['write', 'maintain', 'admin'],
169
+ maintain: ['maintain', 'admin'],
170
+ admin: ['admin'],
171
+ };
172
+ const allowedPermissions = actionPermissionMap[requiredAction] || [];
173
+ return allowedPermissions.includes(permission);
174
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub OAuth and Authentication Types
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });