@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,130 @@
1
+ /**
2
+ * Activity Get Team Handler
3
+ * Retrieves team activity summary (managers only)
4
+ *
5
+ * GET /api/activity/team
6
+ * Auth: Cognito JWT required, Manager or Admin role
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
10
+
11
+ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters }) => {
12
+ const Request_ID = requestContext.requestId;
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
+ // Check if user is a manager or admin
20
+ const managerCheck = await executeQuery(`
21
+ SELECT ue."Company_ID", ue."Manager", ue."Admin", u."Super_Admin"
22
+ FROM "UserEntitlements" ue
23
+ JOIN "Users" u ON ue."Email_Address" = u."Email_Address"
24
+ WHERE ue."Email_Address" = $1
25
+ `, [email]);
26
+
27
+ if (managerCheck.rowCount === 0) {
28
+ return createErrorResponse(403, 'Access denied');
29
+ }
30
+
31
+ const userRole = managerCheck.rows[0];
32
+ const isAuthorized = userRole.Manager || userRole.Admin || userRole.Super_Admin;
33
+
34
+ if (!isAuthorized) {
35
+ return createErrorResponse(403, 'Manager or Admin access required');
36
+ }
37
+
38
+ const companyId = queryStringParameters?.company_id || userRole.Company_ID;
39
+
40
+ // Get team summary
41
+ const summaryResult = await executeQuery(`
42
+ SELECT * FROM rapport.v_team_activity_summary
43
+ WHERE company_id = $1
44
+ `, [companyId]);
45
+
46
+ // Get individual developer activity
47
+ const developersResult = await executeQuery(`
48
+ SELECT
49
+ email_address,
50
+ display_name,
51
+ sessions_30d,
52
+ sessions_7d,
53
+ last_session,
54
+ commits_30d,
55
+ commits_7d,
56
+ last_commit,
57
+ days_since_commit,
58
+ prs_merged_30d,
59
+ session_to_commit_conversion_pct
60
+ FROM rapport.mv_developer_activity
61
+ WHERE company_id = $1
62
+ ORDER BY
63
+ CASE
64
+ WHEN days_since_commit IS NULL THEN 999
65
+ ELSE days_since_commit
66
+ END DESC,
67
+ sessions_30d DESC
68
+ `, [companyId]);
69
+
70
+ const summary = summaryResult.rows[0] || {
71
+ total_developers: 0,
72
+ active_developers: 0,
73
+ stale_developers: 0,
74
+ very_stale_developers: 0,
75
+ avg_sessions_30d: 0,
76
+ avg_commits_30d: 0,
77
+ avg_conversion_pct: 0
78
+ };
79
+
80
+ const developers = developersResult.rows.map(dev => ({
81
+ email_address: dev.email_address,
82
+ display_name: dev.display_name,
83
+ sessions: {
84
+ last_30_days: parseInt(dev.sessions_30d) || 0,
85
+ last_7_days: parseInt(dev.sessions_7d) || 0,
86
+ last_session: dev.last_session
87
+ },
88
+ commits: {
89
+ last_30_days: parseInt(dev.commits_30d) || 0,
90
+ last_7_days: parseInt(dev.commits_7d) || 0,
91
+ last_commit: dev.last_commit,
92
+ days_since_commit: parseInt(dev.days_since_commit) || null
93
+ },
94
+ prs_merged_30d: parseInt(dev.prs_merged_30d) || 0,
95
+ session_to_commit_conversion_pct: parseFloat(dev.session_to_commit_conversion_pct) || 0,
96
+ status: getDevStatus(dev)
97
+ }));
98
+
99
+ return createSuccessResponse(
100
+ {
101
+ Records: [{
102
+ company_id: companyId,
103
+ summary: {
104
+ total_developers: parseInt(summary.total_developers) || 0,
105
+ active_developers: parseInt(summary.active_developers) || 0,
106
+ stale_developers: parseInt(summary.stale_developers) || 0,
107
+ very_stale_developers: parseInt(summary.very_stale_developers) || 0,
108
+ avg_sessions_30d: parseFloat(summary.avg_sessions_30d)?.toFixed(1) || 0,
109
+ avg_commits_30d: parseFloat(summary.avg_commits_30d)?.toFixed(1) || 0,
110
+ avg_conversion_pct: parseFloat(summary.avg_conversion_pct)?.toFixed(1) || 0
111
+ },
112
+ developers
113
+ }]
114
+ },
115
+ 'Team activity retrieved',
116
+ { Total_Records: developers.length, Request_ID, Timestamp: new Date().toISOString() }
117
+ );
118
+ });
119
+
120
+ function getDevStatus(dev) {
121
+ const daysSinceCommit = parseInt(dev.days_since_commit);
122
+ const sessions30d = parseInt(dev.sessions_30d) || 0;
123
+ const commits30d = parseInt(dev.commits_30d) || 0;
124
+
125
+ if (daysSinceCommit > 14) return 'very_stale';
126
+ if (daysSinceCommit > 7) return 'stale';
127
+ if (sessions30d === 0 && commits30d > 0) return 'no_ai_usage';
128
+ if (sessions30d > 0 && commits30d === 0) return 'low_output';
129
+ return 'active';
130
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Alerts Acknowledge Handler
3
+ * Acknowledges or resolves an attention alert
4
+ *
5
+ * PUT /api/alerts/{alert_id}/acknowledge
6
+ * Auth: Cognito JWT required, Manager or Admin role
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
10
+
11
+ exports.handler = wrapHandler(async ({ requestContext, pathParameters, body }) => {
12
+ const Request_ID = requestContext.requestId;
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 alertId = pathParameters?.alert_id;
20
+ if (!alertId) {
21
+ return createErrorResponse(400, 'Alert ID required');
22
+ }
23
+
24
+ // Check user role
25
+ const userCheck = await executeQuery(`
26
+ SELECT ue."Company_ID", ue."Manager", ue."Admin", u."Super_Admin"
27
+ FROM "UserEntitlements" ue
28
+ JOIN "Users" u ON ue."Email_Address" = u."Email_Address"
29
+ WHERE ue."Email_Address" = $1
30
+ `, [email]);
31
+
32
+ if (userCheck.rowCount === 0) {
33
+ return createErrorResponse(403, 'Access denied');
34
+ }
35
+
36
+ const userRole = userCheck.rows[0];
37
+ const isManager = userRole.Manager || userRole.Admin || userRole.Super_Admin;
38
+
39
+ if (!isManager) {
40
+ return createErrorResponse(403, 'Manager or Admin access required');
41
+ }
42
+
43
+ const action = body?.action || 'acknowledge'; // acknowledge or resolve
44
+
45
+ // Verify alert exists and belongs to user's company
46
+ const alertCheck = await executeQuery(`
47
+ SELECT alert_id, company_id, status
48
+ FROM rapport.attention_alerts
49
+ WHERE alert_id = $1
50
+ `, [alertId]);
51
+
52
+ if (alertCheck.rowCount === 0) {
53
+ return createErrorResponse(404, 'Alert not found');
54
+ }
55
+
56
+ const alert = alertCheck.rows[0];
57
+ if (alert.company_id !== userRole.Company_ID && !userRole.Super_Admin) {
58
+ return createErrorResponse(403, 'Access denied to this alert');
59
+ }
60
+
61
+ // Update alert
62
+ let result;
63
+ if (action === 'resolve') {
64
+ result = await executeQuery(`
65
+ UPDATE rapport.attention_alerts
66
+ SET
67
+ status = 'resolved',
68
+ acknowledged_by = $1,
69
+ acknowledged_at = COALESCE(acknowledged_at, NOW()),
70
+ resolved_at = NOW()
71
+ WHERE alert_id = $2
72
+ RETURNING *
73
+ `, [email, alertId]);
74
+ } else {
75
+ result = await executeQuery(`
76
+ UPDATE rapport.attention_alerts
77
+ SET
78
+ status = 'acknowledged',
79
+ acknowledged_by = $1,
80
+ acknowledged_at = NOW()
81
+ WHERE alert_id = $2
82
+ RETURNING *
83
+ `, [email, alertId]);
84
+ }
85
+
86
+ return createSuccessResponse(
87
+ { Records: result.rows },
88
+ `Alert ${action}d`,
89
+ { Total_Records: 1, Request_ID, Timestamp: new Date().toISOString() }
90
+ );
91
+ });
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Alerts Get Handler
3
+ * Retrieves attention alerts (own alerts or team alerts for managers)
4
+ *
5
+ * GET /api/alerts
6
+ * Auth: Cognito JWT required
7
+ *
8
+ * Query Parameters:
9
+ * - status: 'active' (default), 'acknowledged', 'resolved', or 'all'
10
+ * - type: Filter by alert type (optional)
11
+ * - severity: Filter by severity (optional)
12
+ */
13
+
14
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
15
+
16
+ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters }) => {
17
+ const Request_ID = requestContext.requestId;
18
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
19
+
20
+ if (!email) {
21
+ return createErrorResponse(401, 'Authentication required');
22
+ }
23
+
24
+ // Check user role
25
+ const userCheck = await executeQuery(`
26
+ SELECT
27
+ ue."Company_ID",
28
+ ue."Manager",
29
+ ue."Admin",
30
+ u."Super_Admin"
31
+ FROM "UserEntitlements" ue
32
+ JOIN "Users" u ON ue."Email_Address" = u."Email_Address"
33
+ WHERE ue."Email_Address" = $1
34
+ `, [email]);
35
+
36
+ if (userCheck.rowCount === 0) {
37
+ return createErrorResponse(403, 'Access denied');
38
+ }
39
+
40
+ const userRole = userCheck.rows[0];
41
+ const isManager = userRole.Manager || userRole.Admin || userRole.Super_Admin;
42
+ const companyId = userRole.Company_ID;
43
+
44
+ // Parse query parameters
45
+ const status = queryStringParameters?.status || 'active';
46
+ const alertType = queryStringParameters?.type || null;
47
+ const severity = queryStringParameters?.severity || null;
48
+
49
+ let result;
50
+
51
+ if (isManager) {
52
+ // Get team alerts with optional filters
53
+ let query = `
54
+ SELECT
55
+ aa.alert_id,
56
+ aa.email_address,
57
+ u."User_Display_Name" as user_name,
58
+ aa.alert_type,
59
+ aa.severity,
60
+ aa.details,
61
+ aa.status,
62
+ aa.acknowledged_by,
63
+ aa.acknowledged_at,
64
+ aa.expires_at,
65
+ aa.created_at
66
+ FROM rapport.attention_alerts aa
67
+ JOIN "Users" u ON aa.email_address = u."Email_Address"
68
+ WHERE aa.company_id = $1
69
+ `;
70
+
71
+ const params = [companyId];
72
+ let paramIndex = 2;
73
+
74
+ // Status filter
75
+ if (status !== 'all') {
76
+ query += ` AND aa.status = $${paramIndex}`;
77
+ params.push(status);
78
+ paramIndex++;
79
+ }
80
+
81
+ // Alert type filter
82
+ if (alertType) {
83
+ query += ` AND aa.alert_type = $${paramIndex}`;
84
+ params.push(alertType);
85
+ paramIndex++;
86
+ }
87
+
88
+ // Severity filter
89
+ if (severity) {
90
+ query += ` AND aa.severity = $${paramIndex}`;
91
+ params.push(severity);
92
+ paramIndex++;
93
+ }
94
+
95
+ // Order by severity (critical > warning > info) then by date
96
+ query += `
97
+ ORDER BY
98
+ CASE aa.severity
99
+ WHEN 'critical' THEN 1
100
+ WHEN 'warning' THEN 2
101
+ ELSE 3
102
+ END,
103
+ aa.created_at DESC
104
+ `;
105
+
106
+ result = await executeQuery(query, params);
107
+ } else {
108
+ // Get own alerts only
109
+ let query = `
110
+ SELECT
111
+ aa.alert_id,
112
+ aa.email_address,
113
+ aa.alert_type,
114
+ aa.severity,
115
+ aa.details,
116
+ aa.status,
117
+ aa.expires_at,
118
+ aa.created_at
119
+ FROM rapport.attention_alerts aa
120
+ WHERE aa.email_address = $1
121
+ `;
122
+
123
+ const params = [email];
124
+ let paramIndex = 2;
125
+
126
+ if (status !== 'all') {
127
+ query += ` AND aa.status = $${paramIndex}`;
128
+ params.push(status);
129
+ paramIndex++;
130
+ }
131
+
132
+ if (alertType) {
133
+ query += ` AND aa.alert_type = $${paramIndex}`;
134
+ params.push(alertType);
135
+ paramIndex++;
136
+ }
137
+
138
+ if (severity) {
139
+ query += ` AND aa.severity = $${paramIndex}`;
140
+ params.push(severity);
141
+ paramIndex++;
142
+ }
143
+
144
+ query += ` ORDER BY aa.created_at DESC`;
145
+
146
+ result = await executeQuery(query, params);
147
+ }
148
+
149
+ const alerts = result.rows.map(alert => ({
150
+ alert_id: alert.alert_id,
151
+ email_address: alert.email_address,
152
+ user_name: alert.user_name || null,
153
+ alert_type: alert.alert_type,
154
+ severity: alert.severity,
155
+ details: alert.details,
156
+ status: alert.status,
157
+ acknowledged_by: alert.acknowledged_by || null,
158
+ acknowledged_at: alert.acknowledged_at || null,
159
+ expires_at: alert.expires_at || null,
160
+ created_at: alert.created_at,
161
+ message: getAlertMessage(alert.alert_type, alert.details),
162
+ recommendation: getAlertRecommendation(alert.alert_type, alert.severity)
163
+ }));
164
+
165
+ // Calculate summary statistics
166
+ const summary = {
167
+ total: alerts.length,
168
+ by_severity: {
169
+ critical: alerts.filter(a => a.severity === 'critical').length,
170
+ warning: alerts.filter(a => a.severity === 'warning').length,
171
+ info: alerts.filter(a => a.severity === 'info').length
172
+ },
173
+ by_type: {}
174
+ };
175
+
176
+ for (const alert of alerts) {
177
+ summary.by_type[alert.alert_type] = (summary.by_type[alert.alert_type] || 0) + 1;
178
+ }
179
+
180
+ return createSuccessResponse(
181
+ { Records: alerts, summary, is_manager_view: isManager },
182
+ 'Alerts retrieved',
183
+ { Total_Records: alerts.length, Request_ID, Timestamp: new Date().toISOString() }
184
+ );
185
+ });
186
+
187
+ /**
188
+ * Generate human-readable message for alert
189
+ */
190
+ function getAlertMessage(alertType, details) {
191
+ switch (alertType) {
192
+ case 'stale_commits':
193
+ return `No commits in ${details.days_since_commit} days`;
194
+
195
+ case 'low_conversion':
196
+ return `Low session-to-commit conversion: ${details.conversion_pct}%`;
197
+
198
+ case 'no_ai_usage':
199
+ return `Active committer not using AI assistance (${details.commits_30d} commits, 0 sessions)`;
200
+
201
+ case 'high_violation_rate':
202
+ return `High standards violation rate: ${details.violation_rate}% (${details.violations}/${details.standards_shown})`;
203
+
204
+ case 'stalled_patterns':
205
+ return `Stalled patterns: ${details.stale_patterns} patterns unused, ${details.provisional_patterns} stuck in provisional`;
206
+
207
+ case 'declining_activity':
208
+ const decline = details.decline_pct || 0;
209
+ return `Activity declined ${decline}% (was ${details.avg_previous_weeks} sessions/week, now ${details.current_week})`;
210
+
211
+ default:
212
+ return `Alert: ${alertType}`;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Generate actionable recommendation for alert
218
+ */
219
+ function getAlertRecommendation(alertType, severity) {
220
+ const recommendations = {
221
+ stale_commits: {
222
+ critical: 'Schedule a 1:1 to check in on progress and identify blockers',
223
+ warning: 'Consider reaching out to offer assistance',
224
+ info: 'Monitor for continued inactivity'
225
+ },
226
+ low_conversion: {
227
+ critical: 'Review session recordings to identify workflow issues',
228
+ warning: 'Check if developer needs additional training on AI tools',
229
+ info: 'Provide tips for effective AI-assisted development'
230
+ },
231
+ no_ai_usage: {
232
+ info: 'Share AI adoption resources and success stories from team'
233
+ },
234
+ high_violation_rate: {
235
+ critical: 'Schedule standards review session with developer',
236
+ warning: 'Share relevant standards documentation',
237
+ info: 'Consider adding standards to onboarding materials'
238
+ },
239
+ stalled_patterns: {
240
+ warning: 'Review patterns for promotion or deprecation',
241
+ info: 'Encourage pattern documentation and sharing'
242
+ },
243
+ declining_activity: {
244
+ warning: 'Check in on workload and well-being',
245
+ info: 'Monitor for continued decline'
246
+ }
247
+ };
248
+
249
+ return recommendations[alertType]?.[severity] || recommendations[alertType]?.info || 'Monitor and follow up as needed';
250
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Collaborator Add Handler
3
+ * Adds a collaborator to a project
4
+ *
5
+ * POST /api/projects/{projectId}/collaborators
6
+ * Body: { email, role }
7
+ * Auth: Cognito JWT required
8
+ */
9
+
10
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, checkCollaboratorBillingLimits, checkEnterpriseSeatLimits } = require('./helpers');
11
+
12
+ /**
13
+ * Add collaborator to project
14
+ * Requires owner or admin role on project
15
+ */
16
+ async function addCollaborator({ pathParameters = {}, body: requestBody = {}, requestContext }) {
17
+ try {
18
+ const Request_ID = requestContext.requestId;
19
+ const inviterEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
+ const { projectId } = pathParameters;
21
+ const { email, role = 'collaborator' } = requestBody;
22
+
23
+ if (!inviterEmail) {
24
+ return createErrorResponse(401, 'Authentication required');
25
+ }
26
+
27
+ if (!projectId) {
28
+ return createErrorResponse(400, 'projectId is required');
29
+ }
30
+
31
+ if (!email) {
32
+ return createErrorResponse(400, 'email is required');
33
+ }
34
+
35
+ // Validate role
36
+ const validRoles = ['admin', 'collaborator', 'viewer'];
37
+ if (!validRoles.includes(role)) {
38
+ return createErrorResponse(400, `Invalid role. Must be one of: ${validRoles.join(', ')}`);
39
+ }
40
+
41
+ // Check inviter has permission (owner or admin)
42
+ const accessQuery = `
43
+ SELECT
44
+ pc.role,
45
+ p.company_id,
46
+ c.client_id,
47
+ c.subscription_tier,
48
+ c.billing_type,
49
+ c.billable_users,
50
+ c.seat_count,
51
+ c.enterprise_package
52
+ FROM rapport.projects p
53
+ JOIN rapport.project_collaborators pc
54
+ ON p.project_id = pc.project_id
55
+ AND pc.email_address = $1
56
+ JOIN rapport.clients c ON p.company_id = c.client_id
57
+ WHERE p.project_id = $2
58
+ `;
59
+ const accessCheck = await executeQuery(accessQuery, [inviterEmail, projectId]);
60
+
61
+ if (accessCheck.rowCount === 0) {
62
+ return createErrorResponse(403, 'You do not have access to this project');
63
+ }
64
+
65
+ const access = accessCheck.rows[0];
66
+ if (access.role !== 'owner' && access.role !== 'admin') {
67
+ return createErrorResponse(403, 'Only project owners and admins can add collaborators');
68
+ }
69
+
70
+ // Check billing/subscription limits based on billing type
71
+ const collaboratorCountQuery = `
72
+ SELECT COUNT(*) as count
73
+ FROM rapport.project_collaborators
74
+ WHERE project_id = $1
75
+ `;
76
+ const countResult = await executeQuery(collaboratorCountQuery, [projectId]);
77
+ const currentCount = parseInt(countResult.rows[0].count) || 0;
78
+
79
+ // For enterprise tier, check seat limits
80
+ if (access.subscription_tier === 'enterprise') {
81
+ // Count total users across all projects for this client
82
+ const totalUsersQuery = `
83
+ SELECT COUNT(DISTINCT pc.email_address) as total_users
84
+ FROM rapport.project_collaborators pc
85
+ JOIN rapport.projects p ON pc.project_id = p.project_id
86
+ WHERE p.company_id = $1
87
+ `;
88
+ const totalUsersResult = await executeQuery(totalUsersQuery, [access.client_id]);
89
+ const totalUsers = parseInt(totalUsersResult.rows[0].total_users) || 0;
90
+
91
+ const seatCheck = checkEnterpriseSeatLimits(
92
+ {
93
+ subscription_tier: access.subscription_tier,
94
+ seat_count: access.seat_count
95
+ },
96
+ totalUsers
97
+ );
98
+
99
+ if (!seatCheck.allowed) {
100
+ return createErrorResponse(403, seatCheck.message, {
101
+ code: 'ENTERPRISE_SEAT_LIMIT',
102
+ seatsUsed: seatCheck.seatsUsed,
103
+ seatCount: seatCheck.seatCount,
104
+ billingAction: seatCheck.billingAction
105
+ });
106
+ }
107
+ }
108
+
109
+ const billingCheck = checkCollaboratorBillingLimits(
110
+ {
111
+ subscription_tier: access.subscription_tier,
112
+ billing_type: access.billing_type
113
+ },
114
+ currentCount
115
+ );
116
+
117
+ if (!billingCheck.allowed) {
118
+ return createErrorResponse(403, billingCheck.message, {
119
+ code: 'SUBSCRIPTION_LIMIT',
120
+ current: currentCount,
121
+ limit: billingCheck.limit
122
+ });
123
+ }
124
+
125
+ // Check if collaborator already exists
126
+ const existsQuery = `
127
+ SELECT email_address FROM rapport.project_collaborators
128
+ WHERE project_id = $1 AND email_address = $2
129
+ `;
130
+ const existsCheck = await executeQuery(existsQuery, [projectId, email]);
131
+
132
+ if (existsCheck.rowCount > 0) {
133
+ return createErrorResponse(409, 'User is already a collaborator on this project');
134
+ }
135
+
136
+ // Check if user exists in system (internal) or is external
137
+ const userQuery = `SELECT email_address FROM rapport.users WHERE email_address = $1`;
138
+ const userCheck = await executeQuery(userQuery, [email]);
139
+ const isExternal = userCheck.rowCount === 0;
140
+
141
+ // Add collaborator
142
+ const insertQuery = `
143
+ INSERT INTO rapport.project_collaborators
144
+ (project_id, email_address, role, invited_by, invited_at, is_external, accepted_at)
145
+ VALUES
146
+ ($1, $2, $3, $4, NOW(), $5, ${isExternal ? 'NULL' : 'NOW()'})
147
+ RETURNING
148
+ project_id,
149
+ email_address,
150
+ role,
151
+ invited_by,
152
+ invited_at,
153
+ is_external,
154
+ accepted_at
155
+ `;
156
+
157
+ const result = await executeQuery(insertQuery, [
158
+ projectId,
159
+ email,
160
+ role,
161
+ inviterEmail,
162
+ isExternal
163
+ ]);
164
+
165
+ const collaborator = result.rows[0];
166
+
167
+ // For enterprise invoice billing, increment billable_users count
168
+ if (billingCheck.billingAction === 'increment_billable_users') {
169
+ await executeQuery(`
170
+ UPDATE rapport.clients
171
+ SET billable_users = COALESCE(billable_users, 0) + 1,
172
+ last_updated = NOW()
173
+ WHERE client_id = $1
174
+ `, [access.client_id]);
175
+ }
176
+
177
+ return createSuccessResponse(
178
+ {
179
+ Records: [{
180
+ ...collaborator,
181
+ status: isExternal ? 'pending_invite' : 'active',
182
+ message: isExternal
183
+ ? 'Collaborator added. Send invite to complete setup.'
184
+ : 'Collaborator added successfully.'
185
+ }]
186
+ },
187
+ isExternal ? 'Collaborator added (pending invite)' : 'Collaborator added',
188
+ {
189
+ Total_Records: 1,
190
+ Request_ID,
191
+ Timestamp: new Date().toISOString()
192
+ }
193
+ );
194
+
195
+ } catch (error) {
196
+ console.error('Handler Error:', error);
197
+ return handleError(error);
198
+ }
199
+ }
200
+
201
+ exports.handler = wrapHandler(addCollaborator);