@equilateral_ai/mindmeld 3.5.3 → 4.0.2

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