@equilateral_ai/mindmeld 3.5.2 → 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 (140) hide show
  1. package/hooks/session-end.js +25 -0
  2. package/hooks/session-start.js +363 -83
  3. package/hooks/session-watcher.js +585 -0
  4. package/package.json +19 -13
  5. package/scripts/init-project.js +9 -23
  6. package/src/client/dbShim.js +16 -0
  7. package/src/core/AuthManager.js +3 -2
  8. package/src/handlers/helpers/dbOperations.js +9 -46
  9. package/src/index.js +2 -217
  10. package/src/utils/piiMask.js +16 -0
  11. package/scripts/harvest.js +0 -601
  12. package/scripts/inject.js +0 -409
  13. package/scripts/mcp-bridge.js +0 -220
  14. package/scripts/repo-analyzer.js +0 -870
  15. package/src/collaboration/CollaborationPrompt.js +0 -460
  16. package/src/core/AlertEngine.js +0 -813
  17. package/src/core/AlertNotifier.js +0 -363
  18. package/src/core/CorrelationAnalyzer.js +0 -931
  19. package/src/core/CrossReferenceEngine.js +0 -624
  20. package/src/core/CurationEngine.js +0 -688
  21. package/src/core/DeprecationScheduler.js +0 -183
  22. package/src/core/LoadBearingDetector.js +0 -242
  23. package/src/core/NotificationService.js +0 -1032
  24. package/src/core/RapportOrchestrator.js +0 -632
  25. package/src/core/RelevanceDetector.js +0 -694
  26. package/src/core/StandardLifecycle.js +0 -244
  27. package/src/core/StandardsIngestion.js +0 -991
  28. package/src/core/TeamLoadBearingDetector.js +0 -431
  29. package/src/core/parsers/adrParser.js +0 -479
  30. package/src/core/parsers/cursorRulesParser.js +0 -564
  31. package/src/core/parsers/eslintParser.js +0 -439
  32. package/src/database/dbOperations.js +0 -105
  33. package/src/handlers/activity/activityGetMe.js +0 -98
  34. package/src/handlers/activity/activityGetTeam.js +0 -175
  35. package/src/handlers/admin/adminSetup.js +0 -216
  36. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  37. package/src/handlers/alerts/alertsGet.js +0 -250
  38. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  39. package/src/handlers/analytics/coachingGet.js +0 -361
  40. package/src/handlers/analytics/convergenceGet.js +0 -236
  41. package/src/handlers/analytics/developerScoreGet.js +0 -137
  42. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  43. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  44. package/src/handlers/collaborators/collaboratorList.js +0 -82
  45. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  46. package/src/handlers/collaborators/inviteAccept.js +0 -122
  47. package/src/handlers/company/companyUsersDelete.js +0 -141
  48. package/src/handlers/company/companyUsersGet.js +0 -90
  49. package/src/handlers/company/companyUsersPost.js +0 -267
  50. package/src/handlers/company/companyUsersPut.js +0 -76
  51. package/src/handlers/context/contextGet.js +0 -57
  52. package/src/handlers/context/invariantsGet.js +0 -74
  53. package/src/handlers/context/loopsGet.js +0 -82
  54. package/src/handlers/context/notesCreate.js +0 -74
  55. package/src/handlers/context/purposeGet.js +0 -78
  56. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  57. package/src/handlers/correlations/correlationsGet.js +0 -93
  58. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  59. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  60. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  61. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  62. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  63. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  64. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  65. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  66. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  67. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  68. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  69. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  70. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  71. package/src/handlers/github/githubConnectionStatus.js +0 -49
  72. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  73. package/src/handlers/github/githubOAuthCallback.js +0 -178
  74. package/src/handlers/github/githubOAuthStart.js +0 -59
  75. package/src/handlers/github/githubPatternsReview.js +0 -76
  76. package/src/handlers/github/githubReposList.js +0 -105
  77. package/src/handlers/health/healthGet.js +0 -55
  78. package/src/handlers/helpers/auditLogger.js +0 -201
  79. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  80. package/src/handlers/helpers/decisionFrames.js +0 -29
  81. package/src/handlers/helpers/errorHandler.js +0 -49
  82. package/src/handlers/helpers/index.js +0 -138
  83. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  84. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  85. package/src/handlers/helpers/predictiveCache.js +0 -51
  86. package/src/handlers/helpers/projectAccess.js +0 -88
  87. package/src/handlers/helpers/responseUtil.js +0 -55
  88. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  89. package/src/handlers/mcp/mcpHandler.js +0 -569
  90. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  91. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  92. package/src/handlers/notifications/getPreferences.js +0 -84
  93. package/src/handlers/notifications/sendNotification.js +0 -170
  94. package/src/handlers/notifications/updatePreferences.js +0 -316
  95. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  96. package/src/handlers/patterns/patternUsagePost.js +0 -182
  97. package/src/handlers/patterns/patternViolationPost.js +0 -185
  98. package/src/handlers/projects/projectCreate.js +0 -248
  99. package/src/handlers/projects/projectDelete.js +0 -82
  100. package/src/handlers/projects/projectGet.js +0 -95
  101. package/src/handlers/projects/projectUpdate.js +0 -117
  102. package/src/handlers/reports/aiLeverage.js +0 -210
  103. package/src/handlers/reports/engineeringInvestment.js +0 -132
  104. package/src/handlers/reports/riskForecast.js +0 -206
  105. package/src/handlers/reports/standardsRoi.js +0 -254
  106. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  107. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  108. package/src/handlers/scheduled/generateAlerts.js +0 -135
  109. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  110. package/src/handlers/scheduled/refreshActivity.js +0 -21
  111. package/src/handlers/scheduled/scanCompliance.js +0 -334
  112. package/src/handlers/sessions/sessionEndPost.js +0 -180
  113. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  114. package/src/handlers/standards/catalogGet.js +0 -185
  115. package/src/handlers/standards/catalogSync.js +0 -120
  116. package/src/handlers/standards/discoveriesGet.js +0 -89
  117. package/src/handlers/standards/projectStandardsGet.js +0 -129
  118. package/src/handlers/standards/projectStandardsPut.js +0 -151
  119. package/src/handlers/standards/standardsAuditGet.js +0 -65
  120. package/src/handlers/standards/standardsParseUpload.js +0 -149
  121. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  122. package/src/handlers/standards/standardsTransition.js +0 -161
  123. package/src/handlers/stripe/addonManagePost.js +0 -240
  124. package/src/handlers/stripe/billingPortalPost.js +0 -93
  125. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  126. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  127. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  128. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  129. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  130. package/src/handlers/stripe/webhookPost.js +0 -482
  131. package/src/handlers/user/apiTokenCreate.js +0 -71
  132. package/src/handlers/user/apiTokenList.js +0 -64
  133. package/src/handlers/user/userSplashAck.js +0 -91
  134. package/src/handlers/user/userSplashGet.js +0 -211
  135. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  136. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  137. package/src/handlers/users/userEntitlementsGet.js +0 -89
  138. package/src/handlers/users/userGet.js +0 -118
  139. package/src/handlers/users/userProfilePut.js +0 -77
  140. 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
- }