@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.
- package/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collaborator Invite Handler
|
|
3
|
+
* Sends invitation email to a collaborator
|
|
4
|
+
*
|
|
5
|
+
* POST /api/projects/{projectId}/collaborators/{collaboratorEmail}/invite
|
|
6
|
+
* Auth: Cognito JWT required
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
10
|
+
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
|
|
11
|
+
|
|
12
|
+
const ses = new SESClient({ region: process.env.AWS_REGION || 'us-east-2' });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Send invitation email to collaborator
|
|
16
|
+
* Requires owner or admin role on project
|
|
17
|
+
*/
|
|
18
|
+
async function inviteCollaborator({ pathParameters = {}, requestContext }) {
|
|
19
|
+
try {
|
|
20
|
+
const Request_ID = requestContext.requestId;
|
|
21
|
+
const inviterEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
22
|
+
const { projectId, collaboratorEmail } = pathParameters;
|
|
23
|
+
|
|
24
|
+
if (!inviterEmail) {
|
|
25
|
+
return createErrorResponse(401, 'Authentication required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!projectId || !collaboratorEmail) {
|
|
29
|
+
return createErrorResponse(400, 'projectId and collaboratorEmail are required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetEmail = decodeURIComponent(collaboratorEmail);
|
|
33
|
+
|
|
34
|
+
// Check inviter has permission and get project details
|
|
35
|
+
const accessQuery = `
|
|
36
|
+
SELECT
|
|
37
|
+
pc.role as inviter_role,
|
|
38
|
+
p.project_name,
|
|
39
|
+
p.description,
|
|
40
|
+
tc.email_address as target_email,
|
|
41
|
+
tc.role as target_role,
|
|
42
|
+
tc.invited_at,
|
|
43
|
+
tc.accepted_at,
|
|
44
|
+
u.full_name as inviter_name
|
|
45
|
+
FROM rapport.projects p
|
|
46
|
+
JOIN rapport.project_collaborators pc
|
|
47
|
+
ON p.project_id = pc.project_id
|
|
48
|
+
AND pc.email_address = $1
|
|
49
|
+
JOIN rapport.project_collaborators tc
|
|
50
|
+
ON p.project_id = tc.project_id
|
|
51
|
+
AND tc.email_address = $3
|
|
52
|
+
LEFT JOIN rapport.users u ON u.email_address = $1
|
|
53
|
+
WHERE p.project_id = $2
|
|
54
|
+
`;
|
|
55
|
+
const accessCheck = await executeQuery(accessQuery, [inviterEmail, projectId, targetEmail]);
|
|
56
|
+
|
|
57
|
+
if (accessCheck.rowCount === 0) {
|
|
58
|
+
return createErrorResponse(404, 'Project or collaborator not found');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data = accessCheck.rows[0];
|
|
62
|
+
|
|
63
|
+
// Check permissions
|
|
64
|
+
if (data.inviter_role !== 'owner' && data.inviter_role !== 'admin') {
|
|
65
|
+
return createErrorResponse(403, 'Only project owners and admins can send invites');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check if already accepted
|
|
69
|
+
if (data.accepted_at) {
|
|
70
|
+
return createErrorResponse(400, 'Collaborator has already accepted the invitation');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Generate invite token (simple UUID-based token)
|
|
74
|
+
const inviteToken = `inv_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
75
|
+
|
|
76
|
+
// Store invite token
|
|
77
|
+
await executeQuery(`
|
|
78
|
+
UPDATE rapport.project_collaborators
|
|
79
|
+
SET invite_token = $1, invited_at = NOW()
|
|
80
|
+
WHERE project_id = $2 AND email_address = $3
|
|
81
|
+
`, [inviteToken, projectId, targetEmail]);
|
|
82
|
+
|
|
83
|
+
// Build invite URL
|
|
84
|
+
const appUrl = process.env.APP_URL || 'https://mindmeld.dev';
|
|
85
|
+
const inviteUrl = `${appUrl}/invite/accept?token=${inviteToken}`;
|
|
86
|
+
|
|
87
|
+
// Send email
|
|
88
|
+
const inviterName = data.inviter_name || inviterEmail;
|
|
89
|
+
const emailParams = {
|
|
90
|
+
Source: process.env.EMAIL_FROM || 'noreply@mindmeld.dev',
|
|
91
|
+
Destination: {
|
|
92
|
+
ToAddresses: [targetEmail]
|
|
93
|
+
},
|
|
94
|
+
Message: {
|
|
95
|
+
Subject: {
|
|
96
|
+
Data: `You've been invited to collaborate on ${data.project_name}`,
|
|
97
|
+
Charset: 'UTF-8'
|
|
98
|
+
},
|
|
99
|
+
Body: {
|
|
100
|
+
Html: {
|
|
101
|
+
Data: `
|
|
102
|
+
<!DOCTYPE html>
|
|
103
|
+
<html>
|
|
104
|
+
<head>
|
|
105
|
+
<meta charset="utf-8">
|
|
106
|
+
<style>
|
|
107
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
108
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
109
|
+
.header { text-align: center; margin-bottom: 30px; }
|
|
110
|
+
.logo { font-size: 24px; font-weight: bold; color: #2563eb; }
|
|
111
|
+
.content { background: #f8fafc; border-radius: 8px; padding: 30px; margin-bottom: 20px; }
|
|
112
|
+
.button { display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500; }
|
|
113
|
+
.button:hover { background: #1d4ed8; }
|
|
114
|
+
.footer { text-align: center; color: #64748b; font-size: 14px; }
|
|
115
|
+
.project-name { font-size: 20px; font-weight: 600; color: #1e293b; }
|
|
116
|
+
.role-badge { display: inline-block; background: #e0e7ff; color: #3730a3; padding: 4px 12px; border-radius: 20px; font-size: 14px; }
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<div class="container">
|
|
121
|
+
<div class="header">
|
|
122
|
+
<div class="logo">MindMeld</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="content">
|
|
125
|
+
<p>Hi there,</p>
|
|
126
|
+
<p><strong>${inviterName}</strong> has invited you to collaborate on a project:</p>
|
|
127
|
+
<p class="project-name">${data.project_name}</p>
|
|
128
|
+
${data.description ? `<p style="color: #64748b;">${data.description}</p>` : ''}
|
|
129
|
+
<p>Your role: <span class="role-badge">${data.target_role}</span></p>
|
|
130
|
+
<p style="margin-top: 30px;">
|
|
131
|
+
<a href="${inviteUrl}" class="button">Accept Invitation</a>
|
|
132
|
+
</p>
|
|
133
|
+
<p style="margin-top: 20px; font-size: 14px; color: #64748b;">
|
|
134
|
+
Or copy this link: ${inviteUrl}
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="footer">
|
|
138
|
+
<p>MindMeld - Collaboration memory that compounds</p>
|
|
139
|
+
<p>Powered by Equilateral AI</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
144
|
+
`,
|
|
145
|
+
Charset: 'UTF-8'
|
|
146
|
+
},
|
|
147
|
+
Text: {
|
|
148
|
+
Data: `
|
|
149
|
+
You've been invited to collaborate on ${data.project_name}
|
|
150
|
+
|
|
151
|
+
${inviterName} has invited you to join their project on MindMeld.
|
|
152
|
+
|
|
153
|
+
Project: ${data.project_name}
|
|
154
|
+
${data.description ? `Description: ${data.description}` : ''}
|
|
155
|
+
Your role: ${data.target_role}
|
|
156
|
+
|
|
157
|
+
Accept the invitation: ${inviteUrl}
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
MindMeld - Collaboration memory that compounds
|
|
161
|
+
Powered by Equilateral AI
|
|
162
|
+
`,
|
|
163
|
+
Charset: 'UTF-8'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await ses.send(new SendEmailCommand(emailParams));
|
|
171
|
+
} catch (sesError) {
|
|
172
|
+
console.error('SES Error:', sesError);
|
|
173
|
+
// Don't fail the request, just note that email failed
|
|
174
|
+
return createSuccessResponse(
|
|
175
|
+
{
|
|
176
|
+
Records: [{
|
|
177
|
+
email: targetEmail,
|
|
178
|
+
project_id: projectId,
|
|
179
|
+
invite_url: inviteUrl,
|
|
180
|
+
email_sent: false,
|
|
181
|
+
email_error: 'Failed to send email. Share the invite link manually.'
|
|
182
|
+
}]
|
|
183
|
+
},
|
|
184
|
+
'Invite created but email failed to send',
|
|
185
|
+
{
|
|
186
|
+
Total_Records: 1,
|
|
187
|
+
Request_ID,
|
|
188
|
+
Timestamp: new Date().toISOString()
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return createSuccessResponse(
|
|
194
|
+
{
|
|
195
|
+
Records: [{
|
|
196
|
+
email: targetEmail,
|
|
197
|
+
project_id: projectId,
|
|
198
|
+
project_name: data.project_name,
|
|
199
|
+
role: data.target_role,
|
|
200
|
+
invite_url: inviteUrl,
|
|
201
|
+
email_sent: true
|
|
202
|
+
}]
|
|
203
|
+
},
|
|
204
|
+
'Invitation sent successfully',
|
|
205
|
+
{
|
|
206
|
+
Total_Records: 1,
|
|
207
|
+
Request_ID,
|
|
208
|
+
Timestamp: new Date().toISOString()
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('Handler Error:', error);
|
|
214
|
+
return handleError(error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
exports.handler = wrapHandler(inviteCollaborator);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collaborator List Handler
|
|
3
|
+
* Lists all collaborators for a project
|
|
4
|
+
*
|
|
5
|
+
* GET /api/projects/{projectId}/collaborators
|
|
6
|
+
* Auth: Cognito JWT required
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* List project collaborators
|
|
13
|
+
* Requires collaborator access to project
|
|
14
|
+
*/
|
|
15
|
+
async function listCollaborators({ pathParameters = {}, requestContext }) {
|
|
16
|
+
try {
|
|
17
|
+
const Request_ID = requestContext.requestId;
|
|
18
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
19
|
+
const { projectId } = pathParameters;
|
|
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
|
+
// Check user has access to project
|
|
30
|
+
const accessQuery = `
|
|
31
|
+
SELECT pc.role
|
|
32
|
+
FROM rapport.project_collaborators pc
|
|
33
|
+
WHERE pc.project_id = $1 AND pc.email_address = $2
|
|
34
|
+
`;
|
|
35
|
+
const accessCheck = await executeQuery(accessQuery, [projectId, email]);
|
|
36
|
+
|
|
37
|
+
if (accessCheck.rowCount === 0) {
|
|
38
|
+
return createErrorResponse(403, 'You do not have access to this project');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get all collaborators
|
|
42
|
+
const query = `
|
|
43
|
+
SELECT
|
|
44
|
+
pc.email_address,
|
|
45
|
+
pc.role,
|
|
46
|
+
pc.invited_by,
|
|
47
|
+
pc.invited_at,
|
|
48
|
+
pc.accepted_at,
|
|
49
|
+
pc.is_external,
|
|
50
|
+
u.full_name,
|
|
51
|
+
CASE
|
|
52
|
+
WHEN pc.accepted_at IS NOT NULL THEN 'active'
|
|
53
|
+
WHEN pc.invited_at IS NOT NULL THEN 'pending'
|
|
54
|
+
ELSE 'unknown'
|
|
55
|
+
END as status
|
|
56
|
+
FROM rapport.project_collaborators pc
|
|
57
|
+
LEFT JOIN rapport.users u ON u.email_address = pc.email_address
|
|
58
|
+
WHERE pc.project_id = $1
|
|
59
|
+
ORDER BY
|
|
60
|
+
CASE pc.role
|
|
61
|
+
WHEN 'owner' THEN 1
|
|
62
|
+
WHEN 'admin' THEN 2
|
|
63
|
+
WHEN 'collaborator' THEN 3
|
|
64
|
+
WHEN 'viewer' THEN 4
|
|
65
|
+
ELSE 5
|
|
66
|
+
END,
|
|
67
|
+
pc.invited_at ASC
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const result = await executeQuery(query, [projectId]);
|
|
71
|
+
|
|
72
|
+
return createSuccessResponse(
|
|
73
|
+
{ Records: result.rows },
|
|
74
|
+
'Collaborators retrieved',
|
|
75
|
+
{
|
|
76
|
+
Total_Records: result.rowCount,
|
|
77
|
+
Request_ID,
|
|
78
|
+
Timestamp: new Date().toISOString()
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Handler Error:', error);
|
|
84
|
+
return handleError(error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
exports.handler = wrapHandler(listCollaborators);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collaborator Remove Handler
|
|
3
|
+
* Removes a collaborator from a project
|
|
4
|
+
*
|
|
5
|
+
* DELETE /api/projects/{projectId}/collaborators/{collaboratorEmail}
|
|
6
|
+
* Auth: Cognito JWT required
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Remove collaborator from project
|
|
13
|
+
* Requires owner or admin role on project
|
|
14
|
+
* Owners cannot be removed (must transfer ownership first)
|
|
15
|
+
*/
|
|
16
|
+
async function removeCollaborator({ pathParameters = {}, requestContext }) {
|
|
17
|
+
try {
|
|
18
|
+
const Request_ID = requestContext.requestId;
|
|
19
|
+
const removerEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
20
|
+
const { projectId, collaboratorEmail } = pathParameters;
|
|
21
|
+
|
|
22
|
+
if (!removerEmail) {
|
|
23
|
+
return createErrorResponse(401, 'Authentication required');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!projectId) {
|
|
27
|
+
return createErrorResponse(400, 'projectId is required');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!collaboratorEmail) {
|
|
31
|
+
return createErrorResponse(400, 'collaboratorEmail is required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Decode email from URL (may be URL encoded)
|
|
35
|
+
const targetEmail = decodeURIComponent(collaboratorEmail);
|
|
36
|
+
|
|
37
|
+
// Check remover has permission and get billing info
|
|
38
|
+
const accessQuery = `
|
|
39
|
+
SELECT
|
|
40
|
+
pc.role,
|
|
41
|
+
c.client_id,
|
|
42
|
+
c.billing_type
|
|
43
|
+
FROM rapport.project_collaborators pc
|
|
44
|
+
JOIN rapport.projects p ON pc.project_id = p.project_id
|
|
45
|
+
JOIN rapport.clients c ON p.company_id = c.client_id
|
|
46
|
+
WHERE pc.project_id = $1 AND pc.email_address = $2
|
|
47
|
+
`;
|
|
48
|
+
const accessCheck = await executeQuery(accessQuery, [projectId, removerEmail]);
|
|
49
|
+
|
|
50
|
+
if (accessCheck.rowCount === 0) {
|
|
51
|
+
return createErrorResponse(403, 'You do not have access to this project');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { role: removerRole, client_id, billing_type } = accessCheck.rows[0];
|
|
55
|
+
const isSelfRemoval = removerEmail.toLowerCase() === targetEmail.toLowerCase();
|
|
56
|
+
|
|
57
|
+
// Allow self-removal for anyone except owner
|
|
58
|
+
// Only owner/admin can remove others
|
|
59
|
+
if (!isSelfRemoval && removerRole !== 'owner' && removerRole !== 'admin') {
|
|
60
|
+
return createErrorResponse(403, 'Only project owners and admins can remove collaborators');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check target collaborator exists and get their role
|
|
64
|
+
const targetQuery = `
|
|
65
|
+
SELECT role FROM rapport.project_collaborators
|
|
66
|
+
WHERE project_id = $1 AND email_address = $2
|
|
67
|
+
`;
|
|
68
|
+
const targetCheck = await executeQuery(targetQuery, [projectId, targetEmail]);
|
|
69
|
+
|
|
70
|
+
if (targetCheck.rowCount === 0) {
|
|
71
|
+
return createErrorResponse(404, 'Collaborator not found on this project');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const targetRole = targetCheck.rows[0].role;
|
|
75
|
+
|
|
76
|
+
// Prevent removing owner
|
|
77
|
+
if (targetRole === 'owner') {
|
|
78
|
+
return createErrorResponse(400, 'Cannot remove project owner. Transfer ownership first.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Prevent admin from removing another admin (only owner can)
|
|
82
|
+
if (targetRole === 'admin' && removerRole !== 'owner') {
|
|
83
|
+
return createErrorResponse(403, 'Only project owner can remove admins');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Remove collaborator
|
|
87
|
+
const deleteQuery = `
|
|
88
|
+
DELETE FROM rapport.project_collaborators
|
|
89
|
+
WHERE project_id = $1 AND email_address = $2
|
|
90
|
+
RETURNING project_id, email_address, role
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const result = await executeQuery(deleteQuery, [projectId, targetEmail]);
|
|
94
|
+
|
|
95
|
+
// For enterprise invoice billing, decrement billable_users count
|
|
96
|
+
if (billing_type === 'invoice' && client_id) {
|
|
97
|
+
await executeQuery(`
|
|
98
|
+
UPDATE rapport.clients
|
|
99
|
+
SET billable_users = GREATEST(COALESCE(billable_users, 0) - 1, 0),
|
|
100
|
+
last_updated = NOW()
|
|
101
|
+
WHERE client_id = $1
|
|
102
|
+
`, [client_id]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return createSuccessResponse(
|
|
106
|
+
{
|
|
107
|
+
Records: [{
|
|
108
|
+
...result.rows[0],
|
|
109
|
+
removed_by: removerEmail,
|
|
110
|
+
removed_at: new Date().toISOString()
|
|
111
|
+
}]
|
|
112
|
+
},
|
|
113
|
+
isSelfRemoval ? 'You have left the project' : 'Collaborator removed',
|
|
114
|
+
{
|
|
115
|
+
Total_Records: 1,
|
|
116
|
+
Request_ID,
|
|
117
|
+
Timestamp: new Date().toISOString()
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Handler Error:', error);
|
|
123
|
+
return handleError(error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exports.handler = wrapHandler(removeCollaborator);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invite Accept Handler
|
|
3
|
+
* Accepts a collaboration invitation via token
|
|
4
|
+
*
|
|
5
|
+
* POST /api/invites/accept
|
|
6
|
+
* Body: { token }
|
|
7
|
+
* Auth: Cognito JWT required (user must be logged in)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Accept collaboration invitation
|
|
14
|
+
* User must be authenticated and email must match invite
|
|
15
|
+
*/
|
|
16
|
+
async function acceptInvite({ body: requestBody = {}, requestContext }) {
|
|
17
|
+
try {
|
|
18
|
+
const Request_ID = requestContext.requestId;
|
|
19
|
+
const userEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
20
|
+
const { token } = requestBody;
|
|
21
|
+
|
|
22
|
+
if (!userEmail) {
|
|
23
|
+
return createErrorResponse(401, 'Authentication required. Please sign in first.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!token) {
|
|
27
|
+
return createErrorResponse(400, 'Invite token is required');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Find invite by token
|
|
31
|
+
const inviteQuery = `
|
|
32
|
+
SELECT
|
|
33
|
+
pc.project_id,
|
|
34
|
+
pc.email_address,
|
|
35
|
+
pc.role,
|
|
36
|
+
pc.invited_by,
|
|
37
|
+
pc.invited_at,
|
|
38
|
+
pc.accepted_at,
|
|
39
|
+
p.project_name,
|
|
40
|
+
p.description,
|
|
41
|
+
p.company_id
|
|
42
|
+
FROM rapport.project_collaborators pc
|
|
43
|
+
JOIN rapport.projects p ON p.project_id = pc.project_id
|
|
44
|
+
WHERE pc.invite_token = $1
|
|
45
|
+
`;
|
|
46
|
+
const inviteCheck = await executeQuery(inviteQuery, [token]);
|
|
47
|
+
|
|
48
|
+
if (inviteCheck.rowCount === 0) {
|
|
49
|
+
return createErrorResponse(404, 'Invalid or expired invite token');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const invite = inviteCheck.rows[0];
|
|
53
|
+
|
|
54
|
+
// Check if already accepted
|
|
55
|
+
if (invite.accepted_at) {
|
|
56
|
+
return createErrorResponse(400, 'This invitation has already been accepted');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Verify email matches (case insensitive)
|
|
60
|
+
if (invite.email_address.toLowerCase() !== userEmail.toLowerCase()) {
|
|
61
|
+
return createErrorResponse(403,
|
|
62
|
+
'This invitation was sent to a different email address. ' +
|
|
63
|
+
'Please sign in with the email the invite was sent to.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if invite is expired (7 days)
|
|
68
|
+
const invitedAt = new Date(invite.invited_at);
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const daysSinceInvite = (now - invitedAt) / (1000 * 60 * 60 * 24);
|
|
71
|
+
|
|
72
|
+
if (daysSinceInvite > 7) {
|
|
73
|
+
return createErrorResponse(410, 'This invitation has expired. Please ask for a new invite.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Accept the invite
|
|
77
|
+
const acceptQuery = `
|
|
78
|
+
UPDATE rapport.project_collaborators
|
|
79
|
+
SET
|
|
80
|
+
accepted_at = NOW(),
|
|
81
|
+
invite_token = NULL,
|
|
82
|
+
is_external = false
|
|
83
|
+
WHERE project_id = $1 AND email_address = $2
|
|
84
|
+
RETURNING project_id, email_address, role, accepted_at
|
|
85
|
+
`;
|
|
86
|
+
const acceptResult = await executeQuery(acceptQuery, [invite.project_id, invite.email_address]);
|
|
87
|
+
|
|
88
|
+
// Ensure user exists in users table
|
|
89
|
+
await executeQuery(`
|
|
90
|
+
INSERT INTO rapport.users (email_address, client_id, active, created_at)
|
|
91
|
+
VALUES ($1, $2, true, NOW())
|
|
92
|
+
ON CONFLICT (email_address) DO UPDATE SET
|
|
93
|
+
active = true,
|
|
94
|
+
updated_at = NOW()
|
|
95
|
+
`, [userEmail, invite.company_id]);
|
|
96
|
+
|
|
97
|
+
return createSuccessResponse(
|
|
98
|
+
{
|
|
99
|
+
Records: [{
|
|
100
|
+
project_id: invite.project_id,
|
|
101
|
+
project_name: invite.project_name,
|
|
102
|
+
description: invite.description,
|
|
103
|
+
role: invite.role,
|
|
104
|
+
accepted_at: acceptResult.rows[0].accepted_at,
|
|
105
|
+
invited_by: invite.invited_by
|
|
106
|
+
}]
|
|
107
|
+
},
|
|
108
|
+
`Welcome to ${invite.project_name}!`,
|
|
109
|
+
{
|
|
110
|
+
Total_Records: 1,
|
|
111
|
+
Request_ID,
|
|
112
|
+
Timestamp: new Date().toISOString()
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Handler Error:', error);
|
|
118
|
+
return handleError(error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
exports.handler = wrapHandler(acceptInvite);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Get Handler
|
|
3
|
+
* Retrieves full user context for a scope (invariants, purpose, notes, loops)
|
|
4
|
+
* Used by mobile/iOS apps for session injection
|
|
5
|
+
*
|
|
6
|
+
* GET /api/context?scope=jarvis
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, checkSuperAdmin } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get user context for mobile injection
|
|
13
|
+
*/
|
|
14
|
+
async function getContext({ queryStringParameters: queryParams = {}, requestContext }) {
|
|
15
|
+
try {
|
|
16
|
+
const Request_ID = requestContext.requestId;
|
|
17
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
18
|
+
|
|
19
|
+
if (!email) {
|
|
20
|
+
return createErrorResponse(401, 'Unauthorized');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Gate to super admins only (internal/beta endpoint)
|
|
24
|
+
await checkSuperAdmin.requireSuperAdmin(email);
|
|
25
|
+
|
|
26
|
+
const scope = queryParams.scope || 'jarvis';
|
|
27
|
+
|
|
28
|
+
// Use the database function for efficient retrieval
|
|
29
|
+
const query = `SELECT rapport.get_user_context($1, $2) as context`;
|
|
30
|
+
const result = await executeQuery(query, [email, scope]);
|
|
31
|
+
|
|
32
|
+
const context = result.rows[0]?.context || {
|
|
33
|
+
invariants: { agent_level: [], relationship_level: [] },
|
|
34
|
+
purpose: null,
|
|
35
|
+
recent_notes: [],
|
|
36
|
+
active_loops: []
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return createSuccessResponse(
|
|
40
|
+
{
|
|
41
|
+
scope,
|
|
42
|
+
...context
|
|
43
|
+
},
|
|
44
|
+
'Context retrieved successfully',
|
|
45
|
+
{
|
|
46
|
+
Request_ID,
|
|
47
|
+
Timestamp: new Date().toISOString()
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Handler Error:', error);
|
|
53
|
+
return handleError(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
exports.handler = wrapHandler(getContext);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariants Get Handler
|
|
3
|
+
* Retrieves user invariants for a scope
|
|
4
|
+
*
|
|
5
|
+
* GET /api/context/invariants?scope=jarvis
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, checkSuperAdmin } = require('./helpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get user invariants
|
|
12
|
+
*/
|
|
13
|
+
async function getInvariants({ queryStringParameters: queryParams = {}, requestContext }) {
|
|
14
|
+
try {
|
|
15
|
+
const Request_ID = requestContext.requestId;
|
|
16
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
17
|
+
|
|
18
|
+
if (!email) {
|
|
19
|
+
return createErrorResponse(401, 'Unauthorized');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Gate to super admins only (internal/beta endpoint)
|
|
23
|
+
await checkSuperAdmin.requireSuperAdmin(email);
|
|
24
|
+
|
|
25
|
+
const scope = queryParams.scope || 'global';
|
|
26
|
+
|
|
27
|
+
// Get agent-level invariants (global only)
|
|
28
|
+
const agentQuery = `
|
|
29
|
+
SELECT invariant_text as text, maturity, tier
|
|
30
|
+
FROM rapport.user_invariants
|
|
31
|
+
WHERE email_address = $1
|
|
32
|
+
AND scope = 'global'
|
|
33
|
+
AND level = 'agent'
|
|
34
|
+
AND archived_at IS NULL
|
|
35
|
+
ORDER BY
|
|
36
|
+
CASE tier WHEN 'critical' THEN 1 WHEN 'important' THEN 2 ELSE 3 END,
|
|
37
|
+
created_at
|
|
38
|
+
`;
|
|
39
|
+
const agentResult = await executeQuery(agentQuery, [email]);
|
|
40
|
+
|
|
41
|
+
// Get relationship-level invariants (scope-specific)
|
|
42
|
+
const relationshipQuery = `
|
|
43
|
+
SELECT invariant_text as text, maturity, tier
|
|
44
|
+
FROM rapport.user_invariants
|
|
45
|
+
WHERE email_address = $1
|
|
46
|
+
AND scope = $2
|
|
47
|
+
AND level = 'relationship'
|
|
48
|
+
AND archived_at IS NULL
|
|
49
|
+
ORDER BY
|
|
50
|
+
CASE tier WHEN 'critical' THEN 1 WHEN 'important' THEN 2 ELSE 3 END,
|
|
51
|
+
created_at
|
|
52
|
+
`;
|
|
53
|
+
const relationshipResult = await executeQuery(relationshipQuery, [email, scope]);
|
|
54
|
+
|
|
55
|
+
return createSuccessResponse(
|
|
56
|
+
{
|
|
57
|
+
scope,
|
|
58
|
+
agent_level: agentResult.rows,
|
|
59
|
+
relationship_level: relationshipResult.rows
|
|
60
|
+
},
|
|
61
|
+
`${agentResult.rowCount} agent + ${relationshipResult.rowCount} relationship invariants`,
|
|
62
|
+
{
|
|
63
|
+
Request_ID,
|
|
64
|
+
Timestamp: new Date().toISOString()
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Handler Error:', error);
|
|
70
|
+
return handleError(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
exports.handler = wrapHandler(getInvariants);
|