@equilateral_ai/mindmeld 3.5.3 → 4.0.1

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 (139) hide show
  1. package/hooks/session-start.js +312 -85
  2. package/package.json +20 -14
  3. package/scripts/init-project.js +9 -23
  4. package/src/client/dbShim.js +16 -0
  5. package/src/core/AuthManager.js +3 -2
  6. package/src/handlers/helpers/dbOperations.js +9 -46
  7. package/src/index.js +2 -217
  8. package/src/utils/piiMask.js +16 -0
  9. package/scripts/harvest.js +0 -601
  10. package/scripts/inject.js +0 -409
  11. package/scripts/mcp-bridge.js +0 -220
  12. package/scripts/repo-analyzer.js +0 -870
  13. package/scripts/standards.js +0 -285
  14. package/src/collaboration/CollaborationPrompt.js +0 -460
  15. package/src/core/AlertEngine.js +0 -813
  16. package/src/core/AlertNotifier.js +0 -363
  17. package/src/core/CorrelationAnalyzer.js +0 -931
  18. package/src/core/CrossReferenceEngine.js +0 -624
  19. package/src/core/CurationEngine.js +0 -688
  20. package/src/core/DeprecationScheduler.js +0 -183
  21. package/src/core/LoadBearingDetector.js +0 -242
  22. package/src/core/NotificationService.js +0 -1032
  23. package/src/core/RapportOrchestrator.js +0 -632
  24. package/src/core/RelevanceDetector.js +0 -694
  25. package/src/core/StandardLifecycle.js +0 -244
  26. package/src/core/StandardsIngestion.js +0 -991
  27. package/src/core/TeamLoadBearingDetector.js +0 -431
  28. package/src/core/parsers/adrParser.js +0 -479
  29. package/src/core/parsers/cursorRulesParser.js +0 -564
  30. package/src/core/parsers/eslintParser.js +0 -439
  31. package/src/database/dbOperations.js +0 -105
  32. package/src/handlers/activity/activityGetMe.js +0 -98
  33. package/src/handlers/activity/activityGetTeam.js +0 -175
  34. package/src/handlers/admin/adminSetup.js +0 -216
  35. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  36. package/src/handlers/alerts/alertsGet.js +0 -250
  37. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  38. package/src/handlers/analytics/coachingGet.js +0 -361
  39. package/src/handlers/analytics/convergenceGet.js +0 -236
  40. package/src/handlers/analytics/developerScoreGet.js +0 -137
  41. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  42. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  43. package/src/handlers/collaborators/collaboratorList.js +0 -82
  44. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  45. package/src/handlers/collaborators/inviteAccept.js +0 -122
  46. package/src/handlers/company/companyUsersDelete.js +0 -141
  47. package/src/handlers/company/companyUsersGet.js +0 -90
  48. package/src/handlers/company/companyUsersPost.js +0 -267
  49. package/src/handlers/company/companyUsersPut.js +0 -76
  50. package/src/handlers/context/contextGet.js +0 -57
  51. package/src/handlers/context/invariantsGet.js +0 -74
  52. package/src/handlers/context/loopsGet.js +0 -82
  53. package/src/handlers/context/notesCreate.js +0 -74
  54. package/src/handlers/context/purposeGet.js +0 -78
  55. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  56. package/src/handlers/correlations/correlationsGet.js +0 -93
  57. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  58. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  59. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  60. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  61. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  62. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  63. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  64. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  65. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  66. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  67. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  68. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  69. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  70. package/src/handlers/github/githubConnectionStatus.js +0 -49
  71. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  72. package/src/handlers/github/githubOAuthCallback.js +0 -178
  73. package/src/handlers/github/githubOAuthStart.js +0 -59
  74. package/src/handlers/github/githubPatternsReview.js +0 -76
  75. package/src/handlers/github/githubReposList.js +0 -105
  76. package/src/handlers/health/healthGet.js +0 -55
  77. package/src/handlers/helpers/auditLogger.js +0 -201
  78. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  79. package/src/handlers/helpers/decisionFrames.js +0 -29
  80. package/src/handlers/helpers/errorHandler.js +0 -49
  81. package/src/handlers/helpers/index.js +0 -138
  82. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  83. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  84. package/src/handlers/helpers/predictiveCache.js +0 -51
  85. package/src/handlers/helpers/projectAccess.js +0 -88
  86. package/src/handlers/helpers/responseUtil.js +0 -55
  87. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  88. package/src/handlers/mcp/mcpHandler.js +0 -569
  89. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  90. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  91. package/src/handlers/notifications/getPreferences.js +0 -84
  92. package/src/handlers/notifications/sendNotification.js +0 -170
  93. package/src/handlers/notifications/updatePreferences.js +0 -316
  94. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  95. package/src/handlers/patterns/patternUsagePost.js +0 -182
  96. package/src/handlers/patterns/patternViolationPost.js +0 -185
  97. package/src/handlers/projects/projectCreate.js +0 -248
  98. package/src/handlers/projects/projectDelete.js +0 -82
  99. package/src/handlers/projects/projectGet.js +0 -95
  100. package/src/handlers/projects/projectUpdate.js +0 -117
  101. package/src/handlers/reports/aiLeverage.js +0 -210
  102. package/src/handlers/reports/engineeringInvestment.js +0 -132
  103. package/src/handlers/reports/riskForecast.js +0 -206
  104. package/src/handlers/reports/standardsRoi.js +0 -254
  105. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  106. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  107. package/src/handlers/scheduled/generateAlerts.js +0 -135
  108. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  109. package/src/handlers/scheduled/refreshActivity.js +0 -21
  110. package/src/handlers/scheduled/scanCompliance.js +0 -334
  111. package/src/handlers/sessions/sessionEndPost.js +0 -180
  112. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  113. package/src/handlers/standards/catalogGet.js +0 -185
  114. package/src/handlers/standards/catalogSync.js +0 -120
  115. package/src/handlers/standards/discoveriesGet.js +0 -89
  116. package/src/handlers/standards/projectStandardsGet.js +0 -129
  117. package/src/handlers/standards/projectStandardsPut.js +0 -151
  118. package/src/handlers/standards/standardsAuditGet.js +0 -65
  119. package/src/handlers/standards/standardsParseUpload.js +0 -149
  120. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  121. package/src/handlers/standards/standardsTransition.js +0 -161
  122. package/src/handlers/stripe/addonManagePost.js +0 -240
  123. package/src/handlers/stripe/billingPortalPost.js +0 -93
  124. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  125. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  126. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  127. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  128. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  129. package/src/handlers/stripe/webhookPost.js +0 -482
  130. package/src/handlers/user/apiTokenCreate.js +0 -71
  131. package/src/handlers/user/apiTokenList.js +0 -64
  132. package/src/handlers/user/userSplashAck.js +0 -91
  133. package/src/handlers/user/userSplashGet.js +0 -211
  134. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  135. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  136. package/src/handlers/users/userEntitlementsGet.js +0 -89
  137. package/src/handlers/users/userGet.js +0 -118
  138. package/src/handlers/users/userProfilePut.js +0 -77
  139. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,114 +0,0 @@
