@equilateral_ai/mindmeld 3.3.0 → 3.4.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 +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-start.js +636 -42
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +12 -8
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +46 -51
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Token Create Handler
|
|
3
|
+
*
|
|
4
|
+
* Creates a new MCP API token for the authenticated user.
|
|
5
|
+
* Token plaintext is returned ONCE — only the SHA-256 hash is stored.
|
|
6
|
+
*
|
|
7
|
+
* POST /api/user/api-tokens
|
|
8
|
+
* Auth: Cognito JWT required
|
|
9
|
+
* Body: { name?: string }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
async function apiTokenCreate({ body, requestContext }) {
|
|
16
|
+
const email = requestContext.authorizer?.claims?.email
|
|
17
|
+
|| requestContext.authorizer?.jwt?.claims?.email;
|
|
18
|
+
|
|
19
|
+
if (!email) {
|
|
20
|
+
return createErrorResponse(401, 'Authentication required');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tokenName = body.name || 'Default';
|
|
24
|
+
|
|
25
|
+
// Look up user's client and company
|
|
26
|
+
const userResult = await executeQuery(`
|
|
27
|
+
SELECT u.client_id, ue.company_id
|
|
28
|
+
FROM rapport.users u
|
|
29
|
+
LEFT JOIN rapport.user_entitlements ue ON u.email_address = ue.email_address
|
|
30
|
+
WHERE u.email_address = $1
|
|
31
|
+
LIMIT 1
|
|
32
|
+
`, [email]);
|
|
33
|
+
|
|
34
|
+
if (userResult.rows.length === 0) {
|
|
35
|
+
return createErrorResponse(404, 'User not found');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { client_id, company_id } = userResult.rows[0];
|
|
39
|
+
|
|
40
|
+
// Verify active subscription
|
|
41
|
+
const clientResult = await executeQuery(
|
|
42
|
+
'SELECT subscription_tier FROM rapport.clients WHERE client_id = $1',
|
|
43
|
+
[client_id]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (clientResult.rows.length === 0 || !clientResult.rows[0].subscription_tier || clientResult.rows[0].subscription_tier === 'free') {
|
|
47
|
+
return createErrorResponse(403, 'Active MindMeld subscription required to create API tokens');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate token: mm_live_ + 40 random hex chars
|
|
51
|
+
const tokenId = crypto.randomUUID();
|
|
52
|
+
const tokenRandom = crypto.randomBytes(20).toString('hex');
|
|
53
|
+
const plaintext = `mm_live_${tokenRandom}`;
|
|
54
|
+
const tokenPrefix = plaintext.substring(0, 12);
|
|
55
|
+
const tokenHash = crypto.createHash('sha256').update(plaintext).digest('hex');
|
|
56
|
+
|
|
57
|
+
await executeQuery(`
|
|
58
|
+
INSERT INTO rapport.api_tokens (token_id, token_hash, token_prefix, email_address, client_id, company_id, token_name)
|
|
59
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
60
|
+
`, [tokenId, tokenHash, tokenPrefix, email, client_id, company_id || null, tokenName]);
|
|
61
|
+
|
|
62
|
+
return createSuccessResponse({
|
|
63
|
+
token_id: tokenId,
|
|
64
|
+
token: plaintext,
|
|
65
|
+
token_prefix: tokenPrefix,
|
|
66
|
+
name: tokenName,
|
|
67
|
+
message: 'Save this token — it will not be shown again.'
|
|
68
|
+
}, 'API token created');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
exports.handler = wrapHandler(apiTokenCreate);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Token List/Revoke Handler
|
|
3
|
+
*
|
|
4
|
+
* GET /api/user/api-tokens — List tokens (prefix only, never plaintext)
|
|
5
|
+
* DELETE /api/user/api-tokens?token_id=xxx — Revoke a token
|
|
6
|
+
*
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
async function apiTokenList({ body, queryParams, requestContext, httpMethod }) {
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email
|
|
14
|
+
|| requestContext.authorizer?.jwt?.claims?.email;
|
|
15
|
+
|
|
16
|
+
if (!email) {
|
|
17
|
+
return createErrorResponse(401, 'Authentication required');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// DELETE — revoke a token
|
|
21
|
+
if (httpMethod === 'DELETE') {
|
|
22
|
+
const tokenId = queryParams.token_id || body.token_id;
|
|
23
|
+
if (!tokenId) {
|
|
24
|
+
return createErrorResponse(400, 'token_id is required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await executeQuery(`
|
|
28
|
+
UPDATE rapport.api_tokens
|
|
29
|
+
SET status = 'revoked', revoked_at = NOW()
|
|
30
|
+
WHERE token_id = $1 AND email_address = $2 AND status = 'active'
|
|
31
|
+
RETURNING token_id
|
|
32
|
+
`, [tokenId, email]);
|
|
33
|
+
|
|
34
|
+
if (result.rows.length === 0) {
|
|
35
|
+
return createErrorResponse(404, 'Token not found or already revoked');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return createSuccessResponse({ token_id: tokenId }, 'Token revoked');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GET — list tokens
|
|
42
|
+
const result = await executeQuery(`
|
|
43
|
+
SELECT token_id, token_prefix, token_name, status,
|
|
44
|
+
last_used_at, request_count, created_at, revoked_at
|
|
45
|
+
FROM rapport.api_tokens
|
|
46
|
+
WHERE email_address = $1
|
|
47
|
+
ORDER BY created_at DESC
|
|
48
|
+
`, [email]);
|
|
49
|
+
|
|
50
|
+
return createSuccessResponse({
|
|
51
|
+
tokens: result.rows.map(r => ({
|
|
52
|
+
token_id: r.token_id,
|
|
53
|
+
prefix: r.token_prefix,
|
|
54
|
+
name: r.token_name,
|
|
55
|
+
status: r.status,
|
|
56
|
+
last_used: r.last_used_at,
|
|
57
|
+
request_count: r.request_count,
|
|
58
|
+
created_at: r.created_at,
|
|
59
|
+
revoked_at: r.revoked_at
|
|
60
|
+
}))
|
|
61
|
+
}, `${result.rows.length} token(s) found`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.handler = wrapHandler(apiTokenList);
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Returns:
|
|
9
9
|
* - show_splash: whether to display the splash (false if already acknowledged this week)
|
|
10
|
-
* - summary: weekly activity metrics
|
|
11
|
-
* - is_greenfield: true if user has < 7 days of
|
|
10
|
+
* - summary: weekly activity metrics from sessions + session_standards
|
|
11
|
+
* - is_greenfield: true if user has < 7 days of session history
|
|
12
12
|
* - tips: getting-started tips for greenfield users
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ async function getUserSplash({ requestContext }) {
|
|
|
22
22
|
return createErrorResponse(401, 'Authentication required');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Calculate current week boundaries (Monday to Sunday)
|
|
25
|
+
// Calculate current week boundaries (Monday to Sunday UTC)
|
|
26
26
|
const now = new Date();
|
|
27
27
|
const dayOfWeek = now.getUTCDay();
|
|
28
28
|
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
|
@@ -62,119 +62,136 @@ async function getUserSplash({ requestContext }) {
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// Determine if user is greenfield (< 7 days of
|
|
65
|
+
// Determine if user is greenfield (< 7 days of session history)
|
|
66
66
|
let isGreenfield = true;
|
|
67
67
|
try {
|
|
68
68
|
const activityCheck = await executeQuery(`
|
|
69
|
-
SELECT MIN(
|
|
70
|
-
FROM rapport.
|
|
69
|
+
SELECT MIN(started_at) as first_session
|
|
70
|
+
FROM rapport.sessions
|
|
71
71
|
WHERE email_address = $1
|
|
72
72
|
`, [email]);
|
|
73
73
|
|
|
74
|
-
if (activityCheck.rowCount > 0 && activityCheck.rows[0].
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
isGreenfield =
|
|
74
|
+
if (activityCheck.rowCount > 0 && activityCheck.rows[0].first_session) {
|
|
75
|
+
const firstSession = new Date(activityCheck.rows[0].first_session);
|
|
76
|
+
const daysSinceFirst = Math.floor((now.getTime() - firstSession.getTime()) / (1000 * 60 * 60 * 24));
|
|
77
|
+
isGreenfield = daysSinceFirst < 7;
|
|
78
78
|
}
|
|
79
79
|
} catch (err) {
|
|
80
80
|
console.log('[Splash] Could not determine greenfield status:', err.message);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// Aggregate weekly
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const patternsResult = await executeQuery(`
|
|
87
|
-
SELECT COUNT(*) as count
|
|
88
|
-
FROM rapport.patterns
|
|
89
|
-
WHERE created_by = $1
|
|
90
|
-
AND created_at >= $2
|
|
91
|
-
AND created_at <= $3
|
|
92
|
-
`, [email, weekStart.toISOString(), weekEnd.toISOString()]);
|
|
93
|
-
|
|
94
|
-
patternsHarvested = parseInt(patternsResult.rows[0].count, 10) || 0;
|
|
95
|
-
} catch (err) {
|
|
96
|
-
console.log('[Splash] Patterns query failed:', err.message);
|
|
97
|
-
}
|
|
83
|
+
// Aggregate weekly metrics from sessions + session_standards
|
|
84
|
+
const weekStartISO = weekStart.toISOString();
|
|
85
|
+
const weekEndISO = weekEnd.toISOString();
|
|
98
86
|
|
|
99
|
-
|
|
87
|
+
// Session stats (count, duration, projects)
|
|
88
|
+
let sessionsCount = 0;
|
|
89
|
+
let totalDurationMinutes = 0;
|
|
90
|
+
let activeProjects = 0;
|
|
100
91
|
try {
|
|
101
|
-
const
|
|
102
|
-
SELECT
|
|
103
|
-
|
|
92
|
+
const sessionResult = await executeQuery(`
|
|
93
|
+
SELECT
|
|
94
|
+
COUNT(*) as session_count,
|
|
95
|
+
COALESCE(ROUND(SUM(duration_seconds) / 60.0), 0) as total_minutes,
|
|
96
|
+
COUNT(DISTINCT project_id) as project_count
|
|
97
|
+
FROM rapport.sessions
|
|
104
98
|
WHERE email_address = $1
|
|
105
|
-
AND
|
|
106
|
-
AND
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
AND started_at >= $2
|
|
100
|
+
AND started_at <= $3
|
|
101
|
+
`, [email, weekStartISO, weekEndISO]);
|
|
102
|
+
|
|
103
|
+
if (sessionResult.rowCount > 0) {
|
|
104
|
+
sessionsCount = parseInt(sessionResult.rows[0].session_count, 10) || 0;
|
|
105
|
+
totalDurationMinutes = parseInt(sessionResult.rows[0].total_minutes, 10) || 0;
|
|
106
|
+
activeProjects = parseInt(sessionResult.rows[0].project_count, 10) || 0;
|
|
107
|
+
}
|
|
111
108
|
} catch (err) {
|
|
112
|
-
console.log('[Splash]
|
|
109
|
+
console.log('[Splash] Session stats query failed:', err.message);
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
|
|
112
|
+
// Standards stats (injected, followed, violated)
|
|
113
|
+
let standardsInjected = 0;
|
|
114
|
+
let standardsFollowed = 0;
|
|
115
|
+
let violationsDetected = 0;
|
|
116
116
|
try {
|
|
117
|
-
const
|
|
118
|
-
SELECT
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
const standardsResult = await executeQuery(`
|
|
118
|
+
SELECT
|
|
119
|
+
COUNT(*) as total_injected,
|
|
120
|
+
COUNT(*) FILTER (WHERE ss.followed = true) as total_followed,
|
|
121
|
+
COUNT(*) FILTER (WHERE ss.violated = true) as total_violated
|
|
122
|
+
FROM rapport.session_standards ss
|
|
123
|
+
JOIN rapport.sessions s ON s.session_id = ss.session_id
|
|
124
|
+
WHERE s.email_address = $1
|
|
125
|
+
AND s.started_at >= $2
|
|
126
|
+
AND s.started_at <= $3
|
|
127
|
+
`, [email, weekStartISO, weekEndISO]);
|
|
128
|
+
|
|
129
|
+
if (standardsResult.rowCount > 0) {
|
|
130
|
+
standardsInjected = parseInt(standardsResult.rows[0].total_injected, 10) || 0;
|
|
131
|
+
standardsFollowed = parseInt(standardsResult.rows[0].total_followed, 10) || 0;
|
|
132
|
+
violationsDetected = parseInt(standardsResult.rows[0].total_violated, 10) || 0;
|
|
133
|
+
}
|
|
127
134
|
} catch (err) {
|
|
128
|
-
console.log('[Splash]
|
|
135
|
+
console.log('[Splash] Standards stats query failed:', err.message);
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
|
|
138
|
+
// Patterns harvested (keep existing query — patterns table exists)
|
|
139
|
+
let patternsHarvested = 0;
|
|
132
140
|
try {
|
|
133
|
-
const
|
|
141
|
+
const patternsResult = await executeQuery(`
|
|
134
142
|
SELECT COUNT(*) as count
|
|
135
|
-
FROM rapport.
|
|
136
|
-
WHERE
|
|
137
|
-
AND action = 'violation_detected'
|
|
143
|
+
FROM rapport.patterns
|
|
144
|
+
WHERE created_by = $1
|
|
138
145
|
AND created_at >= $2
|
|
139
146
|
AND created_at <= $3
|
|
140
|
-
`, [email,
|
|
147
|
+
`, [email, weekStartISO, weekEndISO]);
|
|
141
148
|
|
|
142
|
-
|
|
149
|
+
patternsHarvested = parseInt(patternsResult.rows[0].count, 10) || 0;
|
|
143
150
|
} catch (err) {
|
|
144
|
-
console.log('[Splash]
|
|
151
|
+
console.log('[Splash] Patterns query failed:', err.message);
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
// Recent category distribution (what the user has been working on)
|
|
155
|
+
let recentCategories = {};
|
|
148
156
|
try {
|
|
149
|
-
const
|
|
150
|
-
SELECT COUNT(
|
|
151
|
-
FROM rapport.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
const categoryResult = await executeQuery(`
|
|
158
|
+
SELECT sp.category, COUNT(*) as usage_count
|
|
159
|
+
FROM rapport.session_standards ss
|
|
160
|
+
JOIN rapport.sessions s ON s.session_id = ss.session_id
|
|
161
|
+
JOIN rapport.standards_patterns sp ON sp.pattern_id = ss.standard_id
|
|
162
|
+
WHERE s.email_address = $1
|
|
163
|
+
AND s.started_at >= $2
|
|
164
|
+
AND s.started_at <= $3
|
|
165
|
+
GROUP BY sp.category
|
|
166
|
+
ORDER BY usage_count DESC
|
|
167
|
+
LIMIT 5
|
|
168
|
+
`, [email, weekStartISO, weekEndISO]);
|
|
169
|
+
|
|
170
|
+
for (const row of categoryResult.rows) {
|
|
171
|
+
recentCategories[row.category] = parseInt(row.usage_count, 10);
|
|
172
|
+
}
|
|
159
173
|
} catch (err) {
|
|
160
|
-
console.log('[Splash]
|
|
174
|
+
console.log('[Splash] Recent categories query failed:', err.message);
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
// Build tips for greenfield users
|
|
164
178
|
const tips = isGreenfield ? [
|
|
165
|
-
'Connect a GitHub repository to start harvesting patterns from your codebase.',
|
|
166
179
|
'MindMeld automatically injects relevant standards into your AI coding sessions.',
|
|
180
|
+
'Use /mm-load <domain> to pre-load standards before starting work (e.g., /mm-load Frontend).',
|
|
181
|
+
'Use /mm-workflows to see available step-by-step procedures.',
|
|
167
182
|
'Patterns discovered in your code can be promoted to team-wide standards.',
|
|
168
|
-
'
|
|
169
|
-
'Visit the Dashboard regularly to track your team\'s standards adoption progress.'
|
|
183
|
+
'Use /mm-status to check your system health and API connectivity.'
|
|
170
184
|
] : [];
|
|
171
185
|
|
|
172
186
|
const summary = {
|
|
173
|
-
|
|
187
|
+
sessions_count: sessionsCount,
|
|
188
|
+
total_duration_minutes: totalDurationMinutes,
|
|
174
189
|
standards_injected: standardsInjected,
|
|
175
|
-
|
|
190
|
+
standards_followed: standardsFollowed,
|
|
176
191
|
violations_detected: violationsDetected,
|
|
177
192
|
active_projects: activeProjects,
|
|
193
|
+
recent_categories: recentCategories,
|
|
194
|
+
patterns_harvested: patternsHarvested,
|
|
178
195
|
week_start: weekStartStr,
|
|
179
196
|
week_end: weekEndStr
|
|
180
197
|
};
|
|
@@ -111,7 +111,7 @@ async function handler(event, context) {
|
|
|
111
111
|
|
|
112
112
|
console.log('Personal company created:', companyId);
|
|
113
113
|
|
|
114
|
-
// 4. Create admin entitlement
|
|
114
|
+
// 4. Create admin entitlement for personal workspace
|
|
115
115
|
const entitlementQuery = `
|
|
116
116
|
INSERT INTO rapport.user_entitlements (
|
|
117
117
|
email_address,
|
|
@@ -128,6 +128,42 @@ async function handler(event, context) {
|
|
|
128
128
|
|
|
129
129
|
console.log('Entitlement created for:', email);
|
|
130
130
|
|
|
131
|
+
// 5. Check for and accept pending enterprise invitations
|
|
132
|
+
const pendingInvites = await executeQuery(`
|
|
133
|
+
SELECT invitation_id, client_id, company_id, role
|
|
134
|
+
FROM rapport.enterprise_invitations
|
|
135
|
+
WHERE email = $1 AND status = 'pending'
|
|
136
|
+
`, [email.toLowerCase()]);
|
|
137
|
+
|
|
138
|
+
if (pendingInvites.rows.length > 0) {
|
|
139
|
+
console.log(`Found ${pendingInvites.rows.length} pending enterprise invitation(s)`);
|
|
140
|
+
|
|
141
|
+
for (const invite of pendingInvites.rows) {
|
|
142
|
+
// Create entitlement for the enterprise company
|
|
143
|
+
const isAdmin = invite.role === 'admin';
|
|
144
|
+
await executeQuery(`
|
|
145
|
+
INSERT INTO rapport.user_entitlements (
|
|
146
|
+
email_address,
|
|
147
|
+
client_id,
|
|
148
|
+
company_id,
|
|
149
|
+
admin,
|
|
150
|
+
member
|
|
151
|
+
)
|
|
152
|
+
VALUES ($1, $2, $3, $4, true)
|
|
153
|
+
ON CONFLICT (email_address, company_id) DO NOTHING
|
|
154
|
+
`, [email, invite.client_id, invite.company_id, isAdmin]);
|
|
155
|
+
|
|
156
|
+
// Mark invitation as accepted
|
|
157
|
+
await executeQuery(`
|
|
158
|
+
UPDATE rapport.enterprise_invitations
|
|
159
|
+
SET status = 'accepted', accepted_at = NOW()
|
|
160
|
+
WHERE invitation_id = $1
|
|
161
|
+
`, [invite.invitation_id]);
|
|
162
|
+
|
|
163
|
+
console.log(`Accepted enterprise invitation for company: ${invite.company_id} (role: ${invite.role})`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
await executeQuery('COMMIT');
|
|
132
168
|
|
|
133
169
|
console.log('Post-confirmation complete for:', email);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognito PreSignUp Handler
|
|
3
|
+
* Blocks bot registrations before user creation in Cognito
|
|
4
|
+
* Auto-confirms Google OAuth users and admin-invited users
|
|
5
|
+
*
|
|
6
|
+
* Triggered by: Cognito User Pool pre-signup trigger
|
|
7
|
+
*
|
|
8
|
+
* Note: Cognito trigger must be configured manually in Cognito console
|
|
9
|
+
* as SAM cannot reference external user pools
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { executeQuery } = require('./helpers');
|
|
13
|
+
|
|
14
|
+
const DISPOSABLE_DOMAINS = new Set([
|
|
15
|
+
'mailinator.com', 'guerrillamail.com', 'tempmail.com',
|
|
16
|
+
'throwaway.email', 'yopmail.com', 'sharklasers.com',
|
|
17
|
+
'guerrillamailblock.com', 'grr.la', 'maildrop.cc',
|
|
18
|
+
'dispostable.com', 'temp-mail.org', '10minutemail.com',
|
|
19
|
+
'trashmail.com', 'fakeinbox.com', 'mailnesia.com',
|
|
20
|
+
'tempinbox.com', 'mailcatch.com', 'throwam.com'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect dot-stuffed Gmail addresses used by signup bots
|
|
25
|
+
* Gmail ignores dots in the local part, so bots insert random dots
|
|
26
|
+
* to generate unique-looking addresses that all deliver to the same inbox
|
|
27
|
+
*/
|
|
28
|
+
function isDotStuffedGmail(email) {
|
|
29
|
+
const atIndex = email.lastIndexOf('@');
|
|
30
|
+
if (atIndex === -1) return false;
|
|
31
|
+
|
|
32
|
+
const localPart = email.substring(0, atIndex).toLowerCase();
|
|
33
|
+
const domain = email.substring(atIndex + 1).toLowerCase();
|
|
34
|
+
|
|
35
|
+
if (domain !== 'gmail.com' && domain !== 'googlemail.com') return false;
|
|
36
|
+
|
|
37
|
+
const dotCount = (localPart.match(/\./g) || []).length;
|
|
38
|
+
const cleanLength = localPart.replace(/\./g, '').length;
|
|
39
|
+
|
|
40
|
+
if (cleanLength === 0) return true;
|
|
41
|
+
|
|
42
|
+
// 4+ dots in a short local part is a strong bot signal
|
|
43
|
+
if (dotCount >= 4 && cleanLength < 20) return true;
|
|
44
|
+
|
|
45
|
+
// More than 30% dots relative to actual characters
|
|
46
|
+
if (dotCount > 0 && dotCount / cleanLength > 0.3) return true;
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if email uses a known disposable email domain
|
|
53
|
+
*/
|
|
54
|
+
function isDisposableEmail(email) {
|
|
55
|
+
const domain = email.toLowerCase().split('@')[1];
|
|
56
|
+
return DISPOSABLE_DOMAINS.has(domain);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handler(event) {
|
|
60
|
+
console.log('PreSignUp trigger:', JSON.stringify({
|
|
61
|
+
triggerSource: event.triggerSource,
|
|
62
|
+
userName: event.userName,
|
|
63
|
+
email: event.request?.userAttributes?.email
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Auto-confirm external provider signups (Google OAuth)
|
|
67
|
+
// Google provides verified email — no Cognito verification needed
|
|
68
|
+
if (event.triggerSource === 'PreSignUp_ExternalProvider') {
|
|
69
|
+
console.log(`[PreSignUp] Auto-confirming external provider user: ${event.request?.userAttributes?.email}`);
|
|
70
|
+
event.response.autoConfirmUser = true;
|
|
71
|
+
event.response.autoVerifyEmail = true;
|
|
72
|
+
return event;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Only validate user-initiated signups (not admin-created)
|
|
76
|
+
if (event.triggerSource !== 'PreSignUp_SignUp') {
|
|
77
|
+
return event;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const email = event.request?.userAttributes?.email;
|
|
81
|
+
if (!email) {
|
|
82
|
+
throw new Error('Email address is required for registration');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isDisposableEmail(email)) {
|
|
86
|
+
console.log(`[PreSignUp] Blocked disposable email: ${email}`);
|
|
87
|
+
throw new Error('Please use a permanent email address to register.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isDotStuffedGmail(email)) {
|
|
91
|
+
console.log(`[PreSignUp] Blocked dot-stuffed Gmail: ${email}`);
|
|
92
|
+
throw new Error('This email address format is not accepted. Please use your primary email address.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Auto-confirm users who have been invited (entitlement already exists)
|
|
96
|
+
try {
|
|
97
|
+
const result = await executeQuery(
|
|
98
|
+
'SELECT 1 FROM rapport.user_entitlements WHERE email_address = $1 LIMIT 1',
|
|
99
|
+
[email.toLowerCase()]
|
|
100
|
+
);
|
|
101
|
+
if (result.rows.length > 0) {
|
|
102
|
+
console.log(`[PreSignUp] Auto-confirming invited user: ${email}`);
|
|
103
|
+
event.response.autoConfirmUser = true;
|
|
104
|
+
event.response.autoVerifyEmail = true;
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// DB check failed — don't block signup, just skip auto-confirm
|
|
108
|
+
console.error(`[PreSignUp] Entitlement check failed for ${email}:`, err.message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return event;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { handler };
|
|
@@ -22,7 +22,7 @@ async function getUser({ requestContext }) {
|
|
|
22
22
|
return createErrorResponse(401, 'Authentication required');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Get user with their primary client subscription
|
|
25
|
+
// Get user with their primary client subscription and company
|
|
26
26
|
const query = `
|
|
27
27
|
SELECT
|
|
28
28
|
u.email_address,
|
|
@@ -35,11 +35,14 @@ async function getUser({ requestContext }) {
|
|
|
35
35
|
c.subscription_tier,
|
|
36
36
|
c.subscription_status,
|
|
37
37
|
c.subscription_ends_at,
|
|
38
|
-
c.stripe_customer_id
|
|
38
|
+
c.stripe_customer_id,
|
|
39
|
+
ue.company_id
|
|
39
40
|
FROM rapport.users u
|
|
40
41
|
LEFT JOIN rapport.clients c ON u.client_id = c.client_id
|
|
42
|
+
LEFT JOIN rapport.user_entitlements ue ON u.email_address = ue.email_address
|
|
41
43
|
WHERE u.email_address = $1
|
|
42
44
|
AND u.active = true
|
|
45
|
+
LIMIT 1
|
|
43
46
|
`;
|
|
44
47
|
|
|
45
48
|
const result = await executeQuery(query, [email]);
|
|
@@ -51,20 +54,20 @@ async function getUser({ requestContext }) {
|
|
|
51
54
|
const user = result.rows[0];
|
|
52
55
|
const tierConfig = getTierConfig(user.subscription_tier || 'free');
|
|
53
56
|
|
|
54
|
-
// Get usage counts
|
|
57
|
+
// Get usage counts scoped to user's entitled companies
|
|
55
58
|
const usageQuery = `
|
|
56
59
|
SELECT
|
|
57
60
|
(SELECT COUNT(*) FROM rapport.user_entitlements WHERE client_id = $1) as collaborators,
|
|
58
61
|
(SELECT COUNT(*) FROM rapport.projects p
|
|
59
|
-
JOIN rapport.
|
|
60
|
-
WHERE
|
|
62
|
+
JOIN rapport.user_entitlements ue ON p.company_id = ue.company_id
|
|
63
|
+
WHERE ue.email_address = $2 AND p.archived = false) as projects,
|
|
61
64
|
(SELECT COUNT(*) FROM rapport.invariants i
|
|
62
65
|
JOIN rapport.projects p ON i.project_id = p.project_id
|
|
63
|
-
JOIN rapport.
|
|
64
|
-
WHERE
|
|
66
|
+
JOIN rapport.user_entitlements ue ON p.company_id = ue.company_id
|
|
67
|
+
WHERE ue.email_address = $2) as invariants
|
|
65
68
|
`;
|
|
66
69
|
|
|
67
|
-
const usageResult = await executeQuery(usageQuery, [user.client_id]);
|
|
70
|
+
const usageResult = await executeQuery(usageQuery, [user.client_id, email]);
|
|
68
71
|
const usage = usageResult.rows[0];
|
|
69
72
|
|
|
70
73
|
return createSuccessResponse(
|
|
@@ -74,6 +77,7 @@ async function getUser({ requestContext }) {
|
|
|
74
77
|
first_name: user.first_name,
|
|
75
78
|
last_name: user.last_name,
|
|
76
79
|
client_id: user.client_id,
|
|
80
|
+
company_id: user.company_id,
|
|
77
81
|
client_name: user.client_name,
|
|
78
82
|
user_status: user.user_status,
|
|
79
83
|
member_since: user.create_date,
|