@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.
- package/hooks/session-start.js +312 -85
- package/package.json +20 -14
- package/scripts/init-project.js +9 -23
- package/src/client/dbShim.js +16 -0
- package/src/core/AuthManager.js +3 -2
- package/src/handlers/helpers/dbOperations.js +9 -46
- package/src/index.js +2 -217
- package/src/utils/piiMask.js +16 -0
- package/scripts/harvest.js +0 -601
- package/scripts/inject.js +0 -409
- package/scripts/mcp-bridge.js +0 -220
- package/scripts/repo-analyzer.js +0 -870
- package/scripts/standards.js +0 -285
- package/src/collaboration/CollaborationPrompt.js +0 -460
- package/src/core/AlertEngine.js +0 -813
- package/src/core/AlertNotifier.js +0 -363
- package/src/core/CorrelationAnalyzer.js +0 -931
- package/src/core/CrossReferenceEngine.js +0 -624
- package/src/core/CurationEngine.js +0 -688
- package/src/core/DeprecationScheduler.js +0 -183
- package/src/core/LoadBearingDetector.js +0 -242
- package/src/core/NotificationService.js +0 -1032
- package/src/core/RapportOrchestrator.js +0 -632
- package/src/core/RelevanceDetector.js +0 -694
- package/src/core/StandardLifecycle.js +0 -244
- package/src/core/StandardsIngestion.js +0 -991
- package/src/core/TeamLoadBearingDetector.js +0 -431
- package/src/core/parsers/adrParser.js +0 -479
- package/src/core/parsers/cursorRulesParser.js +0 -564
- package/src/core/parsers/eslintParser.js +0 -439
- package/src/database/dbOperations.js +0 -105
- package/src/handlers/activity/activityGetMe.js +0 -98
- package/src/handlers/activity/activityGetTeam.js +0 -175
- package/src/handlers/admin/adminSetup.js +0 -216
- package/src/handlers/alerts/alertsAcknowledge.js +0 -92
- package/src/handlers/alerts/alertsGet.js +0 -250
- package/src/handlers/analytics/activitySummaryGet.js +0 -234
- package/src/handlers/analytics/coachingGet.js +0 -361
- package/src/handlers/analytics/convergenceGet.js +0 -236
- package/src/handlers/analytics/developerScoreGet.js +0 -137
- package/src/handlers/collaborators/collaboratorAdd.js +0 -200
- package/src/handlers/collaborators/collaboratorInvite.js +0 -219
- package/src/handlers/collaborators/collaboratorList.js +0 -82
- package/src/handlers/collaborators/collaboratorRemove.js +0 -128
- package/src/handlers/collaborators/inviteAccept.js +0 -122
- package/src/handlers/company/companyUsersDelete.js +0 -141
- package/src/handlers/company/companyUsersGet.js +0 -90
- package/src/handlers/company/companyUsersPost.js +0 -267
- package/src/handlers/company/companyUsersPut.js +0 -76
- package/src/handlers/context/contextGet.js +0 -57
- package/src/handlers/context/invariantsGet.js +0 -74
- package/src/handlers/context/loopsGet.js +0 -82
- package/src/handlers/context/notesCreate.js +0 -74
- package/src/handlers/context/purposeGet.js +0 -78
- package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
- package/src/handlers/correlations/correlationsGet.js +0 -93
- package/src/handlers/correlations/correlationsProjectGet.js +0 -153
- package/src/handlers/enterprise/controlTowerGet.js +0 -224
- package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
- package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
- package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
- package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
- package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
- package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
- package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
- package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
- package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
- package/src/handlers/github/githubConnectionStatus.js +0 -49
- package/src/handlers/github/githubDiscoverPatterns.js +0 -621
- package/src/handlers/github/githubOAuthCallback.js +0 -178
- package/src/handlers/github/githubOAuthStart.js +0 -59
- package/src/handlers/github/githubPatternsReview.js +0 -76
- package/src/handlers/github/githubReposList.js +0 -105
- package/src/handlers/health/healthGet.js +0 -55
- package/src/handlers/helpers/auditLogger.js +0 -201
- package/src/handlers/helpers/checkSuperAdmin.js +0 -84
- package/src/handlers/helpers/decisionFrames.js +0 -29
- package/src/handlers/helpers/errorHandler.js +0 -49
- package/src/handlers/helpers/index.js +0 -138
- package/src/handlers/helpers/lambdaWrapper.js +0 -60
- package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
- package/src/handlers/helpers/predictiveCache.js +0 -51
- package/src/handlers/helpers/projectAccess.js +0 -88
- package/src/handlers/helpers/responseUtil.js +0 -55
- package/src/handlers/helpers/subscriptionTiers.js +0 -1168
- package/src/handlers/mcp/mcpHandler.js +0 -569
- package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
- package/src/handlers/notifications/getPreferences.js +0 -84
- package/src/handlers/notifications/sendNotification.js +0 -170
- package/src/handlers/notifications/updatePreferences.js +0 -316
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
- package/src/handlers/patterns/patternUsagePost.js +0 -182
- package/src/handlers/patterns/patternViolationPost.js +0 -185
- package/src/handlers/projects/projectCreate.js +0 -248
- package/src/handlers/projects/projectDelete.js +0 -82
- package/src/handlers/projects/projectGet.js +0 -95
- package/src/handlers/projects/projectUpdate.js +0 -117
- package/src/handlers/reports/aiLeverage.js +0 -210
- package/src/handlers/reports/engineeringInvestment.js +0 -132
- package/src/handlers/reports/riskForecast.js +0 -206
- package/src/handlers/reports/standardsRoi.js +0 -254
- package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
- package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
- package/src/handlers/scheduled/generateAlerts.js +0 -135
- package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
- package/src/handlers/scheduled/refreshActivity.js +0 -21
- package/src/handlers/scheduled/scanCompliance.js +0 -334
- package/src/handlers/sessions/sessionEndPost.js +0 -180
- package/src/handlers/sessions/sessionStandardsPost.js +0 -171
- package/src/handlers/standards/catalogGet.js +0 -185
- package/src/handlers/standards/catalogSync.js +0 -120
- package/src/handlers/standards/discoveriesGet.js +0 -89
- package/src/handlers/standards/projectStandardsGet.js +0 -129
- package/src/handlers/standards/projectStandardsPut.js +0 -151
- package/src/handlers/standards/standardsAuditGet.js +0 -65
- package/src/handlers/standards/standardsParseUpload.js +0 -149
- package/src/handlers/standards/standardsRelevantPost.js +0 -405
- package/src/handlers/standards/standardsTransition.js +0 -161
- package/src/handlers/stripe/addonManagePost.js +0 -240
- package/src/handlers/stripe/billingPortalPost.js +0 -93
- package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
- package/src/handlers/stripe/seatsUpdatePost.js +0 -185
- package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
- package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
- package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
- package/src/handlers/stripe/webhookPost.js +0 -482
- package/src/handlers/user/apiTokenCreate.js +0 -71
- package/src/handlers/user/apiTokenList.js +0 -64
- package/src/handlers/user/userSplashAck.js +0 -91
- package/src/handlers/user/userSplashGet.js +0 -211
- package/src/handlers/users/cognitoPostConfirmation.js +0 -186
- package/src/handlers/users/cognitoPreSignUp.js +0 -114
- package/src/handlers/users/userEntitlementsGet.js +0 -89
- package/src/handlers/users/userGet.js +0 -118
- package/src/handlers/users/userProfilePut.js +0 -77
- package/src/handlers/webhooks/githubWebhook.js +0 -215
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Developer Score Analytics Handler
|
|
3
|
-
* Quality scoring and standards adherence metrics per developer
|
|
4
|
-
*
|
|
5
|
-
* GET /api/analytics/developer-scores
|
|
6
|
-
* Query params:
|
|
7
|
-
* - project_id (required) - Project to scope metrics
|
|
8
|
-
* - period (optional, default: 30d) - 30d or 90d
|
|
9
|
-
*
|
|
10
|
-
* Auth: Cognito JWT required, Manager or Admin role
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
14
|
-
|
|
15
|
-
async function getDeveloperScores({ requestContext, queryStringParameters }) {
|
|
16
|
-
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
17
|
-
|
|
18
|
-
if (!email) {
|
|
19
|
-
return createErrorResponse(401, 'Unauthorized');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const params = queryStringParameters || {};
|
|
23
|
-
const projectId = params.project_id;
|
|
24
|
-
const period = params.period || '30d';
|
|
25
|
-
|
|
26
|
-
if (!projectId) {
|
|
27
|
-
return createErrorResponse(400, 'project_id is required');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!['30d', '90d'].includes(period)) {
|
|
31
|
-
return createErrorResponse(400, 'period must be 30d or 90d');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Validate access - must be manager/admin with access to this project
|
|
35
|
-
const accessCheck = await executeQuery(`
|
|
36
|
-
SELECT ue.company_id
|
|
37
|
-
FROM rapport.user_entitlements ue
|
|
38
|
-
JOIN rapport.projects p ON p.company_id = ue.company_id
|
|
39
|
-
WHERE ue.email_address = $1
|
|
40
|
-
AND p.project_id = $2
|
|
41
|
-
AND (ue.admin = true OR ue.manager = true)
|
|
42
|
-
LIMIT 1
|
|
43
|
-
`, [email, projectId]);
|
|
44
|
-
|
|
45
|
-
if (accessCheck.rows.length === 0) {
|
|
46
|
-
return createErrorResponse(403, 'Manager or Admin access required for this project');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const companyId = accessCheck.rows[0].company_id;
|
|
50
|
-
const periodDays = period === '90d' ? 90 : 30;
|
|
51
|
-
const periodStart = new Date();
|
|
52
|
-
periodStart.setDate(periodStart.getDate() - periodDays);
|
|
53
|
-
|
|
54
|
-
// Fetch developer metrics — join pattern_usage through sessions for project scope
|
|
55
|
-
const developerResult = await executeQuery(`
|
|
56
|
-
SELECT
|
|
57
|
-
u.email_address as user_email,
|
|
58
|
-
CONCAT(u.first_name, ' ', u.last_name) as display_name,
|
|
59
|
-
COALESCE(pu_counts.patterns_used, 0) as patterns_used,
|
|
60
|
-
COALESCE(dm_scores.compliance_score, 0) as compliance_score,
|
|
61
|
-
MAX(sc.session_started_at) as last_active
|
|
62
|
-
FROM rapport.user_entitlements ue
|
|
63
|
-
JOIN rapport.users u ON u.email_address = ue.email_address
|
|
64
|
-
LEFT JOIN (
|
|
65
|
-
SELECT pu.email_address, COUNT(*) as patterns_used
|
|
66
|
-
FROM rapport.pattern_usage pu
|
|
67
|
-
JOIN rapport.sessions s ON pu.session_id = s.session_id
|
|
68
|
-
WHERE s.project_id = $2
|
|
69
|
-
AND pu.used_at >= $3
|
|
70
|
-
GROUP BY pu.email_address
|
|
71
|
-
) pu_counts ON pu_counts.email_address = u.email_address
|
|
72
|
-
LEFT JOIN (
|
|
73
|
-
SELECT dm.developer_email, AVG(dm.compliance_score) as compliance_score
|
|
74
|
-
FROM rapport.developer_metrics dm
|
|
75
|
-
JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
|
|
76
|
-
JOIN rapport.projects p ON p.repo_url = r.repo_url
|
|
77
|
-
WHERE p.project_id = $2
|
|
78
|
-
AND dm.period_start >= $3
|
|
79
|
-
GROUP BY dm.developer_email
|
|
80
|
-
) dm_scores ON dm_scores.developer_email = u.email_address
|
|
81
|
-
LEFT JOIN rapport.session_correlations sc ON sc.email_address = u.email_address
|
|
82
|
-
AND sc.project_id = $2
|
|
83
|
-
AND sc.session_started_at >= $3
|
|
84
|
-
WHERE ue.company_id = $1
|
|
85
|
-
GROUP BY u.email_address, u.first_name, u.last_name,
|
|
86
|
-
pu_counts.patterns_used, dm_scores.compliance_score
|
|
87
|
-
ORDER BY u.first_name ASC, u.last_name ASC
|
|
88
|
-
`, [companyId, projectId, periodStart]);
|
|
89
|
-
|
|
90
|
-
// Build developer list with quality scores
|
|
91
|
-
const developers = developerResult.rows.map(row => {
|
|
92
|
-
const patternsUsed = parseInt(row.patterns_used) || 0;
|
|
93
|
-
const complianceScore = parseFloat(row.compliance_score) || 0;
|
|
94
|
-
const standardsAdherence = complianceScore / 100;
|
|
95
|
-
|
|
96
|
-
// Quality score: patterns used + standards adherence, capped 0-100
|
|
97
|
-
const rawScore = (patternsUsed * 2) + (standardsAdherence * 50);
|
|
98
|
-
const qualityScore = Math.max(0, Math.min(100, Math.round(rawScore)));
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
user_email: row.user_email,
|
|
102
|
-
display_name: row.display_name || row.user_email.split('@')[0],
|
|
103
|
-
quality_score: qualityScore,
|
|
104
|
-
standards_adherence: parseFloat(standardsAdherence.toFixed(2)),
|
|
105
|
-
patterns_used: patternsUsed,
|
|
106
|
-
violations: 0,
|
|
107
|
-
violations_by_category: {},
|
|
108
|
-
trend: 'stable',
|
|
109
|
-
last_active: row.last_active
|
|
110
|
-
? new Date(row.last_active).toISOString().split('T')[0]
|
|
111
|
-
: null
|
|
112
|
-
};
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Calculate team summary
|
|
116
|
-
const totalDevelopers = developers.length;
|
|
117
|
-
const avgQualityScore = totalDevelopers > 0
|
|
118
|
-
? Math.round(developers.reduce((sum, d) => sum + d.quality_score, 0) / totalDevelopers)
|
|
119
|
-
: 0;
|
|
120
|
-
const avgAdherence = totalDevelopers > 0
|
|
121
|
-
? parseFloat((developers.reduce((sum, d) => sum + d.standards_adherence, 0) / totalDevelopers).toFixed(2))
|
|
122
|
-
: 0;
|
|
123
|
-
|
|
124
|
-
return createSuccessResponse({
|
|
125
|
-
team_summary: {
|
|
126
|
-
total_developers: totalDevelopers,
|
|
127
|
-
avg_quality_score: avgQualityScore,
|
|
128
|
-
standards_adherence: avgAdherence,
|
|
129
|
-
total_violations: 0,
|
|
130
|
-
period
|
|
131
|
-
},
|
|
132
|
-
developers,
|
|
133
|
-
trend_data: []
|
|
134
|
-
}, 'Developer scores retrieved successfully');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
exports.handler = wrapHandler(getDeveloperScores);
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collaborator Add Handler
|
|
3
|
-
* Adds a collaborator to a project
|
|
4
|
-
*
|
|
5
|
-
* POST /api/collaborators
|
|
6
|
-
* Body: { project_id, 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({ 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 { project_id: projectId, email, role = 'collaborator' } = requestBody;
|
|
21
|
-
|
|
22
|
-
if (!inviterEmail) {
|
|
23
|
-
return createErrorResponse(401, 'Authentication required');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!projectId) {
|
|
27
|
-
return createErrorResponse(400, 'projectId is required');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!email) {
|
|
31
|
-
return createErrorResponse(400, 'email is required');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Validate role
|
|
35
|
-
const validRoles = ['admin', 'collaborator', 'viewer'];
|
|
36
|
-
if (!validRoles.includes(role)) {
|
|
37
|
-
return createErrorResponse(400, `Invalid role. Must be one of: ${validRoles.join(', ')}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check inviter has permission (owner or admin)
|
|
41
|
-
const accessQuery = `
|
|
42
|
-
SELECT
|
|
43
|
-
pc.role,
|
|
44
|
-
p.company_id,
|
|
45
|
-
c.client_id,
|
|
46
|
-
c.subscription_tier,
|
|
47
|
-
c.billing_type,
|
|
48
|
-
c.billable_users,
|
|
49
|
-
c.seat_count,
|
|
50
|
-
c.enterprise_package
|
|
51
|
-
FROM rapport.projects p
|
|
52
|
-
JOIN rapport.project_collaborators pc
|
|
53
|
-
ON p.project_id = pc.project_id
|
|
54
|
-
AND pc.email_address = $1
|
|
55
|
-
JOIN rapport.clients c ON p.company_id = c.client_id
|
|
56
|
-
WHERE p.project_id = $2
|
|
57
|
-
`;
|
|
58
|
-
const accessCheck = await executeQuery(accessQuery, [inviterEmail, projectId]);
|
|
59
|
-
|
|
60
|
-
if (accessCheck.rowCount === 0) {
|
|
61
|
-
return createErrorResponse(403, 'You do not have access to this project');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const access = accessCheck.rows[0];
|
|
65
|
-
if (access.role !== 'owner' && access.role !== 'admin') {
|
|
66
|
-
return createErrorResponse(403, 'Only project owners and admins can add collaborators');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check billing/subscription limits based on billing type
|
|
70
|
-
const collaboratorCountQuery = `
|
|
71
|
-
SELECT COUNT(*) as count
|
|
72
|
-
FROM rapport.project_collaborators
|
|
73
|
-
WHERE project_id = $1
|
|
74
|
-
`;
|
|
75
|
-
const countResult = await executeQuery(collaboratorCountQuery, [projectId]);
|
|
76
|
-
const currentCount = parseInt(countResult.rows[0].count) || 0;
|
|
77
|
-
|
|
78
|
-
// For enterprise tier, check seat limits
|
|
79
|
-
if (access.subscription_tier === 'enterprise') {
|
|
80
|
-
// Count total users across all projects for this client
|
|
81
|
-
const totalUsersQuery = `
|
|
82
|
-
SELECT COUNT(DISTINCT pc.email_address) as total_users
|
|
83
|
-
FROM rapport.project_collaborators pc
|
|
84
|
-
JOIN rapport.projects p ON pc.project_id = p.project_id
|
|
85
|
-
WHERE p.company_id = $1
|
|
86
|
-
`;
|
|
87
|
-
const totalUsersResult = await executeQuery(totalUsersQuery, [access.client_id]);
|
|
88
|
-
const totalUsers = parseInt(totalUsersResult.rows[0].total_users) || 0;
|
|
89
|
-
|
|
90
|
-
const seatCheck = checkEnterpriseSeatLimits(
|
|
91
|
-
{
|
|
92
|
-
subscription_tier: access.subscription_tier,
|
|
93
|
-
seat_count: access.seat_count
|
|
94
|
-
},
|
|
95
|
-
totalUsers
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
if (!seatCheck.allowed) {
|
|
99
|
-
return createErrorResponse(403, seatCheck.message, {
|
|
100
|
-
code: 'ENTERPRISE_SEAT_LIMIT',
|
|
101
|
-
seatsUsed: seatCheck.seatsUsed,
|
|
102
|
-
seatCount: seatCheck.seatCount,
|
|
103
|
-
billingAction: seatCheck.billingAction
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const billingCheck = checkCollaboratorBillingLimits(
|
|
109
|
-
{
|
|
110
|
-
subscription_tier: access.subscription_tier,
|
|
111
|
-
billing_type: access.billing_type
|
|
112
|
-
},
|
|
113
|
-
currentCount
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
if (!billingCheck.allowed) {
|
|
117
|
-
return createErrorResponse(403, billingCheck.message, {
|
|
118
|
-
code: 'SUBSCRIPTION_LIMIT',
|
|
119
|
-
current: currentCount,
|
|
120
|
-
limit: billingCheck.limit
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check if collaborator already exists
|
|
125
|
-
const existsQuery = `
|
|
126
|
-
SELECT email_address FROM rapport.project_collaborators
|
|
127
|
-
WHERE project_id = $1 AND email_address = $2
|
|
128
|
-
`;
|
|
129
|
-
const existsCheck = await executeQuery(existsQuery, [projectId, email]);
|
|
130
|
-
|
|
131
|
-
if (existsCheck.rowCount > 0) {
|
|
132
|
-
return createErrorResponse(409, 'User is already a collaborator on this project');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Check if user exists in system (internal) or is external
|
|
136
|
-
const userQuery = `SELECT email_address FROM rapport.users WHERE email_address = $1`;
|
|
137
|
-
const userCheck = await executeQuery(userQuery, [email]);
|
|
138
|
-
const isExternal = userCheck.rowCount === 0;
|
|
139
|
-
|
|
140
|
-
// Add collaborator
|
|
141
|
-
const insertQuery = `
|
|
142
|
-
INSERT INTO rapport.project_collaborators
|
|
143
|
-
(project_id, email_address, role, invited_by, invited_at, is_external, accepted_at)
|
|
144
|
-
VALUES
|
|
145
|
-
($1, $2, $3, $4, NOW(), $5, ${isExternal ? 'NULL' : 'NOW()'})
|
|
146
|
-
RETURNING
|
|
147
|
-
project_id,
|
|
148
|
-
email_address,
|
|
149
|
-
role,
|
|
150
|
-
invited_by,
|
|
151
|
-
invited_at,
|
|
152
|
-
is_external,
|
|
153
|
-
accepted_at
|
|
154
|
-
`;
|
|
155
|
-
|
|
156
|
-
const result = await executeQuery(insertQuery, [
|
|
157
|
-
projectId,
|
|
158
|
-
email,
|
|
159
|
-
role,
|
|
160
|
-
inviterEmail,
|
|
161
|
-
isExternal
|
|
162
|
-
]);
|
|
163
|
-
|
|
164
|
-
const collaborator = result.rows[0];
|
|
165
|
-
|
|
166
|
-
// For enterprise invoice billing, increment billable_users count
|
|
167
|
-
if (billingCheck.billingAction === 'increment_billable_users') {
|
|
168
|
-
await executeQuery(`
|
|
169
|
-
UPDATE rapport.clients
|
|
170
|
-
SET billable_users = COALESCE(billable_users, 0) + 1,
|
|
171
|
-
last_updated = NOW()
|
|
172
|
-
WHERE client_id = $1
|
|
173
|
-
`, [access.client_id]);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return createSuccessResponse(
|
|
177
|
-
{
|
|
178
|
-
Records: [{
|
|
179
|
-
...collaborator,
|
|
180
|
-
status: isExternal ? 'pending_invite' : 'active',
|
|
181
|
-
message: isExternal
|
|
182
|
-
? 'Collaborator added. Send invite to complete setup.'
|
|
183
|
-
: 'Collaborator added successfully.'
|
|
184
|
-
}]
|
|
185
|
-
},
|
|
186
|
-
isExternal ? 'Collaborator added (pending invite)' : 'Collaborator added',
|
|
187
|
-
{
|
|
188
|
-
Total_Records: 1,
|
|
189
|
-
Request_ID,
|
|
190
|
-
Timestamp: new Date().toISOString()
|
|
191
|
-
}
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
} catch (error) {
|
|
195
|
-
console.error('Handler Error:', error);
|
|
196
|
-
return handleError(error);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
exports.handler = wrapHandler(addCollaborator);
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collaborator Invite Handler
|
|
3
|
-
* Sends invitation email to a collaborator
|
|
4
|
-
*
|
|
5
|
-
* POST /api/collaborators/invite
|
|
6
|
-
* Body: { project_id, email }
|
|
7
|
-
* Auth: Cognito JWT required
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
11
|
-
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
|
|
12
|
-
|
|
13
|
-
const ses = new SESClient({ region: process.env.AWS_REGION || 'us-east-2' });
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Send invitation email to collaborator
|
|
17
|
-
* Requires owner or admin role on project
|
|
18
|
-
*/
|
|
19
|
-
async function inviteCollaborator({ body: requestBody = {}, requestContext }) {
|
|
20
|
-
try {
|
|
21
|
-
const Request_ID = requestContext.requestId;
|
|
22
|
-
const inviterEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
23
|
-
const { project_id: projectId, email: collaboratorEmail } = requestBody;
|
|
24
|
-
|
|
25
|
-
if (!inviterEmail) {
|
|
26
|
-
return createErrorResponse(401, 'Authentication required');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!projectId || !collaboratorEmail) {
|
|
30
|
-
return createErrorResponse(400, 'project_id and email are required');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const targetEmail = collaboratorEmail;
|
|
34
|
-
|
|
35
|
-
// Check inviter has permission and get project details
|
|
36
|
-
const accessQuery = `
|
|
37
|
-
SELECT
|
|
38
|
-
pc.role as inviter_role,
|
|
39
|
-
p.project_name,
|
|
40
|
-
p.description,
|
|
41
|
-
tc.email_address as target_email,
|
|
42
|
-
tc.role as target_role,
|
|
43
|
-
tc.invited_at,
|
|
44
|
-
tc.accepted_at,
|
|
45
|
-
u.full_name as inviter_name
|
|
46
|
-
FROM rapport.projects p
|
|
47
|
-
JOIN rapport.project_collaborators pc
|
|
48
|
-
ON p.project_id = pc.project_id
|
|
49
|
-
AND pc.email_address = $1
|
|
50
|
-
JOIN rapport.project_collaborators tc
|
|
51
|
-
ON p.project_id = tc.project_id
|
|
52
|
-
AND tc.email_address = $3
|
|
53
|
-
LEFT JOIN rapport.users u ON u.email_address = $1
|
|
54
|
-
WHERE p.project_id = $2
|
|
55
|
-
`;
|
|
56
|
-
const accessCheck = await executeQuery(accessQuery, [inviterEmail, projectId, targetEmail]);
|
|
57
|
-
|
|
58
|
-
if (accessCheck.rowCount === 0) {
|
|
59
|
-
return createErrorResponse(404, 'Project or collaborator not found');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const data = accessCheck.rows[0];
|
|
63
|
-
|
|
64
|
-
// Check permissions
|
|
65
|
-
if (data.inviter_role !== 'owner' && data.inviter_role !== 'admin') {
|
|
66
|
-
return createErrorResponse(403, 'Only project owners and admins can send invites');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check if already accepted
|
|
70
|
-
if (data.accepted_at) {
|
|
71
|
-
return createErrorResponse(400, 'Collaborator has already accepted the invitation');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Generate invite token (simple UUID-based token)
|
|
75
|
-
const inviteToken = `inv_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
76
|
-
|
|
77
|
-
// Store invite token
|
|
78
|
-
await executeQuery(`
|
|
79
|
-
UPDATE rapport.project_collaborators
|
|
80
|
-
SET invite_token = $1, invited_at = NOW()
|
|
81
|
-
WHERE project_id = $2 AND email_address = $3
|
|
82
|
-
`, [inviteToken, projectId, targetEmail]);
|
|
83
|
-
|
|
84
|
-
// Build invite URL
|
|
85
|
-
const appUrl = process.env.APP_URL || 'https://app.mindmeld.dev';
|
|
86
|
-
const inviteUrl = `${appUrl}/invite/accept?token=${inviteToken}`;
|
|
87
|
-
|
|
88
|
-
// Send email
|
|
89
|
-
const inviterName = data.inviter_name || inviterEmail;
|
|
90
|
-
const emailParams = {
|
|
91
|
-
Source: process.env.EMAIL_FROM || 'noreply@mindmeld.dev',
|
|
92
|
-
Destination: {
|
|
93
|
-
ToAddresses: [targetEmail]
|
|
94
|
-
},
|
|
95
|
-
Message: {
|
|
96
|
-
Subject: {
|
|
97
|
-
Data: `You've been invited to collaborate on ${data.project_name}`,
|
|
98
|
-
Charset: 'UTF-8'
|
|
99
|
-
},
|
|
100
|
-
Body: {
|
|
101
|
-
Html: {
|
|
102
|
-
Data: `
|
|
103
|
-
<!DOCTYPE html>
|
|
104
|
-
<html>
|
|
105
|
-
<head>
|
|
106
|
-
<meta charset="utf-8">
|
|
107
|
-
<style>
|
|
108
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
109
|
-
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
110
|
-
.header { text-align: center; margin-bottom: 30px; }
|
|
111
|
-
.logo { font-size: 24px; font-weight: bold; color: #2563eb; }
|
|
112
|
-
.content { background: #f8fafc; border-radius: 8px; padding: 30px; margin-bottom: 20px; }
|
|
113
|
-
.button { display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500; }
|
|
114
|
-
.button:hover { background: #1d4ed8; }
|
|
115
|
-
.footer { text-align: center; color: #64748b; font-size: 14px; }
|
|
116
|
-
.project-name { font-size: 20px; font-weight: 600; color: #1e293b; }
|
|
117
|
-
.role-badge { display: inline-block; background: #e0e7ff; color: #3730a3; padding: 4px 12px; border-radius: 20px; font-size: 14px; }
|
|
118
|
-
</style>
|
|
119
|
-
</head>
|
|
120
|
-
<body>
|
|
121
|
-
<div class="container">
|
|
122
|
-
<div class="header">
|
|
123
|
-
<div class="logo">MindMeld</div>
|
|
124
|
-
</div>
|
|
125
|
-
<div class="content">
|
|
126
|
-
<p>Hi there,</p>
|
|
127
|
-
<p><strong>${inviterName}</strong> has invited you to collaborate on a project:</p>
|
|
128
|
-
<p class="project-name">${data.project_name}</p>
|
|
129
|
-
${data.description ? `<p style="color: #64748b;">${data.description}</p>` : ''}
|
|
130
|
-
<p>Your role: <span class="role-badge">${data.target_role}</span></p>
|
|
131
|
-
<p style="margin-top: 30px;">
|
|
132
|
-
<a href="${inviteUrl}" class="button">Accept Invitation</a>
|
|
133
|
-
</p>
|
|
134
|
-
<p style="margin-top: 20px; font-size: 14px; color: #64748b;">
|
|
135
|
-
Or copy this link: ${inviteUrl}
|
|
136
|
-
</p>
|
|
137
|
-
</div>
|
|
138
|
-
<div class="footer">
|
|
139
|
-
<p>MindMeld - Collaboration memory that compounds</p>
|
|
140
|
-
<p>Powered by Equilateral AI</p>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
</body>
|
|
144
|
-
</html>
|
|
145
|
-
`,
|
|
146
|
-
Charset: 'UTF-8'
|
|
147
|
-
},
|
|
148
|
-
Text: {
|
|
149
|
-
Data: `
|
|
150
|
-
You've been invited to collaborate on ${data.project_name}
|
|
151
|
-
|
|
152
|
-
${inviterName} has invited you to join their project on MindMeld.
|
|
153
|
-
|
|
154
|
-
Project: ${data.project_name}
|
|
155
|
-
${data.description ? `Description: ${data.description}` : ''}
|
|
156
|
-
Your role: ${data.target_role}
|
|
157
|
-
|
|
158
|
-
Accept the invitation: ${inviteUrl}
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
MindMeld - Collaboration memory that compounds
|
|
162
|
-
Powered by Equilateral AI
|
|
163
|
-
`,
|
|
164
|
-
Charset: 'UTF-8'
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
await ses.send(new SendEmailCommand(emailParams));
|
|
172
|
-
} catch (sesError) {
|
|
173
|
-
console.error('SES Error:', sesError);
|
|
174
|
-
// Don't fail the request, just note that email failed
|
|
175
|
-
return createSuccessResponse(
|
|
176
|
-
{
|
|
177
|
-
Records: [{
|
|
178
|
-
email: targetEmail,
|
|
179
|
-
project_id: projectId,
|
|
180
|
-
invite_url: inviteUrl,
|
|
181
|
-
email_sent: false,
|
|
182
|
-
email_error: 'Failed to send email. Share the invite link manually.'
|
|
183
|
-
}]
|
|
184
|
-
},
|
|
185
|
-
'Invite created but email failed to send',
|
|
186
|
-
{
|
|
187
|
-
Total_Records: 1,
|
|
188
|
-
Request_ID,
|
|
189
|
-
Timestamp: new Date().toISOString()
|
|
190
|
-
}
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return createSuccessResponse(
|
|
195
|
-
{
|
|
196
|
-
Records: [{
|
|
197
|
-
email: targetEmail,
|
|
198
|
-
project_id: projectId,
|
|
199
|
-
project_name: data.project_name,
|
|
200
|
-
role: data.target_role,
|
|
201
|
-
invite_url: inviteUrl,
|
|
202
|
-
email_sent: true
|
|
203
|
-
}]
|
|
204
|
-
},
|
|
205
|
-
'Invitation sent successfully',
|
|
206
|
-
{
|
|
207
|
-
Total_Records: 1,
|
|
208
|
-
Request_ID,
|
|
209
|
-
Timestamp: new Date().toISOString()
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
} catch (error) {
|
|
214
|
-
console.error('Handler Error:', error);
|
|
215
|
-
return handleError(error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
exports.handler = wrapHandler(inviteCollaborator);
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collaborator List Handler
|
|
3
|
-
* Lists all collaborators for a project
|
|
4
|
-
*
|
|
5
|
-
* GET /api/collaborators?project_id=xxx
|
|
6
|
-
* Auth: Cognito JWT required
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, verifyProjectAccess } = require('./helpers');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* List project collaborators
|
|
13
|
-
* Requires collaborator access to project
|
|
14
|
-
*/
|
|
15
|
-
async function listCollaborators({ queryStringParameters: queryParams = {}, requestContext }) {
|
|
16
|
-
try {
|
|
17
|
-
const Request_ID = requestContext.requestId;
|
|
18
|
-
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
19
|
-
const projectId = queryParams.project_id;
|
|
20
|
-
|
|
21
|
-
if (!email) {
|
|
22
|
-
return createErrorResponse(401, 'Authentication required');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!projectId) {
|
|
26
|
-
return createErrorResponse(400, 'projectId is required');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Verify user has access to project (collaborator or company member)
|
|
30
|
-
const projectAccess = await verifyProjectAccess(projectId, email);
|
|
31
|
-
if (!projectAccess) {
|
|
32
|
-
return createErrorResponse(403, 'You do not have access to this project');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Get all collaborators
|
|
36
|
-
const query = `
|
|
37
|
-
SELECT
|
|
38
|
-
pc.email_address,
|
|
39
|
-
pc.role,
|
|
40
|
-
pc.invited_by,
|
|
41
|
-
pc.invited_at,
|
|
42
|
-
pc.accepted_at,
|
|
43
|
-
pc.is_external,
|
|
44
|
-
u.full_name,
|
|
45
|
-
CASE
|
|
46
|
-
WHEN pc.accepted_at IS NOT NULL THEN 'active'
|
|
47
|
-
WHEN pc.invited_at IS NOT NULL THEN 'pending'
|
|
48
|
-
ELSE 'unknown'
|
|
49
|
-
END as status
|
|
50
|
-
FROM rapport.project_collaborators pc
|
|
51
|
-
LEFT JOIN rapport.users u ON u.email_address = pc.email_address
|
|
52
|
-
WHERE pc.project_id = $1
|
|
53
|
-
ORDER BY
|
|
54
|
-
CASE pc.role
|
|
55
|
-
WHEN 'owner' THEN 1
|
|
56
|
-
WHEN 'admin' THEN 2
|
|
57
|
-
WHEN 'collaborator' THEN 3
|
|
58
|
-
WHEN 'viewer' THEN 4
|
|
59
|
-
ELSE 5
|
|
60
|
-
END,
|
|
61
|
-
pc.invited_at ASC
|
|
62
|
-
`;
|
|
63
|
-
|
|
64
|
-
const result = await executeQuery(query, [projectId]);
|
|
65
|
-
|
|
66
|
-
return createSuccessResponse(
|
|
67
|
-
{ Records: result.rows },
|
|
68
|
-
'Collaborators retrieved',
|
|
69
|
-
{
|
|
70
|
-
Total_Records: result.rowCount,
|
|
71
|
-
Request_ID,
|
|
72
|
-
Timestamp: new Date().toISOString()
|
|
73
|
-
}
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error('Handler Error:', error);
|
|
78
|
-
return handleError(error);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
exports.handler = wrapHandler(listCollaborators);
|