@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.
Files changed (86) hide show
  1. package/README.md +300 -0
  2. package/hooks/README.md +494 -0
  3. package/hooks/pre-compact.js +392 -0
  4. package/hooks/session-start.js +264 -0
  5. package/package.json +90 -0
  6. package/scripts/harvest.js +561 -0
  7. package/scripts/init-project.js +437 -0
  8. package/scripts/inject.js +388 -0
  9. package/src/collaboration/CollaborationPrompt.js +460 -0
  10. package/src/core/AlertEngine.js +813 -0
  11. package/src/core/AlertNotifier.js +363 -0
  12. package/src/core/CorrelationAnalyzer.js +774 -0
  13. package/src/core/CurationEngine.js +688 -0
  14. package/src/core/LLMPatternDetector.js +508 -0
  15. package/src/core/LoadBearingDetector.js +242 -0
  16. package/src/core/NotificationService.js +1032 -0
  17. package/src/core/PatternValidator.js +355 -0
  18. package/src/core/README.md +160 -0
  19. package/src/core/RapportOrchestrator.js +446 -0
  20. package/src/core/RelevanceDetector.js +577 -0
  21. package/src/core/StandardsIngestion.js +575 -0
  22. package/src/core/TeamLoadBearingDetector.js +431 -0
  23. package/src/database/dbOperations.js +105 -0
  24. package/src/handlers/activity/activityGetMe.js +98 -0
  25. package/src/handlers/activity/activityGetTeam.js +130 -0
  26. package/src/handlers/alerts/alertsAcknowledge.js +91 -0
  27. package/src/handlers/alerts/alertsGet.js +250 -0
  28. package/src/handlers/collaborators/collaboratorAdd.js +201 -0
  29. package/src/handlers/collaborators/collaboratorInvite.js +218 -0
  30. package/src/handlers/collaborators/collaboratorList.js +88 -0
  31. package/src/handlers/collaborators/collaboratorRemove.js +127 -0
  32. package/src/handlers/collaborators/inviteAccept.js +122 -0
  33. package/src/handlers/context/contextGet.js +57 -0
  34. package/src/handlers/context/invariantsGet.js +74 -0
  35. package/src/handlers/context/loopsGet.js +82 -0
  36. package/src/handlers/context/notesCreate.js +74 -0
  37. package/src/handlers/context/purposeGet.js +78 -0
  38. package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
  39. package/src/handlers/correlations/correlationsGet.js +93 -0
  40. package/src/handlers/correlations/correlationsProjectGet.js +161 -0
  41. package/src/handlers/github/githubConnectionStatus.js +49 -0
  42. package/src/handlers/github/githubDiscoverPatterns.js +364 -0
  43. package/src/handlers/github/githubOAuthCallback.js +166 -0
  44. package/src/handlers/github/githubOAuthStart.js +59 -0
  45. package/src/handlers/github/githubPatternsReview.js +109 -0
  46. package/src/handlers/github/githubReposList.js +105 -0
  47. package/src/handlers/helpers/checkSuperAdmin.js +85 -0
  48. package/src/handlers/helpers/dbOperations.js +53 -0
  49. package/src/handlers/helpers/errorHandler.js +49 -0
  50. package/src/handlers/helpers/index.js +106 -0
  51. package/src/handlers/helpers/lambdaWrapper.js +60 -0
  52. package/src/handlers/helpers/responseUtil.js +55 -0
  53. package/src/handlers/helpers/subscriptionTiers.js +1168 -0
  54. package/src/handlers/notifications/getPreferences.js +84 -0
  55. package/src/handlers/notifications/sendNotification.js +170 -0
  56. package/src/handlers/notifications/updatePreferences.js +316 -0
  57. package/src/handlers/patterns/patternUsagePost.js +182 -0
  58. package/src/handlers/patterns/patternViolationPost.js +185 -0
  59. package/src/handlers/projects/projectCreate.js +107 -0
  60. package/src/handlers/projects/projectDelete.js +82 -0
  61. package/src/handlers/projects/projectGet.js +95 -0
  62. package/src/handlers/projects/projectUpdate.js +118 -0
  63. package/src/handlers/reports/aiLeverage.js +206 -0
  64. package/src/handlers/reports/engineeringInvestment.js +132 -0
  65. package/src/handlers/reports/riskForecast.js +186 -0
  66. package/src/handlers/reports/standardsRoi.js +162 -0
  67. package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
  68. package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
  69. package/src/handlers/scheduled/generateAlerts.js +135 -0
  70. package/src/handlers/scheduled/refreshActivity.js +21 -0
  71. package/src/handlers/scheduled/scanCompliance.js +334 -0
  72. package/src/handlers/sessions/sessionEndPost.js +180 -0
  73. package/src/handlers/sessions/sessionStandardsPost.js +135 -0
  74. package/src/handlers/stripe/addonManagePost.js +240 -0
  75. package/src/handlers/stripe/billingPortalPost.js +93 -0
  76. package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
  77. package/src/handlers/stripe/seatsUpdatePost.js +185 -0
  78. package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
  79. package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
  80. package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
  81. package/src/handlers/stripe/webhookPost.js +454 -0
  82. package/src/handlers/users/cognitoPostConfirmation.js +150 -0
  83. package/src/handlers/users/userEntitlementsGet.js +89 -0
  84. package/src/handlers/users/userGet.js +114 -0
  85. package/src/handlers/webhooks/githubWebhook.js +223 -0
  86. 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 };