@equilateral_ai/mindmeld 3.5.2 → 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.
Files changed (140) hide show
  1. package/hooks/session-end.js +25 -0
  2. package/hooks/session-start.js +363 -83
  3. package/hooks/session-watcher.js +585 -0
  4. package/package.json +19 -13
  5. package/scripts/init-project.js +9 -23
  6. package/src/client/dbShim.js +16 -0
  7. package/src/core/AuthManager.js +3 -2
  8. package/src/handlers/helpers/dbOperations.js +9 -46
  9. package/src/index.js +2 -217
  10. package/src/utils/piiMask.js +16 -0
  11. package/scripts/harvest.js +0 -601
  12. package/scripts/inject.js +0 -409
  13. package/scripts/mcp-bridge.js +0 -220
  14. package/scripts/repo-analyzer.js +0 -870
  15. package/src/collaboration/CollaborationPrompt.js +0 -460
  16. package/src/core/AlertEngine.js +0 -813
  17. package/src/core/AlertNotifier.js +0 -363
  18. package/src/core/CorrelationAnalyzer.js +0 -931
  19. package/src/core/CrossReferenceEngine.js +0 -624
  20. package/src/core/CurationEngine.js +0 -688
  21. package/src/core/DeprecationScheduler.js +0 -183
  22. package/src/core/LoadBearingDetector.js +0 -242
  23. package/src/core/NotificationService.js +0 -1032
  24. package/src/core/RapportOrchestrator.js +0 -632
  25. package/src/core/RelevanceDetector.js +0 -694
  26. package/src/core/StandardLifecycle.js +0 -244
  27. package/src/core/StandardsIngestion.js +0 -991
  28. package/src/core/TeamLoadBearingDetector.js +0 -431
  29. package/src/core/parsers/adrParser.js +0 -479
  30. package/src/core/parsers/cursorRulesParser.js +0 -564
  31. package/src/core/parsers/eslintParser.js +0 -439
  32. package/src/database/dbOperations.js +0 -105
  33. package/src/handlers/activity/activityGetMe.js +0 -98
  34. package/src/handlers/activity/activityGetTeam.js +0 -175
  35. package/src/handlers/admin/adminSetup.js +0 -216
  36. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  37. package/src/handlers/alerts/alertsGet.js +0 -250
  38. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  39. package/src/handlers/analytics/coachingGet.js +0 -361
  40. package/src/handlers/analytics/convergenceGet.js +0 -236
  41. package/src/handlers/analytics/developerScoreGet.js +0 -137
  42. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  43. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  44. package/src/handlers/collaborators/collaboratorList.js +0 -82
  45. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  46. package/src/handlers/collaborators/inviteAccept.js +0 -122
  47. package/src/handlers/company/companyUsersDelete.js +0 -141
  48. package/src/handlers/company/companyUsersGet.js +0 -90
  49. package/src/handlers/company/companyUsersPost.js +0 -267
  50. package/src/handlers/company/companyUsersPut.js +0 -76
  51. package/src/handlers/context/contextGet.js +0 -57
  52. package/src/handlers/context/invariantsGet.js +0 -74
  53. package/src/handlers/context/loopsGet.js +0 -82
  54. package/src/handlers/context/notesCreate.js +0 -74
  55. package/src/handlers/context/purposeGet.js +0 -78
  56. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  57. package/src/handlers/correlations/correlationsGet.js +0 -93
  58. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  59. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  60. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  61. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  62. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  63. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  64. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  65. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  66. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  67. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  68. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  69. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  70. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  71. package/src/handlers/github/githubConnectionStatus.js +0 -49
  72. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  73. package/src/handlers/github/githubOAuthCallback.js +0 -178
  74. package/src/handlers/github/githubOAuthStart.js +0 -59
  75. package/src/handlers/github/githubPatternsReview.js +0 -76
  76. package/src/handlers/github/githubReposList.js +0 -105
  77. package/src/handlers/health/healthGet.js +0 -55
  78. package/src/handlers/helpers/auditLogger.js +0 -201
  79. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  80. package/src/handlers/helpers/decisionFrames.js +0 -29
  81. package/src/handlers/helpers/errorHandler.js +0 -49
  82. package/src/handlers/helpers/index.js +0 -138
  83. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  84. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  85. package/src/handlers/helpers/predictiveCache.js +0 -51
  86. package/src/handlers/helpers/projectAccess.js +0 -88
  87. package/src/handlers/helpers/responseUtil.js +0 -55
  88. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  89. package/src/handlers/mcp/mcpHandler.js +0 -569
  90. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  91. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  92. package/src/handlers/notifications/getPreferences.js +0 -84
  93. package/src/handlers/notifications/sendNotification.js +0 -170
  94. package/src/handlers/notifications/updatePreferences.js +0 -316
  95. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  96. package/src/handlers/patterns/patternUsagePost.js +0 -182
  97. package/src/handlers/patterns/patternViolationPost.js +0 -185
  98. package/src/handlers/projects/projectCreate.js +0 -248
  99. package/src/handlers/projects/projectDelete.js +0 -82
  100. package/src/handlers/projects/projectGet.js +0 -95
  101. package/src/handlers/projects/projectUpdate.js +0 -117
  102. package/src/handlers/reports/aiLeverage.js +0 -210
  103. package/src/handlers/reports/engineeringInvestment.js +0 -132
  104. package/src/handlers/reports/riskForecast.js +0 -206
  105. package/src/handlers/reports/standardsRoi.js +0 -254
  106. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  107. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  108. package/src/handlers/scheduled/generateAlerts.js +0 -135
  109. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  110. package/src/handlers/scheduled/refreshActivity.js +0 -21
  111. package/src/handlers/scheduled/scanCompliance.js +0 -334
  112. package/src/handlers/sessions/sessionEndPost.js +0 -180
  113. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  114. package/src/handlers/standards/catalogGet.js +0 -185
  115. package/src/handlers/standards/catalogSync.js +0 -120
  116. package/src/handlers/standards/discoveriesGet.js +0 -89
  117. package/src/handlers/standards/projectStandardsGet.js +0 -129
  118. package/src/handlers/standards/projectStandardsPut.js +0 -151
  119. package/src/handlers/standards/standardsAuditGet.js +0 -65
  120. package/src/handlers/standards/standardsParseUpload.js +0 -149
  121. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  122. package/src/handlers/standards/standardsTransition.js +0 -161
  123. package/src/handlers/stripe/addonManagePost.js +0 -240
  124. package/src/handlers/stripe/billingPortalPost.js +0 -93
  125. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  126. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  127. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  128. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  129. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  130. package/src/handlers/stripe/webhookPost.js +0 -482
  131. package/src/handlers/user/apiTokenCreate.js +0 -71
  132. package/src/handlers/user/apiTokenList.js +0 -64
  133. package/src/handlers/user/userSplashAck.js +0 -91
  134. package/src/handlers/user/userSplashGet.js +0 -211
  135. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  136. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  137. package/src/handlers/users/userEntitlementsGet.js +0 -89
  138. package/src/handlers/users/userGet.js +0 -118
  139. package/src/handlers/users/userProfilePut.js +0 -77
  140. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,342 +0,0 @@
