@equilateral_ai/mindmeld 3.0.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/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Callback Handler
|
|
3
|
+
* Receives GET redirect from GitHub, exchanges code for token, redirects to frontend
|
|
4
|
+
*
|
|
5
|
+
* GET /api/github/oauth/callback (No auth - GitHub redirect)
|
|
6
|
+
* Query: ?code=...&state=...
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery } = require('../helpers');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
|
|
13
|
+
const FRONTEND_URL = process.env.FRONTEND_URL || 'https://app.mindmeld.dev';
|
|
14
|
+
|
|
15
|
+
function redirect(url) {
|
|
16
|
+
return {
|
|
17
|
+
statusCode: 302,
|
|
18
|
+
headers: {
|
|
19
|
+
'Location': url,
|
|
20
|
+
'Access-Control-Allow-Origin': '*'
|
|
21
|
+
},
|
|
22
|
+
body: ''
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function httpsRequest(options, postData) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const req = https.request(options, (res) => {
|
|
29
|
+
let data = '';
|
|
30
|
+
res.on('data', chunk => data += chunk);
|
|
31
|
+
res.on('end', () => {
|
|
32
|
+
try {
|
|
33
|
+
resolve({ statusCode: res.statusCode, body: JSON.parse(data) });
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// GitHub token endpoint may return form-encoded
|
|
36
|
+
const parsed = {};
|
|
37
|
+
data.split('&').forEach(pair => {
|
|
38
|
+
const [key, val] = pair.split('=');
|
|
39
|
+
if (key) parsed[decodeURIComponent(key)] = decodeURIComponent(val || '');
|
|
40
|
+
});
|
|
41
|
+
resolve({ statusCode: res.statusCode, body: parsed });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
req.on('error', reject);
|
|
46
|
+
if (postData) req.write(postData);
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function encryptToken(token, key) {
|
|
52
|
+
const iv = crypto.randomBytes(16);
|
|
53
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
|
54
|
+
let encrypted = cipher.update(token, 'utf8', 'hex');
|
|
55
|
+
encrypted += cipher.final('hex');
|
|
56
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function githubOAuthCallback({ queryStringParameters }) {
|
|
60
|
+
try {
|
|
61
|
+
const code = queryStringParameters.code;
|
|
62
|
+
const state = queryStringParameters.state;
|
|
63
|
+
|
|
64
|
+
if (!code || !state) {
|
|
65
|
+
console.error('Missing code or state in callback');
|
|
66
|
+
return redirect(`${FRONTEND_URL}/onboarding?error=missing_params`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Look up state token in DB to identify user
|
|
70
|
+
const stateResult = await executeQuery(`
|
|
71
|
+
SELECT email_address FROM rapport.github_oauth_states
|
|
72
|
+
WHERE state_token = $1 AND used = FALSE
|
|
73
|
+
AND created_at > NOW() - INTERVAL '15 minutes'
|
|
74
|
+
`, [state]);
|
|
75
|
+
|
|
76
|
+
if (stateResult.rowCount === 0) {
|
|
77
|
+
console.error('Invalid or expired state token');
|
|
78
|
+
return redirect(`${FRONTEND_URL}/onboarding?error=invalid_state`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const email = stateResult.rows[0].email_address;
|
|
82
|
+
|
|
83
|
+
// Mark state as used
|
|
84
|
+
await executeQuery(`
|
|
85
|
+
UPDATE rapport.github_oauth_states SET used = TRUE
|
|
86
|
+
WHERE state_token = $1
|
|
87
|
+
`, [state]);
|
|
88
|
+
|
|
89
|
+
// Exchange code for access token
|
|
90
|
+
const tokenPayload = JSON.stringify({
|
|
91
|
+
client_id: process.env.GITHUB_OAUTH_CLIENT_ID,
|
|
92
|
+
client_secret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
|
|
93
|
+
code: code
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const tokenResponse = await httpsRequest({
|
|
97
|
+
hostname: 'github.com',
|
|
98
|
+
path: '/login/oauth/access_token',
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
'Accept': 'application/json',
|
|
103
|
+
'Content-Length': Buffer.byteLength(tokenPayload)
|
|
104
|
+
}
|
|
105
|
+
}, tokenPayload);
|
|
106
|
+
|
|
107
|
+
const accessToken = tokenResponse.body.access_token;
|
|
108
|
+
if (!accessToken) {
|
|
109
|
+
console.error('GitHub token exchange failed:', tokenResponse.body);
|
|
110
|
+
return redirect(`${FRONTEND_URL}/onboarding?error=token_exchange_failed`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tokenScope = tokenResponse.body.scope || '';
|
|
114
|
+
|
|
115
|
+
// Get GitHub user info
|
|
116
|
+
const userResponse = await httpsRequest({
|
|
117
|
+
hostname: 'api.github.com',
|
|
118
|
+
path: '/user',
|
|
119
|
+
method: 'GET',
|
|
120
|
+
headers: {
|
|
121
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
122
|
+
'User-Agent': 'MindMeld-App',
|
|
123
|
+
'Accept': 'application/json'
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (userResponse.statusCode !== 200) {
|
|
128
|
+
console.error('GitHub user fetch failed:', userResponse.statusCode);
|
|
129
|
+
return redirect(`${FRONTEND_URL}/onboarding?error=github_user_failed`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const githubUser = userResponse.body;
|
|
133
|
+
|
|
134
|
+
// Encrypt the access token
|
|
135
|
+
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
|
136
|
+
if (!encryptionKey) {
|
|
137
|
+
console.error('GITHUB_TOKEN_ENCRYPTION_KEY not configured');
|
|
138
|
+
return redirect(`${FRONTEND_URL}/onboarding?error=config_error`);
|
|
139
|
+
}
|
|
140
|
+
const encryptedToken = encryptToken(accessToken, encryptionKey);
|
|
141
|
+
|
|
142
|
+
// Upsert GitHub connection
|
|
143
|
+
await executeQuery(`
|
|
144
|
+
INSERT INTO rapport.github_connections (
|
|
145
|
+
email_address, github_username, github_user_id,
|
|
146
|
+
access_token_encrypted, token_scope, connected_at
|
|
147
|
+
) VALUES ($1, $2, $3, $4, $5, NOW())
|
|
148
|
+
ON CONFLICT (email_address) DO UPDATE SET
|
|
149
|
+
github_username = EXCLUDED.github_username,
|
|
150
|
+
github_user_id = EXCLUDED.github_user_id,
|
|
151
|
+
access_token_encrypted = EXCLUDED.access_token_encrypted,
|
|
152
|
+
token_scope = EXCLUDED.token_scope,
|
|
153
|
+
connected_at = NOW(),
|
|
154
|
+
revoked = FALSE
|
|
155
|
+
`, [email, githubUser.login, githubUser.id, encryptedToken, tokenScope]);
|
|
156
|
+
|
|
157
|
+
// Success - redirect to onboarding step 2
|
|
158
|
+
return redirect(`${FRONTEND_URL}/onboarding?step=2`);
|
|
159
|
+
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('GitHub OAuth Callback Error:', error);
|
|
162
|
+
return redirect(`${FRONTEND_URL}/onboarding?error=server_error`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
exports.handler = wrapHandler(githubOAuthCallback);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Start Handler
|
|
3
|
+
* Generates GitHub OAuth authorization URL
|
|
4
|
+
*
|
|
5
|
+
* GET /api/github/oauth/start (CognitoAuthorizer)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('../helpers');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
async function githubOAuthStart({ requestContext }) {
|
|
12
|
+
try {
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Authentication required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const clientId = process.env.GITHUB_OAUTH_CLIENT_ID;
|
|
20
|
+
if (!clientId) {
|
|
21
|
+
return createErrorResponse(500, 'GitHub OAuth not configured');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Generate unique state token and store in DB for CSRF verification
|
|
25
|
+
const stateToken = crypto.randomBytes(32).toString('hex');
|
|
26
|
+
|
|
27
|
+
// Clean up old unused states for this user, then insert new one
|
|
28
|
+
await executeQuery(`
|
|
29
|
+
DELETE FROM rapport.github_oauth_states
|
|
30
|
+
WHERE email_address = $1 OR created_at < NOW() - INTERVAL '15 minutes'
|
|
31
|
+
`, [email]);
|
|
32
|
+
|
|
33
|
+
await executeQuery(`
|
|
34
|
+
INSERT INTO rapport.github_oauth_states (state_token, email_address)
|
|
35
|
+
VALUES ($1, $2)
|
|
36
|
+
`, [stateToken, email]);
|
|
37
|
+
|
|
38
|
+
const callbackUrl = process.env.GITHUB_OAUTH_CALLBACK_URL || 'https://api.mindmeld.dev/api/github/oauth/callback';
|
|
39
|
+
|
|
40
|
+
const params = new URLSearchParams({
|
|
41
|
+
client_id: clientId,
|
|
42
|
+
redirect_uri: callbackUrl,
|
|
43
|
+
scope: 'repo',
|
|
44
|
+
state: stateToken
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const authorization_url = `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
48
|
+
|
|
49
|
+
return createSuccessResponse(
|
|
50
|
+
{ authorization_url },
|
|
51
|
+
'OAuth URL generated'
|
|
52
|
+
);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('GitHub OAuth Start Error:', error);
|
|
55
|
+
return createErrorResponse(500, 'Failed to generate OAuth URL');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
exports.handler = wrapHandler(githubOAuthStart);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Patterns Review Handler
|
|
3
|
+
* Processes user's approve/reject decisions on discovered patterns
|
|
4
|
+
*
|
|
5
|
+
* PUT /api/github/patterns/review (CognitoAuthorizer)
|
|
6
|
+
* Body: { project_id, approvals: [{ discovery_id, approved }] }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('../helpers');
|
|
10
|
+
|
|
11
|
+
async function githubPatternsReview({ body, requestContext }) {
|
|
12
|
+
try {
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Authentication required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { project_id, approvals } = body;
|
|
20
|
+
if (!project_id || !approvals || !Array.isArray(approvals)) {
|
|
21
|
+
return createErrorResponse(400, 'project_id and approvals array are required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Verify user has access to project
|
|
25
|
+
const accessResult = await executeQuery(`
|
|
26
|
+
SELECT role FROM rapport.project_collaborators
|
|
27
|
+
WHERE project_id = $1 AND email_address = $2
|
|
28
|
+
`, [project_id, email]);
|
|
29
|
+
|
|
30
|
+
if (accessResult.rowCount === 0) {
|
|
31
|
+
return createErrorResponse(403, 'Access denied to project');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let patterns_created = 0;
|
|
35
|
+
let patterns_rejected = 0;
|
|
36
|
+
|
|
37
|
+
for (const { discovery_id, approved } of approvals) {
|
|
38
|
+
if (!discovery_id) continue;
|
|
39
|
+
|
|
40
|
+
if (approved) {
|
|
41
|
+
// Get discovery details
|
|
42
|
+
const discoveryResult = await executeQuery(`
|
|
43
|
+
SELECT discovery_type, pattern_name, pattern_description, evidence
|
|
44
|
+
FROM rapport.onboarding_discoveries
|
|
45
|
+
WHERE discovery_id = $1 AND project_id = $2
|
|
46
|
+
`, [discovery_id, project_id]);
|
|
47
|
+
|
|
48
|
+
if (discoveryResult.rowCount === 0) continue;
|
|
49
|
+
|
|
50
|
+
const discovery = discoveryResult.rows[0];
|
|
51
|
+
|
|
52
|
+
// Create pattern entry
|
|
53
|
+
const patternId = `pat_${project_id}_${Date.now()}_${patterns_created}`;
|
|
54
|
+
await executeQuery(`
|
|
55
|
+
INSERT INTO rapport.patterns (
|
|
56
|
+
pattern_id, project_id, intent, constraints, outcome_criteria,
|
|
57
|
+
maturity, discovered_by, pattern_data
|
|
58
|
+
) VALUES ($1, $2, $3, $4, $5, 'provisional', $6, $7)
|
|
59
|
+
`, [
|
|
60
|
+
patternId,
|
|
61
|
+
project_id,
|
|
62
|
+
discovery.pattern_name,
|
|
63
|
+
JSON.stringify([discovery.pattern_description || '']),
|
|
64
|
+
JSON.stringify([`Discovered via GitHub onboarding: ${discovery.discovery_type}`]),
|
|
65
|
+
email,
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
source: 'github_onboarding',
|
|
68
|
+
discovery_type: discovery.discovery_type,
|
|
69
|
+
evidence: discovery.evidence
|
|
70
|
+
})
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
// Update discovery status
|
|
74
|
+
await executeQuery(`
|
|
75
|
+
UPDATE rapport.onboarding_discoveries
|
|
76
|
+
SET status = 'approved', reviewed_at = NOW(), pattern_id = $1
|
|
77
|
+
WHERE discovery_id = $2
|
|
78
|
+
`, [patternId, discovery_id]);
|
|
79
|
+
|
|
80
|
+
patterns_created++;
|
|
81
|
+
} else {
|
|
82
|
+
// Reject
|
|
83
|
+
await executeQuery(`
|
|
84
|
+
UPDATE rapport.onboarding_discoveries
|
|
85
|
+
SET status = 'rejected', reviewed_at = NOW()
|
|
86
|
+
WHERE discovery_id = $1 AND project_id = $2
|
|
87
|
+
`, [discovery_id, project_id]);
|
|
88
|
+
|
|
89
|
+
patterns_rejected++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Mark onboarding as completed
|
|
94
|
+
await executeQuery(`
|
|
95
|
+
UPDATE rapport.projects SET onboarding_completed = TRUE
|
|
96
|
+
WHERE project_id = $1
|
|
97
|
+
`, [project_id]);
|
|
98
|
+
|
|
99
|
+
return createSuccessResponse(
|
|
100
|
+
{ patterns_created, patterns_rejected },
|
|
101
|
+
'Patterns reviewed successfully'
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('GitHub Patterns Review Error:', error);
|
|
105
|
+
return createErrorResponse(500, 'Failed to process pattern reviews');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
exports.handler = wrapHandler(githubPatternsReview);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Repos List Handler
|
|
3
|
+
* Lists user's GitHub repositories
|
|
4
|
+
*
|
|
5
|
+
* GET /api/github/repos (CognitoAuthorizer)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('../helpers');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const https = require('https');
|
|
11
|
+
|
|
12
|
+
function httpsGet(options) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const req = https.request(options, (res) => {
|
|
15
|
+
let data = '';
|
|
16
|
+
res.on('data', chunk => data += chunk);
|
|
17
|
+
res.on('end', () => {
|
|
18
|
+
try {
|
|
19
|
+
resolve({ statusCode: res.statusCode, body: JSON.parse(data) });
|
|
20
|
+
} catch (e) {
|
|
21
|
+
resolve({ statusCode: res.statusCode, body: data });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
req.on('error', reject);
|
|
26
|
+
req.end();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function decryptToken(encryptedData, key) {
|
|
31
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
32
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
33
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
|
34
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
35
|
+
decrypted += decipher.final('utf8');
|
|
36
|
+
return decrypted;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function githubReposList({ requestContext }) {
|
|
40
|
+
try {
|
|
41
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
42
|
+
|
|
43
|
+
if (!email) {
|
|
44
|
+
return createErrorResponse(401, 'Authentication required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get stored connection
|
|
48
|
+
const connResult = await executeQuery(`
|
|
49
|
+
SELECT access_token_encrypted FROM rapport.github_connections
|
|
50
|
+
WHERE email_address = $1 AND revoked = FALSE
|
|
51
|
+
`, [email]);
|
|
52
|
+
|
|
53
|
+
if (connResult.rowCount === 0) {
|
|
54
|
+
return createErrorResponse(404, 'No GitHub connection found. Please connect GitHub first.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Decrypt token
|
|
58
|
+
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
|
59
|
+
const accessToken = decryptToken(connResult.rows[0].access_token_encrypted, encryptionKey);
|
|
60
|
+
|
|
61
|
+
// Fetch repos from GitHub
|
|
62
|
+
const reposResponse = await httpsGet({
|
|
63
|
+
hostname: 'api.github.com',
|
|
64
|
+
path: '/user/repos?sort=updated&per_page=30&type=owner',
|
|
65
|
+
method: 'GET',
|
|
66
|
+
headers: {
|
|
67
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
68
|
+
'User-Agent': 'MindMeld-App',
|
|
69
|
+
'Accept': 'application/json'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (reposResponse.statusCode !== 200) {
|
|
74
|
+
console.error('GitHub repos fetch failed:', reposResponse.statusCode);
|
|
75
|
+
return createErrorResponse(502, 'Failed to fetch repositories from GitHub');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Update last_used_at
|
|
79
|
+
await executeQuery(`
|
|
80
|
+
UPDATE rapport.github_connections SET last_used_at = NOW()
|
|
81
|
+
WHERE email_address = $1
|
|
82
|
+
`, [email]);
|
|
83
|
+
|
|
84
|
+
// Map to minimal repo info
|
|
85
|
+
const repos = reposResponse.body.map(repo => ({
|
|
86
|
+
name: repo.name,
|
|
87
|
+
full_name: repo.full_name,
|
|
88
|
+
language: repo.language,
|
|
89
|
+
default_branch: repo.default_branch,
|
|
90
|
+
private: repo.private,
|
|
91
|
+
html_url: repo.html_url,
|
|
92
|
+
updated_at: repo.updated_at
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
return createSuccessResponse(
|
|
96
|
+
{ repos },
|
|
97
|
+
'Repositories fetched successfully'
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('GitHub Repos List Error:', error);
|
|
101
|
+
return createErrorResponse(500, 'Failed to list repositories');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
exports.handler = wrapHandler(githubReposList);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Super Admin Check Helper
|
|
3
|
+
* Follows Tim-Combo pattern: simple boolean flag in Users table
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { executeQuery } = require('./dbOperations');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if user is a superadmin
|
|
10
|
+
* @param {string} email - User email
|
|
11
|
+
* @returns {Promise<boolean>}
|
|
12
|
+
*/
|
|
13
|
+
async function isSuperAdmin(email) {
|
|
14
|
+
const query = `
|
|
15
|
+
SELECT "Super_Admin"
|
|
16
|
+
FROM "Users"
|
|
17
|
+
WHERE "Email_Address" = $1
|
|
18
|
+
`;
|
|
19
|
+
const result = await executeQuery(query, [email]);
|
|
20
|
+
return result.rows.length > 0 && result.rows[0].Super_Admin === true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Require superadmin access - throws 403 if not superadmin
|
|
25
|
+
* @param {string} email - User email
|
|
26
|
+
* @throws {Error} 403 Forbidden if not superadmin
|
|
27
|
+
*/
|
|
28
|
+
async function requireSuperAdmin(email) {
|
|
29
|
+
const isAdmin = await isSuperAdmin(email);
|
|
30
|
+
if (!isAdmin) {
|
|
31
|
+
const error = new Error('Superadmin access required');
|
|
32
|
+
error.statusCode = 403;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get user with superadmin status
|
|
39
|
+
* @param {string} email - User email
|
|
40
|
+
* @returns {Promise<Object|null>}
|
|
41
|
+
*/
|
|
42
|
+
async function getUserWithSuperAdminStatus(email) {
|
|
43
|
+
const query = `
|
|
44
|
+
SELECT
|
|
45
|
+
"Email_Address",
|
|
46
|
+
"Client_ID",
|
|
47
|
+
"User_Display_Name",
|
|
48
|
+
"First_Name",
|
|
49
|
+
"Last_Name",
|
|
50
|
+
"Super_Admin",
|
|
51
|
+
"User_Status"
|
|
52
|
+
FROM "Users"
|
|
53
|
+
WHERE "Email_Address" = $1
|
|
54
|
+
`;
|
|
55
|
+
const result = await executeQuery(query, [email]);
|
|
56
|
+
return result.rows.length > 0 ? result.rows[0] : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Log superadmin action to audit trail
|
|
61
|
+
* @param {Object} params - Audit parameters
|
|
62
|
+
*/
|
|
63
|
+
async function logSuperAdminAction({ email, action, targetType, targetId, details, ipAddress, userAgent }) {
|
|
64
|
+
const query = `
|
|
65
|
+
INSERT INTO superadmin_audit_log (
|
|
66
|
+
email, action, target_type, target_id, details, ip_address, user_agent
|
|
67
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
68
|
+
`;
|
|
69
|
+
await executeQuery(query, [
|
|
70
|
+
email,
|
|
71
|
+
action,
|
|
72
|
+
targetType || null,
|
|
73
|
+
targetId || null,
|
|
74
|
+
details ? JSON.stringify(details) : null,
|
|
75
|
+
ipAddress || null,
|
|
76
|
+
userAgent || null
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
isSuperAdmin,
|
|
82
|
+
requireSuperAdmin,
|
|
83
|
+
getUserWithSuperAdminStatus,
|
|
84
|
+
logSuperAdminAction
|
|
85
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Operations Helper
|
|
3
|
+
* Cached PostgreSQL client (Lambda database standards compliant)
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: Uses single cached client (NOT connection pool)
|
|
6
|
+
* Follows lambda_database_standards.md
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { Client } = require('pg');
|
|
10
|
+
|
|
11
|
+
// Cached client for connection reuse across warm invocations
|
|
12
|
+
let cachedClient = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get database client (cached)
|
|
16
|
+
* Reuses connection across Lambda invocations
|
|
17
|
+
*/
|
|
18
|
+
async function getClient() {
|
|
19
|
+
if (cachedClient && cachedClient._connected) {
|
|
20
|
+
return cachedClient;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create new client with environment variables (resolved at deployment)
|
|
24
|
+
// AWS RDS requires SSL for all connections
|
|
25
|
+
cachedClient = new Client({
|
|
26
|
+
host: process.env.DB_HOST,
|
|
27
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
28
|
+
database: process.env.DB_NAME,
|
|
29
|
+
user: process.env.DB_USER,
|
|
30
|
+
password: process.env.DB_PASS || process.env.DB_PASSWORD, // Support both naming conventions
|
|
31
|
+
ssl: { rejectUnauthorized: false } // Required for AWS RDS
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await cachedClient.connect();
|
|
35
|
+
cachedClient._connected = true;
|
|
36
|
+
|
|
37
|
+
console.log('Database client connected');
|
|
38
|
+
|
|
39
|
+
return cachedClient;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute query using cached client
|
|
44
|
+
* @param {string} query - SQL query
|
|
45
|
+
* @param {array} params - Query parameters
|
|
46
|
+
* @returns {Promise<object>} Query result
|
|
47
|
+
*/
|
|
48
|
+
async function executeQuery(query, params = []) {
|
|
49
|
+
const client = await getClient();
|
|
50
|
+
return client.query(query, params);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { executeQuery };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Helper
|
|
3
|
+
* Standard error handling and logging for all Rapport handlers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle error with standard response
|
|
8
|
+
* @param {Error} error - Error object
|
|
9
|
+
* @returns {object} Lambda error response
|
|
10
|
+
*/
|
|
11
|
+
function handleError(error) {
|
|
12
|
+
console.error('Error:', error);
|
|
13
|
+
|
|
14
|
+
// Determine status code
|
|
15
|
+
const statusCode = error.statusCode || 500;
|
|
16
|
+
|
|
17
|
+
// Determine message
|
|
18
|
+
let message = 'Internal server error';
|
|
19
|
+
if (error.statusCode) {
|
|
20
|
+
message = error.message;
|
|
21
|
+
} else if (error.code === '23505') {
|
|
22
|
+
message = 'Resource already exists';
|
|
23
|
+
} else if (error.code === '23503') {
|
|
24
|
+
message = 'Referenced resource not found';
|
|
25
|
+
} else if (error.code === '23502') {
|
|
26
|
+
message = 'Required field missing';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
statusCode,
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Access-Control-Allow-Origin': '*',
|
|
34
|
+
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
|
|
35
|
+
'Access-Control-Allow-Headers': 'Content-Type,Authorization'
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
success: false,
|
|
39
|
+
message,
|
|
40
|
+
error: process.env.NODE_ENV === 'dev' ? {
|
|
41
|
+
code: error.code,
|
|
42
|
+
detail: error.detail,
|
|
43
|
+
stack: error.stack
|
|
44
|
+
} : undefined
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { handleError };
|