1
- /**
2
- * Cognito PreSignUp Handler
3
- * Blocks bot registrations before user creation in Cognito
4
- * Auto-confirms Google OAuth users and admin-invited users
5
- *
6
- * Triggered by: Cognito User Pool pre-signup trigger
7
- *
8
- * Note: Cognito trigger must be configured manually in Cognito console
9
- * as SAM cannot reference external user pools
10
- */
11
-
12
- const { executeQuery } = require('./helpers');
13
-
14
- const DISPOSABLE_DOMAINS = new Set([
15
- 'mailinator.com', 'guerrillamail.com', 'tempmail.com',
16
- 'throwaway.email', 'yopmail.com', 'sharklasers.com',
17
- 'guerrillamailblock.com', 'grr.la', 'maildrop.cc',
18
- 'dispostable.com', 'temp-mail.org', '10minutemail.com',
19
- 'trashmail.com', 'fakeinbox.com', 'mailnesia.com',
20
- 'tempinbox.com', 'mailcatch.com', 'throwam.com'
21
- ]);
22
-
23
- /**
24
- * Detect dot-stuffed Gmail addresses used by signup bots
25
- * Gmail ignores dots in the local part, so bots insert random dots
26
- * to generate unique-looking addresses that all deliver to the same inbox
27
- */
28
- function isDotStuffedGmail(email) {
29
- const atIndex = email.lastIndexOf('@');
30
- if (atIndex === -1) return false;
31
-
32
- const localPart = email.substring(0, atIndex).toLowerCase();
33
- const domain = email.substring(atIndex + 1).toLowerCase();
34
-
35
- if (domain !== 'gmail.com' && domain !== 'googlemail.com') return false;
36
-
37
- const dotCount = (localPart.match(/\./g) || []).length;
38
- const cleanLength = localPart.replace(/\./g, '').length;
39
-
40
- if (cleanLength === 0) return true;
41
-
42
- // 4+ dots in a short local part is a strong bot signal
43
- if (dotCount >= 4 && cleanLength < 20) return true;
44
-
45
- // More than 30% dots relative to actual characters
46
- if (dotCount > 0 && dotCount / cleanLength > 0.3) return true;
47
-
48
- return false;
49
- }
50
-
51
- /**
52
- * Check if email uses a known disposable email domain
53
- */
54
- function isDisposableEmail(email) {
55
- const domain = email.toLowerCase().split('@')[1];
56
- return DISPOSABLE_DOMAINS.has(domain);
57
- }
58
-
59
- async function handler(event) {
60
- console.log('PreSignUp trigger:', JSON.stringify({
61
- triggerSource: event.triggerSource,
62
- userName: event.userName,
63
- email: event.request?.userAttributes?.email
64
- }));
65
-
66
- // Auto-confirm external provider signups (Google OAuth)
67
- // Google provides verified email — no Cognito verification needed
68
- if (event.triggerSource === 'PreSignUp_ExternalProvider') {
69
- console.log(`[PreSignUp] Auto-confirming external provider user: ${event.request?.userAttributes?.email}`);
70
- event.response.autoConfirmUser = true;
71
- event.response.autoVerifyEmail = true;
72
- return event;
73
- }
74
-
75
- // Only validate user-initiated signups (not admin-created)
76
- if (event.triggerSource !== 'PreSignUp_SignUp') {
77
- return event;
78
- }
79
-
80
- const email = event.request?.userAttributes?.email;
81
- if (!email) {
82
- throw new Error('Email address is required for registration');
83
- }
84
-
85
- if (isDisposableEmail(email)) {
86
- console.log(`[PreSignUp] Blocked disposable email: ${email}`);
87
- throw new Error('Please use a permanent email address to register.');
88
- }
89
-
90
- if (isDotStuffedGmail(email)) {
91
- console.log(`[PreSignUp] Blocked dot-stuffed Gmail: ${email}`);
92
- throw new Error('This email address format is not accepted. Please use your primary email address.');
93
- }
94
-
95
- // Auto-confirm users who have been invited (entitlement already exists)
96
- try {
97
- const result = await executeQuery(
98
- 'SELECT 1 FROM rapport.user_entitlements WHERE email_address = $1 LIMIT 1',
99
- [email.toLowerCase()]
100
- );
101
- if (result.rows.length > 0) {
102
- console.log(`[PreSignUp] Auto-confirming invited user: ${email}`);
103
- event.response.autoConfirmUser = true;
104
- event.response.autoVerifyEmail = true;
105
- }
106
- } catch (err) {
107
- // DB check failed — don't block signup, just skip auto-confirm
108
- console.error(`[PreSignUp] Entitlement check failed for ${email}:`, err.message);
109
- }
110
-
111
- return event;
112
- }
113
-
114
- module.exports = { handler };
@@ -1,89 +0,0 @@
1
- /**
2
- * User Entitlements Get Handler
3
- * Retrieves user's company/project access rights
4
- *
5
- * GET /api/users/entitlements
6
- * Auth: Cognito JWT required
7
- */
8
-
9
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
10
-
11
- /**
12
- * Get current user's entitlements
13
- */
14
- async function getEntitlements({ requestContext }) {
15
- try {
16
- const Request_ID = requestContext.requestId;
17
- // REST API: requestContext.authorizer.claims.email
18
- // HTTP API: requestContext.authorizer.jwt.claims.email
19
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
-
21
- if (!email) {
22
- return createErrorResponse(401, 'Authentication required');
23
- }
24
-
25
- // Get all entitlements with company and client details
26
- const query = `
27
- SELECT
28
- ue.email_address,
29
- ue.client_id,
30
- c.client_name,
31
- c.subscription_tier,
32
- ue.company_id,
33
- co.company_name,
34
- ue.admin,
35
- ue.member,
36
- ue.create_date
37
- FROM rapport.user_entitlements ue
38
- JOIN rapport.clients c ON ue.client_id = c.client_id
39
- JOIN rapport.companies co ON ue.company_id = co.company_id
40
- WHERE ue.email_address = $1
41
- AND c.active = true
42
- AND co.active = true
43
- ORDER BY c.client_name, co.company_name
44
- `;
45
-
46
- const result = await executeQuery(query, [email]);
47
-
48
- // Group by client
49
- const clientMap = {};
50
- for (const row of result.rows) {
51
- if (!clientMap[row.client_id]) {
52
- clientMap[row.client_id] = {
53
- client_id: row.client_id,
54
- client_name: row.client_name,
55
- subscription_tier: row.subscription_tier,
56
- companies: []
57
- };
58
- }
59
- clientMap[row.client_id].companies.push({
60
- company_id: row.company_id,
61
- company_name: row.company_name,
62
- admin: row.admin,
63
- member: row.member,
64
- since: row.create_date
65
- });
66
- }
67
-
68
- const clients = Object.values(clientMap);
69
-
70
- return createSuccessResponse(
71
- {
72
- Records: clients
73
- },
74
- 'Entitlements retrieved',
75
- {
76
- Total_Records: clients.length,
77
- Total_Companies: result.rowCount,
78
- Request_ID,
79
- Timestamp: new Date().toISOString()
80
- }
81
- );
82
-
83
- } catch (error) {
84
- console.error('Handler Error:', error);
85
- return handleError(error);
86
- }
87
- }
88
-
89
- exports.handler = wrapHandler(getEntitlements);
@@ -1,118 +0,0 @@
1
- /**
2
- * User Get Handler
3
- * Retrieves user profile and subscription info
4
- *
5
- * GET /api/users/me
6
- * Auth: Cognito JWT required
7
- */
8
-
9
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, getTierConfig } = require('./helpers');
10
-
11
- /**
12
- * Get current user's profile
13
- */
14
- async function getUser({ requestContext }) {
15
- try {
16
- const Request_ID = requestContext.requestId;
17
- // REST API: requestContext.authorizer.claims.email
18
- // HTTP API: requestContext.authorizer.jwt.claims.email
19
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
-
21
- if (!email) {
22
- return createErrorResponse(401, 'Authentication required');
23
- }
24
-
25
- // Get user with their primary client subscription and company
26
- const query = `
27
- SELECT
28
- u.email_address,
29
- u.first_name,
30
- u.last_name,
31
- u.client_id,
32
- u.user_status,
33
- u.create_date,
34
- c.client_name,
35
- c.subscription_tier,
36
- c.subscription_status,
37
- c.subscription_ends_at,
38
- c.stripe_customer_id,
39
- ue.company_id
40
- FROM rapport.users u
41
- LEFT JOIN rapport.clients c ON u.client_id = c.client_id
42
- LEFT JOIN rapport.user_entitlements ue ON u.email_address = ue.email_address
43
- WHERE u.email_address = $1
44
- AND u.active = true
45
- LIMIT 1
46
- `;
47
-
48
- const result = await executeQuery(query, [email]);
49
-
50
- if (result.rowCount === 0) {
51
- return createErrorResponse(404, 'User not found');
52
- }
53
-
54
- const user = result.rows[0];
55
- const tierConfig = getTierConfig(user.subscription_tier || 'free');
56
-
57
- // Get usage counts scoped to user's entitled companies
58
- const usageQuery = `
59
- SELECT
60
- (SELECT COUNT(*) FROM rapport.user_entitlements WHERE client_id = $1) as collaborators,
61
- (SELECT COUNT(*) FROM rapport.projects p
62
- JOIN rapport.user_entitlements ue ON p.company_id = ue.company_id
63
- WHERE ue.email_address = $2 AND p.archived = false) as projects,
64
- (SELECT COUNT(*) FROM rapport.invariants i
65
- JOIN rapport.projects p ON i.project_id = p.project_id
66
- JOIN rapport.user_entitlements ue ON p.company_id = ue.company_id
67
- WHERE ue.email_address = $2) as invariants
68
- `;
69
-
70
- const usageResult = await executeQuery(usageQuery, [user.client_id, email]);
71
- const usage = usageResult.rows[0];
72
-
73
- return createSuccessResponse(
74
- {
75
- Records: [{
76
- email_address: user.email_address,
77
- first_name: user.first_name,
78
- last_name: user.last_name,
79
- client_id: user.client_id,
80
- company_id: user.company_id,
81
- client_name: user.client_name,
82
- user_status: user.user_status,
83
- member_since: user.create_date,
84
- subscription: {
85
- tier: user.subscription_tier || 'free',
86
- tier_name: tierConfig?.displayName || 'Free',
87
- status: user.subscription_status || 'active',
88
- ends_at: user.subscription_ends_at,
89
- has_stripe: !!user.stripe_customer_id,
90
- features: tierConfig?.features || []
91
- },
92
- usage: {
93
- collaborators: parseInt(usage.collaborators) || 0,
94
- projects: parseInt(usage.projects) || 0,
95
- invariants: parseInt(usage.invariants) || 0
96
- },
97
- limits: {
98
- max_collaborators: tierConfig?.maxCollaborators ?? null,
99
- max_projects: tierConfig?.maxProjects ?? null,
100
- max_invariants: tierConfig?.maxInvariants ?? null
101
- }
102
- }]
103
- },
104
- 'User profile retrieved',
105
- {
106
- Total_Records: 1,
107
- Request_ID,
108
- Timestamp: new Date().toISOString()
109
- }
110
- );
111
-
112
- } catch (error) {
113
- console.error('Handler Error:', error);
114
- return handleError(error);
115
- }
116
- }
117
-
118
- exports.handler = wrapHandler(getUser);
@@ -1,77 +0,0 @@
1
- /**
2
- * User Profile Update Handler
3
- * Updates user display name (first_name, last_name)
4
- *
5
- * PUT /api/users/profile
6
- * Auth: Cognito JWT required
7
- */
8
-
9
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
10
-
11
- /**
12
- * Update current user's profile
13
- */
14
- async function updateProfile({ body, requestContext }) {
15
- try {
16
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
17
-
18
- if (!email) {
19
- return createErrorResponse(401, 'Authentication required');
20
- }
21
-
22
- const { first_name, last_name } = JSON.parse(body || '{}');
23
-
24
- if (!first_name && !last_name) {
25
- return createErrorResponse(400, 'At least one of first_name or last_name is required');
26
- }
27
-
28
- // Build dynamic update
29
- const setClauses = [];
30
- const params = [];
31
- let paramIndex = 1;
32
-
33
- if (first_name !== undefined) {
34
- setClauses.push(`first_name = $${paramIndex++}`);
35
- params.push(first_name.trim().substring(0, 100));
36
- }
37
- if (last_name !== undefined) {
38
- setClauses.push(`last_name = $${paramIndex++}`);
39
- params.push(last_name.trim().substring(0, 100));
40
- }
41
-
42
- setClauses.push(`last_updated = NOW()`);
43
- params.push(email);
44
-
45
- const query = `
46
- UPDATE rapport.users
47
- SET ${setClauses.join(', ')}
48
- WHERE email_address = $${paramIndex}
49
- AND active = true
50
- RETURNING email_address, first_name, last_name
51
- `;
52
-
53
- const result = await executeQuery(query, params);
54
-
55
- if (result.rowCount === 0) {
56
- return createErrorResponse(404, 'User not found');
57
- }
58
-
59
- const user = result.rows[0];
60
-
61
- return createSuccessResponse(
62
- {
63
- email_address: user.email_address,
64
- first_name: user.first_name,
65
- last_name: user.last_name,
66
- display_name: [user.first_name, user.last_name].filter(Boolean).join(' ')
67
- },
68
- 'Profile updated'
69
- );
70
-
71
- } catch (error) {
72
- console.error('Handler Error:', error);
73
- return handleError(error);
74
- }
75
- }
76
-
77
- exports.handler = wrapHandler(updateProfile);
@@ -1,215 +0,0 @@
1
- /**
2
- * GitHub Webhook Handler
3
- * Receives push and pull_request events to track commits and PRs
4
- *
5
- * POST /api/webhooks/github
6
- * Auth: GitHub webhook signature verification (no Cognito)
7
- */
8
-
9
- const crypto = require('crypto');
10
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
11
-
12
- exports.handler = wrapHandler(async ({ body, headers }) => {
13
- // Verify webhook signature — try per-repo secret first, then global fallback
14
- const signature = headers['x-hub-signature-256'] || headers['X-Hub-Signature-256'];
15
- const rawBody = JSON.stringify(body);
16
- const repoFullName = body?.repository?.full_name;
17
-
18
- let verified = false;
19
-
20
- // Try per-repo secret from git_repositories
21
- if (repoFullName) {
22
- const repoResult = await executeQuery(`
23
- SELECT webhook_secret FROM rapport.git_repositories
24
- WHERE repo_name = $1 AND webhook_secret IS NOT NULL
25
- LIMIT 1
26
- `, [repoFullName]);
27
-
28
- if (repoResult.rowCount > 0 && repoResult.rows[0].webhook_secret) {
29
- verified = verifySignature(rawBody, signature, repoResult.rows[0].webhook_secret);
30
- }
31
- }
32
-
33
- // Fallback to global secret
34
- if (!verified) {
35
- const globalSecret = process.env.GITHUB_WEBHOOK_SECRET;
36
- if (globalSecret) {
37
- verified = verifySignature(rawBody, signature, globalSecret);
38
- }
39
- }
40
-
41
- if (!verified) {
42
- return createErrorResponse(401, 'Invalid signature');
43
- }
44
-
45
- const eventType = headers['x-github-event'] || headers['X-GitHub-Event'];
46
-
47
- if (eventType === 'push') {
48
- await handlePushEvent(body);
49
- } else if (eventType === 'pull_request') {
50
- await handlePREvent(body);
51
- } else if (eventType === 'ping') {
52
- // GitHub sends ping when webhook is first configured
53
- return createSuccessResponse({ received: true }, 'Webhook configured');
54
- }
55
-
56
- return createSuccessResponse(
57
- { received: true, event: eventType },
58
- 'Webhook processed'
59
- );
60
- });
61
-
62
- function verifySignature(payload, signature, secret) {
63
- if (!signature) return false;
64
-
65
- const expectedSignature = 'sha256=' + crypto
66
- .createHmac('sha256', secret)
67
- .update(payload)
68
- .digest('hex');
69
-
70
- return crypto.timingSafeEqual(
71
- Buffer.from(signature),
72
- Buffer.from(expectedSignature)
73
- );
74
- }
75
-
76
- async function handlePushEvent(payload) {
77
- const repoFullName = payload.repository?.full_name;
78
- if (!repoFullName) return;
79
-
80
- const repoUrl = payload.repository?.html_url || `https://github.com/${repoFullName}`;
81
- const branch = payload.ref?.replace('refs/heads/', '') || 'unknown';
82
-
83
- // Find or create repo in git_repositories
84
- let repoResult = await executeQuery(`
85
- SELECT repo_id, company_id FROM rapport.git_repositories
86
- WHERE repo_name = $1 OR repo_url = $2
87
- `, [repoFullName, repoUrl]);
88
-
89
- if (repoResult.rowCount === 0) {
90
- // Look up company from projects table
91
- const projectResult = await executeQuery(`
92
- SELECT project_id, company_id FROM rapport.projects
93
- WHERE repo_url LIKE $1
94
- LIMIT 1
95
- `, [`%${repoFullName}%`]);
96
-
97
- const companyId = projectResult.rows[0]?.company_id || null;
98
-
99
- // Auto-register the repo
100
- repoResult = await executeQuery(`
101
- INSERT INTO rapport.git_repositories (repo_id, repo_name, repo_url, company_id, default_branch, created_at, updated_at)
102
- VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW())
103
- RETURNING repo_id, company_id
104
- `, [repoFullName, repoUrl, companyId, branch]);
105
-
106
- console.log(`Auto-registered repo: ${repoFullName} (repo_id: ${repoResult.rows[0].repo_id})`);
107
- }
108
-
109
- const repoId = repoResult.rows[0].repo_id;
110
-
111
- for (const commit of (payload.commits || [])) {
112
- await recordCommit(commit, repoId, repoFullName, repoUrl);
113
- }
114
-
115
- // Update last_commit_sha on the repo
116
- const lastCommit = payload.head_commit?.id || payload.commits?.[payload.commits.length - 1]?.id;
117
- if (lastCommit) {
118
- await executeQuery(`
119
- UPDATE rapport.git_repositories SET last_commit_sha = $1, updated_at = NOW()
120
- WHERE repo_id = $2
121
- `, [lastCommit, repoId]);
122
- }
123
- }
124
-
125
- async function recordCommit(commit, repoId, repoName, repoUrl) {
126
- const authorEmail = commit.author?.email;
127
- if (!authorEmail) return;
128
-
129
- // Check for duplicate
130
- const existing = await executeQuery(`
131
- SELECT 1 FROM rapport.commits WHERE commit_sha = $1 LIMIT 1
132
- `, [commit.id]);
133
-
134
- if (existing.rowCount > 0) {
135
- console.log(`Commit ${commit.id.substring(0, 8)} already recorded, skipping`);
136
- return;
137
- }
138
-
139
- // Build changed_files JSONB from GitHub payload
140
- const changedFiles = [
141
- ...(commit.added || []).map(f => ({ path: f, action: 'added' })),
142
- ...(commit.modified || []).map(f => ({ path: f, action: 'modified' })),
143
- ...(commit.removed || []).map(f => ({ path: f, action: 'removed' }))
144
- ];
145
-
146
- await executeQuery(`
147
- INSERT INTO rapport.commits (
148
- commit_id, commit_sha, repo_id, repo_url, repo_name,
149
- author_email, author_name, commit_timestamp, message,
150
- changed_files, is_merge, created_at
151
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
152
- `, [
153
- commit.id,
154
- commit.id,
155
- repoId,
156
- repoUrl,
157
- repoName,
158
- authorEmail,
159
- commit.author?.name || authorEmail,
160
- commit.timestamp,
161
- commit.message?.substring(0, 2000),
162
- JSON.stringify(changedFiles),
163
- commit.message?.toLowerCase().startsWith('merge') || false
164
- ]);
165
-
166
- console.log(`Recorded commit ${commit.id.substring(0, 8)} by ${authorEmail}`);
167
- }
168
-
169
- async function handlePREvent(payload) {
170
- const pr = payload.pull_request;
171
- const repoFullName = payload.repository?.full_name;
172
- if (!pr || !repoFullName) return;
173
-
174
- const repoUrl = payload.repository?.html_url || `https://github.com/${repoFullName}`;
175
-
176
- // Find repo
177
- const repoResult = await executeQuery(`
178
- SELECT repo_id FROM rapport.git_repositories
179
- WHERE repo_name = $1 OR repo_url = $2
180
- `, [repoFullName, repoUrl]);
181
-
182
- if (repoResult.rowCount === 0) {
183
- console.log(`Repo not found for: ${repoFullName} — push event must arrive first`);
184
- return;
185
- }
186
-
187
- const repoId = repoResult.rows[0].repo_id;
188
- const authorEmail = pr.user?.email || `${pr.user?.login}@users.noreply.github.com`;
189
- const status = pr.merged ? 'merged' : pr.state;
190
-
191
- // Upsert PR by repo_id + pr_number
192
- await executeQuery(`
193
- INSERT INTO rapport.pull_requests (
194
- pr_id, repo_id, pr_number, title, author_email,
195
- status, created_at, merged_at, closed_at,
196
- lines_added, lines_removed, files_changed, review_comments
197
- ) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
198
- ON CONFLICT (pr_id) DO NOTHING
199
- `, [
200
- repoId,
201
- pr.number,
202
- pr.title?.substring(0, 500),
203
- authorEmail,
204
- status,
205
- pr.created_at,
206
- pr.merged_at || null,
207
- pr.closed_at || null,
208
- pr.additions || 0,
209
- pr.deletions || 0,
210
- pr.changed_files || 0,
211
- pr.review_comments || 0
212
- ]);
213
-
214
- console.log(`Recorded PR #${pr.number} (${status}) for ${authorEmail}`);
215
- }