@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.
- package/hooks/session-end.js +25 -0
- package/hooks/session-start.js +363 -83
- package/hooks/session-watcher.js +585 -0
- package/package.json +19 -13
- 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/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,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
|
-
});
|