@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,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);