1
- /**
2
- * MindMeld MCP Streaming Handler — Lambda Function URL (RESPONSE_STREAM)
3
- *
4
- * Implements MCP Streamable HTTP transport (protocol version 2025-03-26):
5
- * - POST: JSON-RPC messages, responds with JSON or SSE stream
6
- * - GET: SSE handshake or OAuth discovery
7
- * - DELETE: Session close
8
- * - OPTIONS: CORS preflight
9
- *
10
- * Auth: OAuth 2.1 (Cognito JWT) OR X-MindMeld-Token / Bearer API token
11
- *
12
- * OAuth Discovery (RFC 9728):
13
- * GET /.well-known/oauth-protected-resource → resource metadata
14
- * 401 responses include WWW-Authenticate with resource_metadata URL
15
- *
16
- * Uses awslambda.streamifyResponse() for Lambda Function URL streaming.
17
- * The `awslambda` global is provided by the Lambda Node.js runtime.
18
- */
19
-
20
- const { validateApiToken, handleJsonRpc, CORS_HEADERS } = require('./mindmeldMcpCore');
21
- const crypto = require('crypto');
22
-
23
- // OAuth / Cognito configuration
24
- const COGNITO_ISSUER = 'https://cognito-idp.us-east-2.amazonaws.com/us-east-2_638OhwuV1';
25
- const COGNITO_AUTH_DOMAIN = 'https://mindmeld-auth.auth.us-east-2.amazoncognito.com';
26
- const MCP_OAUTH_CLIENT_ID = '5bjpug8up86so7k7ndttobc0qi';
27
-
28
- /**
29
- * Write an SSE event to the response stream
30
- */
31
- function writeSSE(stream, data, eventType) {
32
- if (eventType) {
33
- stream.write(`event: ${eventType}\n`);
34
- }
35
- stream.write(`data: ${JSON.stringify(data)}\n\n`);
36
- }
37
-
38
- /**
39
- * Parse Lambda Function URL event (different format from API Gateway)
40
- */
41
- function parseRequest(event) {
42
- const http = event.requestContext?.http || {};
43
- return {
44
- method: http.method || 'GET',
45
- path: http.path || '/',
46
- headers: event.headers || {},
47
- body: event.body || '',
48
- isBase64Encoded: event.isBase64Encoded || false,
49
- };
50
- }
51
-
52
- /**
53
- * Create a response stream with headers
54
- */
55
- function createStream(responseStream, statusCode, headers) {
56
- return awslambda.HttpResponseStream.from(responseStream, {
57
- statusCode,
58
- headers: { ...CORS_HEADERS, ...headers },
59
- });
60
- }
61
-
62
- /**
63
- * Build the Function URL base from the event
64
- */
65
- function getBaseUrl(event) {
66
- const host = event.headers?.host || '';
67
- return `https://${host}`;
68
- }
69
-
70
- /**
71
- * Return OAuth Protected Resource Metadata (RFC 9728)
72
- * Points to ourselves as the authorization server so we can serve
73
- * metadata that includes code_challenge_methods_supported (PKCE).
74
- * Actual OAuth endpoints still point to Cognito.
75
- */
76
- function getResourceMetadata(baseUrl) {
77
- return {
78
- resource: baseUrl,
79
- authorization_servers: [baseUrl],
80
- scopes_supported: ['mcp/standards', 'openid', 'email', 'profile'],
81
- bearer_methods_supported: ['header'],
82
- };
83
- }
84
-
85
- /**
86
- * Return OAuth Authorization Server Metadata (RFC 8414)
87
- * Wraps Cognito's endpoints with PKCE support declaration.
88
- * Cognito supports PKCE but doesn't advertise it in OIDC metadata.
89
- */
90
- function getAuthServerMetadata(baseUrl) {
91
- return {
92
- issuer: baseUrl,
93
- authorization_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/authorize`,
94
- token_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/token`,
95
- revocation_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/revoke`,
96
- userinfo_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/userInfo`,
97
- jwks_uri: `${COGNITO_ISSUER}/.well-known/jwks.json`,
98
- scopes_supported: ['mcp/standards', 'openid', 'email', 'profile'],
99
- response_types_supported: ['code'],
100
- grant_types_supported: ['authorization_code', 'refresh_token'],
101
- token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'],
102
- code_challenge_methods_supported: ['S256'],
103
- subject_types_supported: ['public'],
104
- id_token_signing_alg_values_supported: ['RS256'],
105
- };
106
- }
107
-
108
- /**
109
- * Return 401 with WWW-Authenticate header per MCP OAuth spec
110
- */
111
- function write401(stream, baseUrl) {
112
- stream.write(JSON.stringify({
113
- error: 'unauthorized',
114
- message: 'Authentication required. Use OAuth or provide X-MindMeld-Token header.',
115
- }));
116
- stream.end();
117
- }
118
-
119
- // Lambda Function URL streaming handler
120
- const streamHandler = async (event, responseStream, context) => {
121
- // CRITICAL: Don't wait for event loop to drain — the cached pg client
122
- // keeps it alive forever, causing 900s timeouts on every request.
123
- context.callbackWaitsForEmptyEventLoop = false;
124
-
125
- const { method, headers, body, isBase64Encoded } = parseRequest(event);
126
- const path = parseRequest(event).path;
127
- const baseUrl = getBaseUrl(event);
128
-
129
- // Request logging for debugging
130
- const hasAuth = !!(headers['authorization'] || headers['x-mindmeld-token']);
131
- console.log(`[MCP-Stream] ${method} ${path} auth=${hasAuth} accept=${headers['accept'] || 'none'}`);
132
-
133
- // === OAuth Discovery: Protected Resource Metadata (RFC 9728) ===
134
- if (method === 'GET' && path === '/.well-known/oauth-protected-resource') {
135
- const stream = createStream(responseStream, 200, {
136
- 'Content-Type': 'application/json',
137
- 'Cache-Control': 'public, max-age=3600',
138
- });
139
- stream.write(JSON.stringify(getResourceMetadata(baseUrl)));
140
- stream.end();
141
- return;
142
- }
143
-
144
- // === OAuth Discovery: Authorization Server Metadata (RFC 8414) ===
145
- if (method === 'GET' && path === '/.well-known/oauth-authorization-server') {
146
- const stream = createStream(responseStream, 200, {
147
- 'Content-Type': 'application/json',
148
- 'Cache-Control': 'public, max-age=3600',
149
- });
150
- stream.write(JSON.stringify(getAuthServerMetadata(baseUrl)));
151
- stream.end();
152
- return;
153
- }
154
-
155
- // === OPTIONS: CORS preflight ===
156
- if (method === 'OPTIONS') {
157
- const stream = createStream(responseStream, 200, {
158
- 'Access-Control-Max-Age': '86400',
159
- });
160
- stream.end();
161
- return;
162
- }
163
-
164
- // === DELETE: Session close ===
165
- if (method === 'DELETE') {
166
- const stream = createStream(responseStream, 200, {
167
- 'Content-Type': 'application/json',
168
- });
169
- stream.end();
170
- return;
171
- }
172
-
173
- // WWW-Authenticate header for 401 responses
174
- const wwwAuth = `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`;
175
-
176
- // === GET: SSE handshake ===
177
- if (method === 'GET') {
178
- const authResult = await validateApiToken(headers);
179
- if (authResult.error) {
180
- const stream = createStream(responseStream, 401, {
181
- 'Content-Type': 'application/json',
182
- 'WWW-Authenticate': wwwAuth,
183
- });
184
- write401(stream, baseUrl);
185
- return;
186
- }
187
-
188
- const sessionId = crypto.randomUUID();
189
- const stream = createStream(responseStream, 200, {
190
- 'Content-Type': 'text/event-stream',
191
- 'Cache-Control': 'no-cache',
192
- 'Connection': 'keep-alive',
193
- 'Mcp-Session-Id': sessionId,
194
- });
195
-
196
- // Send endpoint event — legacy SSE clients expect this
197
- stream.write(`event: endpoint\ndata: ${path}\n\n`);
198
-
199
- // For Streamable HTTP clients probing via GET: close after endpoint event.
200
- // The client will then POST to us for actual JSON-RPC messages.
201
- stream.end();
202
- return;
203
- }
204
-
205
- // === POST: Streamable HTTP JSON-RPC ===
206
- if (method !== 'POST') {
207
- const stream = createStream(responseStream, 405, {
208
- 'Content-Type': 'application/json',
209
- });
210
- stream.write(JSON.stringify({ error: 'Method not allowed' }));
211
- stream.end();
212
- return;
213
- }
214
-
215
- // Auth
216
- const authResult = await validateApiToken(headers);
217
- if (authResult.error) {
218
- console.log(`[MCP-Stream] POST auth failed: ${authResult.error} - ${authResult.message}`);
219
- const stream = createStream(responseStream, 401, {
220
- 'Content-Type': 'application/json',
221
- 'WWW-Authenticate': wwwAuth,
222
- });
223
- write401(stream, baseUrl);
224
- return;
225
- }
226
- console.log(`[MCP-Stream] POST auth OK: ${authResult.user.email}`);
227
-
228
- // Parse body (Function URL may base64-encode it)
229
- let parsedBody;
230
- try {
231
- const raw = isBase64Encoded ? Buffer.from(body, 'base64').toString() : body;
232
- parsedBody = JSON.parse(raw);
233
- } catch (e) {
234
- const stream = createStream(responseStream, 400, {
235
- 'Content-Type': 'application/json',
236
- });
237
- stream.write(JSON.stringify({
238
- jsonrpc: '2.0',
239
- error: { code: -32700, message: 'Parse error: invalid JSON' },
240
- id: null,
241
- }));
242
- stream.end();
243
- return;
244
- }
245
-
246
- // Session ID management
247
- const sessionId = headers['mcp-session-id'] || crypto.randomUUID();
248
-
249
- // Check if client wants SSE response
250
- const acceptHeader = headers['accept'] || '';
251
- const wantsSSE = acceptHeader.includes('text/event-stream');
252
-
253
- if (wantsSSE) {
254
- // === SSE streaming response ===
255
- const stream = createStream(responseStream, 200, {
256
- 'Content-Type': 'text/event-stream',
257
- 'Cache-Control': 'no-cache',
258
- 'Mcp-Session-Id': sessionId,
259
- });
260
-
261
- if (Array.isArray(parsedBody)) {
262
- for (const msg of parsedBody) {
263
- const response = await handleJsonRpc(msg, authResult.user);
264
- if (response) {
265
- writeSSE(stream, response, 'message');
266
- }
267
- }
268
- } else {
269
- const response = await handleJsonRpc(parsedBody, authResult.user);
270
- if (response) {
271
- writeSSE(stream, response, 'message');
272
- }
273
- }
274
-
275
- stream.end();
276
- } else {
277
- // === Standard JSON response ===
278
- const responseHeaders = {
279
- 'Content-Type': 'application/json',
280
- 'Mcp-Session-Id': sessionId,
281
- };
282
-
283
- if (Array.isArray(parsedBody)) {
284
- const responses = [];
285
- for (const msg of parsedBody) {
286
- const response = await handleJsonRpc(msg, authResult.user);
287
- if (response) responses.push(response);
288
- }
289
- const stream = createStream(responseStream,
290
- responses.length > 0 ? 200 : 202, responseHeaders);
291
- if (responses.length > 0) {
292
- stream.write(JSON.stringify(responses));
293
- }
294
- stream.end();
295
- } else {
296
- const response = await handleJsonRpc(parsedBody, authResult.user);
297
- if (!response) {
298
- const stream = createStream(responseStream, 202, responseHeaders);
299
- stream.end();
300
- } else {
301
- const stream = createStream(responseStream, 200, responseHeaders);
302
- stream.write(JSON.stringify(response));
303
- stream.end();
304
- }
305
- }
306
- }
307
- };
308
-
309
- // awslambda is a global provided by the Lambda Node.js runtime.
310
- // In local/test environments it won't exist — export the raw handler for testing.
311
- if (typeof awslambda !== 'undefined') {
312
- exports.handler = awslambda.streamifyResponse(streamHandler);
313
- } else {
314
- // Fallback for local testing: wrap as standard async handler
315
- exports.handler = async (event) => {
316
- let result = { statusCode: 200, headers: {}, body: '' };
317
- const mockStream = {
318
- _headers: {},
319
- _statusCode: 200,
320
- _chunks: [],
321
- write(chunk) { this._chunks.push(chunk); },
322
- end() {},
323
- };
324
- // Mock awslambda.HttpResponseStream.from
325
- const originalFrom = mockStream;
326
- const createMockStream = (_rs, meta) => {
327
- mockStream._statusCode = meta.statusCode;
328
- mockStream._headers = meta.headers;
329
- return mockStream;
330
- };
331
- global.awslambda = {
332
- HttpResponseStream: { from: createMockStream },
333
- };
334
- await streamHandler(event, mockStream, {});
335
- delete global.awslambda;
336
- return {
337
- statusCode: mockStream._statusCode,
338
- headers: mockStream._headers,
339
- body: mockStream._chunks.join(''),
340
- };
341
- };
342
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * Get Notification Preferences Handler
3
- * Retrieves user's notification preferences
4
- *
5
- * GET /api/notifications/preferences
6
- * Auth: Cognito JWT required
7
- */
8
-
9
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
10
-
11
- exports.handler = wrapHandler(async ({ requestContext }) => {
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
- // Get user preferences using the helper function
20
- const prefsResult = await executeQuery(`
21
- SELECT rapport.get_notification_preferences($1) as preferences
22
- `, [email]);
23
-
24
- const preferences = prefsResult.rows[0]?.preferences || getDefaultPreferences();
25
-
26
- // Get notification log summary (last 30 days)
27
- const logSummary = await executeQuery(`
28
- SELECT
29
- notification_type,
30
- channel,
31
- status,
32
- COUNT(*) as count
33
- FROM rapport.notification_log
34
- WHERE email_address = $1
35
- AND created_at > NOW() - INTERVAL '30 days'
36
- GROUP BY notification_type, channel, status
37
- ORDER BY notification_type, channel
38
- `, [email]);
39
-
40
- // Get pending digests
41
- const pendingDigests = await executeQuery(`
42
- SELECT
43
- digest_type,
44
- scheduled_for,
45
- status
46
- FROM rapport.digest_queue
47
- WHERE email_address = $1
48
- AND status = 'pending'
49
- ORDER BY scheduled_for
50
- `, [email]);
51
-
52
- return createSuccessResponse(
53
- {
54
- preferences: preferences,
55
- notification_summary: logSummary.rows,
56
- pending_digests: pendingDigests.rows
57
- },
58
- 'Preferences retrieved',
59
- { Request_ID, Timestamp: new Date().toISOString() }
60
- );
61
- });
62
-
63
- /**
64
- * Get default notification preferences
65
- */
66
- function getDefaultPreferences() {
67
- return {
68
- email_enabled: true,
69
- slack_enabled: true,
70
- notification_types: {
71
- pattern_promotion: { email: true, slack: true },
72
- weekly_digest: { email: true, slack: false },
73
- critical_violation: { email: true, slack: true },
74
- team_alert: { email: true, slack: true },
75
- curation_candidate: { email: true, slack: true }
76
- },
77
- project_overrides: {},
78
- digest_frequency: 'weekly',
79
- digest_day: 1,
80
- quiet_hours_enabled: false,
81
- quiet_hours_timezone: 'America/New_York',
82
- slack_dm_enabled: true
83
- };
84
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * Send Notification Handler
3
- * Sends email and/or Slack notifications based on user preferences
4
- *
5
- * POST /api/notifications/send
6
- * Auth: Cognito JWT required (Admin or internal service)
7
- *
8
- * Body:
9
- * {
10
- * "type": "pattern_promotion|weekly_digest|critical_violation|team_alert|curation_candidate",
11
- * "recipients": ["email@example.com"] | "all_project" | "all_admins",
12
- * "projectId": "prj_xxx" (optional),
13
- * "data": { ... notification-specific data ... }
14
- * }
15
- */
16
-
17
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
18
- const { NotificationService } = require('./core/NotificationService');
19
-
20
- // Initialize notification service (singleton for Lambda warm starts)
21
- const notificationService = new NotificationService();
22
-
23
- exports.handler = wrapHandler(async ({ requestContext, body }) => {
24
- const Request_ID = requestContext.requestId;
25
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
26
-
27
- if (!email) {
28
- return createErrorResponse(401, 'Authentication required');
29
- }
30
-
31
- // Validate required fields
32
- const { type, recipients, projectId, data } = body;
33
-
34
- if (!type) {
35
- return createErrorResponse(400, 'Notification type is required');
36
- }
37
-
38
- if (!recipients) {
39
- return createErrorResponse(400, 'Recipients are required');
40
- }
41
-
42
- if (!data) {
43
- return createErrorResponse(400, 'Notification data is required');
44
- }
45
-
46
- // Validate notification type
47
- const validTypes = ['pattern_promotion', 'weekly_digest', 'critical_violation', 'team_alert', 'curation_candidate'];
48
- if (!validTypes.includes(type)) {
49
- return createErrorResponse(400, `Invalid notification type. Valid types: ${validTypes.join(', ')}`);
50
- }
51
-
52
- // Check authorization (only admins and managers can send notifications)
53
- const authCheck = await executeQuery(`
54
- SELECT
55
- ue.admin,
56
- ue.manager,
57
- u.super_admin,
58
- ue.company_id
59
- FROM rapport.user_entitlements ue
60
- JOIN rapport.users u ON ue.email_address = u.email_address
61
- WHERE ue.email_address = $1
62
- `, [email]);
63
-
64
- if (authCheck.rowCount === 0) {
65
- return createErrorResponse(403, 'Access denied');
66
- }
67
-
68
- const userRole = authCheck.rows[0];
69
- const isAuthorized = userRole.super_admin || userRole.admin || userRole.manager;
70
-
71
- if (!isAuthorized) {
72
- return createErrorResponse(403, 'Only admins and managers can send notifications');
73
- }
74
-
75
- // Resolve recipients
76
- let recipientEmails = [];
77
-
78
- if (Array.isArray(recipients)) {
79
- // Direct email list
80
- recipientEmails = recipients;
81
- } else if (recipients === 'all_project' && projectId) {
82
- // All project collaborators
83
- const collaborators = await executeQuery(`
84
- SELECT pc.email_address
85
- FROM rapport.project_collaborators pc
86
- WHERE pc.project_id = $1
87
- `, [projectId]);
88
- recipientEmails = collaborators.rows.map(r => r.email_address);
89
- } else if (recipients === 'all_admins') {
90
- // All company admins
91
- const admins = await executeQuery(`
92
- SELECT ue.email_address
93
- FROM rapport.user_entitlements ue
94
- WHERE ue.company_id = $1 AND ue.admin = true
95
- `, [userRole.company_id]);
96
- recipientEmails = admins.rows.map(r => r.email_address);
97
- } else if (recipients === 'all_managers') {
98
- // All company managers
99
- const managers = await executeQuery(`
100
- SELECT ue.email_address
101
- FROM rapport.user_entitlements ue
102
- WHERE ue.company_id = $1 AND (ue.manager = true OR ue.admin = true)
103
- `, [userRole.company_id]);
104
- recipientEmails = managers.rows.map(r => r.email_address);
105
- } else {
106
- return createErrorResponse(400, 'Invalid recipients format');
107
- }
108
-
109
- if (recipientEmails.length === 0) {
110
- return createSuccessResponse({
111
- sent: 0,
112
- skipped: 0,
113
- failed: 0,
114
- message: 'No recipients found'
115
- }, 'No notifications sent');
116
- }
117
-
118
- // Get preferences for all recipients
119
- const prefsQuery = await executeQuery(`
120
- SELECT email_address, rapport.get_notification_preferences(email_address) as preferences
121
- FROM rapport.users
122
- WHERE email_address = ANY($1)
123
- `, [recipientEmails]);
124
-
125
- const prefsMap = new Map(prefsQuery.rows.map(r => [r.email_address, r.preferences]));
126
-
127
- // Build recipient list with preferences
128
- const recipientList = recipientEmails.map(recipientEmail => ({
129
- email: recipientEmail,
130
- preferences: prefsMap.get(recipientEmail) || null,
131
- data: data,
132
- projectId: projectId
133
- }));
134
-
135
- // Send batch notifications
136
- const results = await notificationService.sendBatch(recipientList, type);
137
-
138
- // Log notifications
139
- for (const recipientEmail of recipientEmails) {
140
- const status = results.errors.find(e => e.email === recipientEmail) ? 'failed' : 'sent';
141
- const errorMessage = results.errors.find(e => e.email === recipientEmail)?.error || null;
142
-
143
- await executeQuery(`
144
- SELECT rapport.log_notification($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
145
- `, [
146
- recipientEmail,
147
- type,
148
- 'email', // Primary channel
149
- status,
150
- projectId || null,
151
- data.referenceType || null,
152
- data.referenceId || null,
153
- JSON.stringify(data),
154
- null, // message_id populated by NotificationService if available
155
- errorMessage
156
- ]);
157
- }
158
-
159
- return createSuccessResponse(
160
- {
161
- total: results.total,
162
- sent: results.sent,
163
- skipped: results.skipped,
164
- failed: results.failed,
165
- errors: results.errors.length > 0 ? results.errors : undefined
166
- },
167
- 'Notifications processed',
168
- { Request_ID, Timestamp: new Date().toISOString() }
169
- );
170
- });