@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.
Files changed (69) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-start.js +636 -42
  4. package/hooks/subagent-start.js +150 -0
  5. package/hooks/subagent-stop.js +184 -0
  6. package/package.json +8 -7
  7. package/scripts/init-project.js +74 -33
  8. package/scripts/mcp-bridge.js +220 -0
  9. package/src/core/CorrelationAnalyzer.js +157 -0
  10. package/src/core/LLMPatternDetector.js +198 -0
  11. package/src/core/RelevanceDetector.js +123 -36
  12. package/src/core/StandardsIngestion.js +119 -18
  13. package/src/handlers/activity/activityGetMe.js +1 -1
  14. package/src/handlers/activity/activityGetTeam.js +100 -55
  15. package/src/handlers/admin/adminSetup.js +216 -0
  16. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  17. package/src/handlers/alerts/alertsGet.js +11 -11
  18. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  19. package/src/handlers/analytics/coachingGet.js +11 -11
  20. package/src/handlers/analytics/convergenceGet.js +236 -0
  21. package/src/handlers/analytics/developerScoreGet.js +41 -111
  22. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  23. package/src/handlers/company/companyUsersDelete.js +141 -0
  24. package/src/handlers/company/companyUsersGet.js +90 -0
  25. package/src/handlers/company/companyUsersPost.js +267 -0
  26. package/src/handlers/company/companyUsersPut.js +76 -0
  27. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  28. package/src/handlers/correlations/correlationsGet.js +8 -8
  29. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  30. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  31. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  32. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  33. package/src/handlers/github/githubConnectionStatus.js +1 -1
  34. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  35. package/src/handlers/github/githubPatternsReview.js +7 -36
  36. package/src/handlers/health/healthGet.js +55 -0
  37. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  38. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  39. package/src/handlers/mcp/mcpHandler.js +569 -0
  40. package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
  41. package/src/handlers/notifications/sendNotification.js +18 -18
  42. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  43. package/src/handlers/projects/projectCreate.js +124 -10
  44. package/src/handlers/projects/projectDelete.js +4 -4
  45. package/src/handlers/projects/projectGet.js +8 -8
  46. package/src/handlers/projects/projectUpdate.js +4 -4
  47. package/src/handlers/reports/aiLeverage.js +34 -30
  48. package/src/handlers/reports/engineeringInvestment.js +16 -16
  49. package/src/handlers/reports/riskForecast.js +41 -21
  50. package/src/handlers/reports/standardsRoi.js +101 -9
  51. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  52. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  53. package/src/handlers/standards/discoveriesGet.js +93 -0
  54. package/src/handlers/standards/projectStandardsGet.js +2 -2
  55. package/src/handlers/standards/projectStandardsPut.js +2 -2
  56. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  57. package/src/handlers/standards/standardsTransition.js +112 -15
  58. package/src/handlers/stripe/billingPortalPost.js +1 -1
  59. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  60. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  61. package/src/handlers/stripe/webhookPost.js +42 -14
  62. package/src/handlers/user/apiTokenCreate.js +71 -0
  63. package/src/handlers/user/apiTokenList.js +64 -0
  64. package/src/handlers/user/userSplashGet.js +90 -73
  65. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  66. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  67. package/src/handlers/users/userGet.js +12 -8
  68. package/src/handlers/webhooks/githubWebhook.js +117 -125
  69. package/src/index.js +46 -51
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Discoveries Get Handler
3
+ * Returns discovery review queue for a project
4
+ *
5
+ * GET /api/standards/discoveries?project_id=xxx&status=proposed
6
+ * Auth: Cognito JWT required
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
10
+
11
+ async function getDiscoveries({ queryStringParameters, requestContext }) {
12
+ try {
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
+ const projectId = (queryStringParameters || {}).project_id;
20
+ const status = (queryStringParameters || {}).status;
21
+
22
+ if (!projectId) {
23
+ return createErrorResponse(400, 'project_id is required');
24
+ }
25
+
26
+ // Verify user has access to project
27
+ const accessResult = await executeQuery(`
28
+ SELECT role FROM rapport.project_collaborators
29
+ WHERE project_id = $1 AND email_address = $2
30
+ `, [projectId, email]);
31
+
32
+ if (accessResult.rowCount === 0) {
33
+ return createErrorResponse(403, 'Access denied to project');
34
+ }
35
+
36
+ // Build query with optional status filter
37
+ let query = `
38
+ SELECT
39
+ discovery_id,
40
+ project_id,
41
+ pattern_name,
42
+ pattern_description,
43
+ confidence,
44
+ discovery_type,
45
+ status,
46
+ evidence,
47
+ reason,
48
+ reason_code,
49
+ created_at,
50
+ updated_at
51
+ FROM rapport.onboarding_discoveries
52
+ WHERE project_id = $1
53
+ `;
54
+ const params = [projectId];
55
+
56
+ if (status) {
57
+ query += ` AND status = $2`;
58
+ params.push(status);
59
+ }
60
+
61
+ query += ` ORDER BY confidence DESC, created_at DESC`;
62
+
63
+ const result = await executeQuery(query, params);
64
+
65
+ // Map DB columns to frontend Discovery interface
66
+ const records = result.rows.map(row => ({
67
+ id: row.discovery_id,
68
+ project_id: row.project_id,
69
+ name: row.pattern_name,
70
+ description: row.pattern_description || '',
71
+ confidence: parseFloat(row.confidence) || 0,
72
+ source: 'github',
73
+ category: row.discovery_type || 'Architecture',
74
+ status: row.status || 'proposed',
75
+ rule_text: null,
76
+ action_type: null,
77
+ evidence: row.evidence || null,
78
+ created_at: row.created_at,
79
+ updated_at: row.updated_at
80
+ }));
81
+
82
+ return createSuccessResponse({
83
+ records,
84
+ total: result.rowCount
85
+ }, `Found ${result.rowCount} discoveries`);
86
+
87
+ } catch (error) {
88
+ console.error('Discoveries Get Error:', error);
89
+ return createErrorResponse(500, 'Failed to load discoveries');
90
+ }
91
+ }
92
+
93
+ exports.handler = wrapHandler(getDiscoveries);
@@ -8,7 +8,7 @@
8
8
 
9
9
  const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
10
10
 
11
- async function getProjectStandards({ queryStringParameters, requestContext }) {
11
+ async function getProjectStandards({ queryStringParameters, pathParameters, requestContext }) {
12
12
  try {
13
13
  const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
14
14
 
@@ -16,7 +16,7 @@ async function getProjectStandards({ queryStringParameters, requestContext }) {
16
16
  return createErrorResponse(401, 'Authentication required');
17
17
  }
18
18
 
19
- const projectId = (queryStringParameters || {}).project_id;
19
+ const projectId = pathParameters?.projectId || (queryStringParameters || {}).project_id;
20
20
 
21
21
  if (!projectId) {
22
22
  return createErrorResponse(400, 'projectId is required');
@@ -9,7 +9,7 @@
9
9
 
10
10
  const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
11
11
 
12
- async function updateProjectStandards({ body, requestContext }) {
12
+ async function updateProjectStandards({ body, pathParameters, requestContext }) {
13
13
  try {
14
14
  const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
15
15
 
@@ -17,7 +17,7 @@ async function updateProjectStandards({ body, requestContext }) {
17
17
  return createErrorResponse(401, 'Authentication required');
18
18
  }
19
19
 
20
- const projectId = (body || {}).project_id;
20
+ const projectId = pathParameters?.projectId || (body || {}).project_id;
21
21
 
22
22
  if (!projectId) {
23
23
  return createErrorResponse(400, 'projectId is required');
@@ -18,15 +18,16 @@ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse }
18
18
  */
19
19
  const CATEGORY_WEIGHTS = {
20
20
  'serverless-saas-aws': 1.0,
21
- 'multi-agent-orchestration': 1.0,
22
21
  'frontend-development': 1.0,
23
22
  'database': 0.9,
24
- 'compliance-security': 0.8,
25
- 'cost-optimization': 0.7,
26
- 'real-time-systems': 0.8,
27
- 'testing': 0.6,
28
23
  'backend': 0.9,
29
- 'well-architected': 0.7
24
+ 'compliance-security': 0.9,
25
+ 'deployment': 0.8,
26
+ 'testing': 0.7,
27
+ 'real-time-systems': 0.7,
28
+ 'well-architected': 0.7,
29
+ 'cost-optimization': 0.7,
30
+ 'multi-agent-orchestration': 0.1, // Infrastructure config, rarely needed in coding sessions
30
31
  };
31
32
 
32
33
  /**
@@ -54,6 +55,9 @@ function mapCharacteristicsToCategories(characteristics) {
54
55
  if (characteristics.hasTests) {
55
56
  categories.add('testing');
56
57
  }
58
+ if (characteristics.hasSAM || characteristics.hasLambda || characteristics.hasAPI) {
59
+ categories.add('deployment');
60
+ }
57
61
 
58
62
  // Always relevant
59
63
  categories.add('compliance-security');
@@ -65,7 +69,7 @@ function mapCharacteristicsToCategories(characteristics) {
65
69
  /**
66
70
  * Rank standards by relevance score
67
71
  */
68
- function rankStandards(standards) {
72
+ function rankStandards(standards, recentCategories) {
69
73
  return standards.map(standard => {
70
74
  let score = 0;
71
75
 
@@ -98,6 +102,23 @@ function rankStandards(standards) {
98
102
  if (apCount > 0) score += 5;
99
103
  }
100
104
 
105
+ // Workflow bonus — workflows are high-value procedural knowledge
106
+ const isWorkflow = (standard.rule && standard.rule.startsWith('WORKFLOW:'))
107
+ || (Array.isArray(standard.keywords) && standard.keywords.includes('workflow'));
108
+ if (isWorkflow) score += 10;
109
+
110
+ // Recency bonus — boost categories the user has been working in recently
111
+ // Scaled by category weight to prevent feedback loops (low-weight categories
112
+ // that got injected shouldn't bootstrap themselves back into the top 10)
113
+ if (recentCategories && recentCategories[standard.category]) {
114
+ const usageCount = recentCategories[standard.category];
115
+ let rawBonus;
116
+ if (usageCount >= 8) rawBonus = 25;
117
+ else if (usageCount >= 4) rawBonus = 18;
118
+ else rawBonus = 10;
119
+ score += rawBonus * categoryWeight;
120
+ }
121
+
101
122
  return {
102
123
  ...standard,
103
124
  relevance_score: Math.round(score * 10) / 10
@@ -141,6 +162,25 @@ async function getRelevantStandards({ body, requestContext }) {
141
162
  return createErrorResponse(401, 'Authentication required');
142
163
  }
143
164
 
165
+ // Verify active subscription — no free tier access
166
+ const subResult = await executeQuery(`
167
+ SELECT c.subscription_tier, c.subscription_status
168
+ FROM rapport.users u
169
+ JOIN rapport.clients c ON u.client_id = c.client_id
170
+ WHERE u.email_address = $1
171
+ LIMIT 1
172
+ `, [email]);
173
+
174
+ if (subResult.rows.length > 0) {
175
+ const { subscription_tier, subscription_status } = subResult.rows[0];
176
+ if (!subscription_tier || subscription_tier === 'free') {
177
+ return createErrorResponse(403, 'Active MindMeld subscription required. Subscribe at app.mindmeld.dev');
178
+ }
179
+ if (subscription_status === 'canceled') {
180
+ return createErrorResponse(403, 'Subscription canceled. Resubscribe at app.mindmeld.dev');
181
+ }
182
+ }
183
+
144
184
  // Parse request body
145
185
  let requestBody;
146
186
  try {
@@ -166,13 +206,43 @@ async function getRelevantStandards({ body, requestContext }) {
166
206
  }, 'No relevant categories detected');
167
207
  }
168
208
 
169
- // Query standards matching categories
209
+ // Query recent session categories first (fast), then merge into categories for main query
210
+ const recentCategories = {};
211
+ try {
212
+ const recencyResult = await executeQuery(`
213
+ SELECT sp.category, COUNT(*) as usage_count
214
+ FROM rapport.session_standards ss
215
+ JOIN rapport.sessions s ON s.session_id = ss.session_id
216
+ JOIN rapport.standards_patterns sp ON sp.pattern_id = ss.standard_id
217
+ WHERE s.email_address = $1
218
+ AND s.started_at >= NOW() - INTERVAL '7 days'
219
+ GROUP BY sp.category
220
+ ORDER BY usage_count DESC
221
+ LIMIT 5
222
+ `, [email]);
223
+ for (const row of recencyResult.rows) {
224
+ recentCategories[row.category] = parseInt(row.usage_count, 10);
225
+ }
226
+ } catch (err) {
227
+ console.error('[standardsRelevant] Recency query failed:', err.message);
228
+ }
229
+
230
+ // Merge recency categories into query — recent activity should always be represented
231
+ for (const category of Object.keys(recentCategories)) {
232
+ if (!categories.includes(category)) {
233
+ categories.push(category);
234
+ }
235
+ }
236
+
237
+ // Query standards from all relevant categories (static + recency)
170
238
  const result = await executeQuery(`
171
239
  SELECT
172
240
  pattern_id,
173
241
  element,
242
+ title,
174
243
  rule,
175
244
  category,
245
+ keywords,
176
246
  correlation,
177
247
  maturity,
178
248
  applicable_files,
@@ -192,16 +262,41 @@ async function getRelevantStandards({ body, requestContext }) {
192
262
  correlation DESC
193
263
  `, [categories]);
194
264
 
195
- // Rank by relevance
196
- let ranked = rankStandards(result.rows);
265
+ // Rank by relevance with recency boost
266
+ let ranked = rankStandards(result.rows, recentCategories);
267
+
268
+ // Deduplicate by element name (same rule can be ingested with different path prefixes)
269
+ const seenElements = new Set();
270
+ ranked = ranked.filter(standard => {
271
+ const key = standard.element;
272
+ if (seenElements.has(key)) return false;
273
+ seenElements.add(key);
274
+ return true;
275
+ });
197
276
 
198
277
  // Apply preferences if provided
199
278
  if (preferences) {
200
279
  ranked = applyPreferences(ranked, preferences);
201
280
  }
202
281
 
203
- // Return top 10
204
- const top = ranked.slice(0, 10);
282
+ // Return top 10 with diversity caps:
283
+ // - Max 2 per category (prevents single-category saturation)
284
+ // - Max 1 per standard title (prevents same-file rule pairs wasting slots)
285
+ const MAX_PER_CATEGORY = 2;
286
+ const MAX_PER_TITLE = 1;
287
+ const top = [];
288
+ const categoryCounts = {};
289
+ const titleCounts = {};
290
+ for (const standard of ranked) {
291
+ const cat = standard.category;
292
+ const title = standard.title || standard.element;
293
+ categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
294
+ titleCounts[title] = (titleCounts[title] || 0) + 1;
295
+ if (categoryCounts[cat] <= MAX_PER_CATEGORY && titleCounts[title] <= MAX_PER_TITLE) {
296
+ top.push(standard);
297
+ if (top.length >= 10) break;
298
+ }
299
+ }
205
300
 
206
301
  return createSuccessResponse({
207
302
  standards: top,
@@ -1,17 +1,95 @@
1
1
  /**
2
2
  * Standards Transition Handler
3
- * Executes lifecycle state transitions on standards
3
+ * Executes lifecycle state transitions on standards and discoveries
4
4
  *
5
5
  * POST /api/standards/transition
6
- * Body: { standard_id, action: 'approve'|'disable'|'deprecate'|'delete'|'enable'|'cancel_deprecation', reason? }
6
+ * Body: { standard_id, action: 'approve'|'reject'|'observe'|'disable'|'deprecate'|'delete'|'enable'|'cancel_deprecation', reason?, reason_code? }
7
7
  * Auth: Cognito JWT required
8
+ *
9
+ * For discovery IDs (from onboarding_discoveries table):
10
+ * approve → creates pattern + marks discovery approved
11
+ * reject → marks discovery rejected
12
+ * observe → marks discovery as observing (deferred review)
8
13
  */
9
14
 
10
- const { wrapHandler, createSuccessResponse, createErrorResponse } = require('./helpers');
15
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
11
16
  const { StandardLifecycle } = require('./core/StandardLifecycle');
12
17
 
13
18
  const lifecycle = new StandardLifecycle();
14
19
 
20
+ const DISCOVERY_ACTIONS = ['approve', 'reject', 'observe'];
21
+
22
+ /**
23
+ * Handle transitions for onboarding discoveries
24
+ */
25
+ async function transitionDiscovery(discoveryId, action, email, reason, reasonCode) {
26
+ const discovery = await executeQuery(`
27
+ SELECT discovery_id, project_id, discovery_type, pattern_name, pattern_description, confidence, evidence, status
28
+ FROM rapport.onboarding_discoveries
29
+ WHERE discovery_id = $1
30
+ `, [discoveryId]);
31
+
32
+ if (discovery.rowCount === 0) {
33
+ return null; // Not a discovery either
34
+ }
35
+
36
+ const row = discovery.rows[0];
37
+ const oldStatus = row.status;
38
+
39
+ if (action === 'approve') {
40
+ // Create pattern from discovery
41
+ const patternId = `pat_${row.project_id}_${Date.now()}`;
42
+ await executeQuery(`
43
+ INSERT INTO rapport.patterns (
44
+ pattern_id, project_id, intent, constraints, outcome_criteria,
45
+ maturity, discovered_by, pattern_data
46
+ ) VALUES ($1, $2, $3, $4, $5, 'provisional', $6, $7)
47
+ `, [
48
+ patternId,
49
+ row.project_id,
50
+ row.pattern_name,
51
+ JSON.stringify([row.pattern_description || '']),
52
+ JSON.stringify([`Discovered via onboarding: ${row.discovery_type}`]),
53
+ email,
54
+ JSON.stringify({
55
+ source: 'discovery_review',
56
+ discovery_type: row.discovery_type,
57
+ evidence: row.evidence
58
+ })
59
+ ]);
60
+
61
+ await executeQuery(`
62
+ UPDATE rapport.onboarding_discoveries
63
+ SET status = 'approved', reviewed_at = NOW(), updated_at = NOW(), pattern_id = $1
64
+ WHERE discovery_id = $2
65
+ `, [patternId, discoveryId]);
66
+
67
+ return { old_state: oldStatus, new_state: 'approved', pattern_id: patternId };
68
+ }
69
+
70
+ if (action === 'reject') {
71
+ await executeQuery(`
72
+ UPDATE rapport.onboarding_discoveries
73
+ SET status = 'rejected', reviewed_at = NOW(), updated_at = NOW(), reason = $1, reason_code = $2
74
+ WHERE discovery_id = $3
75
+ `, [reason || null, reasonCode || null, discoveryId]);
76
+
77
+ return { old_state: oldStatus, new_state: 'rejected' };
78
+ }
79
+
80
+ if (action === 'observe') {
81
+ await executeQuery(`
82
+ UPDATE rapport.onboarding_discoveries
83
+ SET status = 'observing', updated_at = NOW()
84
+ WHERE discovery_id = $1
85
+ `, [discoveryId]);
86
+
87
+ return { old_state: oldStatus, new_state: 'observing' };
88
+ }
89
+
90
+ return null;
91
+ }
92
+
15
93
  async function transitionStandard({ body, requestContext }) {
16
94
  try {
17
95
  const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
@@ -20,7 +98,7 @@ async function transitionStandard({ body, requestContext }) {
20
98
  return createErrorResponse(401, 'Authentication required');
21
99
  }
22
100
 
23
- const { standard_id: id, action, reason } = body || {};
101
+ const { standard_id: id, action, reason, reason_code: reasonCode } = body || {};
24
102
 
25
103
  if (!id) {
26
104
  return createErrorResponse(400, 'standard_id is required');
@@ -28,25 +106,44 @@ async function transitionStandard({ body, requestContext }) {
28
106
 
29
107
  if (!action) {
30
108
  return createErrorResponse(400, 'action is required', {
31
- valid_actions: ['propose', 'approve', 'reject', 'disable', 'deprecate', 'delete', 'enable', 'cancel_deprecation']
109
+ valid_actions: ['propose', 'approve', 'reject', 'observe', 'disable', 'deprecate', 'delete', 'enable', 'cancel_deprecation']
32
110
  });
33
111
  }
34
112
 
35
- // Execute the transition
36
- const result = await lifecycle.transition(id, action, email, reason);
113
+ // Try standard lifecycle transition first
114
+ try {
115
+ const result = await lifecycle.transition(id, action, email, reason);
116
+
117
+ return createSuccessResponse({
118
+ standard_id: result.standard_id,
119
+ old_state: result.old_state,
120
+ new_state: result.new_state,
121
+ action: result.action,
122
+ audit_entry: result.audit_entry
123
+ }, `Standard transitioned from '${result.old_state}' to '${result.new_state}'`);
124
+ } catch (lifecycleError) {
125
+ // If pattern not found and action is valid for discoveries, try discovery transition
126
+ if (lifecycleError.message.includes('not found') && DISCOVERY_ACTIONS.includes(action)) {
127
+ const discoveryResult = await transitionDiscovery(id, action, email, reason, reasonCode);
37
128
 
38
- return createSuccessResponse({
39
- standard_id: result.standard_id,
40
- old_state: result.old_state,
41
- new_state: result.new_state,
42
- action: result.action,
43
- audit_entry: result.audit_entry
44
- }, `Standard transitioned from '${result.old_state}' to '${result.new_state}'`);
129
+ if (discoveryResult) {
130
+ return createSuccessResponse({
131
+ standard_id: id,
132
+ old_state: discoveryResult.old_state,
133
+ new_state: discoveryResult.new_state,
134
+ action,
135
+ pattern_id: discoveryResult.pattern_id || null
136
+ }, `Discovery transitioned from '${discoveryResult.old_state}' to '${discoveryResult.new_state}'`);
137
+ }
138
+ }
139
+
140
+ // Re-throw if not handled
141
+ throw lifecycleError;
142
+ }
45
143
 
46
144
  } catch (error) {
47
145
  console.error('Standards Transition Error:', error);
48
146
 
49
- // Return user-friendly messages for known validation errors
50
147
  if (error.message.includes('Invalid action')) {
51
148
  return createErrorResponse(400, error.message);
52
149
  }
@@ -67,7 +67,7 @@ async function createBillingPortal({ body: requestBody = {}, requestContext }) {
67
67
  // Create billing portal session
68
68
  const session = await stripe.billingPortal.sessions.create({
69
69
  customer: client.stripe_customer_id,
70
- return_url: returnUrl || `${process.env.APP_URL || 'https://mindmeld.dev'}/settings/billing`
70
+ return_url: returnUrl || `${process.env.APP_URL || 'https://app.mindmeld.dev'}/settings/billing`
71
71
  });
72
72
 
73
73
  return createSuccessResponse(
@@ -159,8 +159,8 @@ async function createEnterpriseCheckout({ body: requestBody = {}, requestContext
159
159
  payment_method_types: ['card'],
160
160
  customer_email: email,
161
161
  line_items: lineItems,
162
- success_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/enterprise/success?session_id={CHECKOUT_SESSION_ID}`,
163
- cancel_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/enterprise/cancel`,
162
+ success_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/enterprise/success?session_id={CHECKOUT_SESSION_ID}`,
163
+ cancel_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/enterprise/cancel`,
164
164
  metadata: {
165
165
  client_id: user.client_id,
166
166
  user_email: email,
@@ -118,8 +118,8 @@ async function createSubscription({ body: requestBody = {}, requestContext }) {
118
118
  price: priceId,
119
119
  quantity: tierConfig.perUser ? userCount : 1
120
120
  }],
121
- success_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
122
- cancel_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/subscription/cancel`,
121
+ success_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/dashboard?checkout=success`,
122
+ cancel_url: `${process.env.APP_URL || 'https://app.mindmeld.dev'}/signup`,
123
123
  metadata: {
124
124
  client_id: user.client_id,
125
125
  user_email: email,
@@ -72,18 +72,22 @@ async function handleCheckoutCompleted(session) {
72
72
  const { client_id, tier, user_email, enterprise_package, seat_count, addons } = session.metadata || {};
73
73
 
74
74
  if (!client_id) {
75
- console.warn('Missing client_id in session metadata');
76
- return;
75
+ console.error('[webhookPost] CRITICAL: Missing client_id in checkout session metadata — subscription will not activate');
76
+ throw new Error('Missing client_id in checkout session metadata');
77
77
  }
78
78
 
79
79
  // Parse enterprise metadata
80
80
  const isEnterprise = tier === 'enterprise';
81
81
  const seatCountNum = seat_count ? parseInt(seat_count, 10) : 1;
82
- const addonsList = addons ? JSON.parse(addons) : [];
82
+ let addonsList = [];
83
+ if (addons) {
84
+ try { addonsList = JSON.parse(addons); }
85
+ catch { console.warn('Failed to parse addons metadata:', addons); }
86
+ }
83
87
 
84
88
  // Update client with subscription
85
89
  if (isEnterprise) {
86
- await executeQuery(`
90
+ const updateResult = await executeQuery(`
87
91
  UPDATE rapport.clients
88
92
  SET stripe_customer_id = $2,
89
93
  stripe_subscription_id = $3,
@@ -96,6 +100,11 @@ async function handleCheckoutCompleted(session) {
96
100
  WHERE client_id = $1
97
101
  `, [client_id, session.customer, session.subscription, enterprise_package, seatCountNum, JSON.stringify(addonsList)]);
98
102
 
103
+ if (updateResult.rowCount === 0) {
104
+ console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
105
+ throw new Error(`Client ${client_id} not found — subscription activation failed`);
106
+ }
107
+
99
108
  // Create addon entitlements if any
100
109
  if (addonsList.length > 0) {
101
110
  for (const addonId of addonsList) {
@@ -112,7 +121,7 @@ async function handleCheckoutCompleted(session) {
112
121
 
113
122
  console.log('Enterprise checkout completed:', { client_id, enterprise_package, seat_count: seatCountNum, addons: addonsList });
114
123
  } else {
115
- await executeQuery(`
124
+ const updateResult = await executeQuery(`
116
125
  UPDATE rapport.clients
117
126
  SET stripe_customer_id = $2,
118
127
  stripe_subscription_id = $3,
@@ -122,6 +131,11 @@ async function handleCheckoutCompleted(session) {
122
131
  WHERE client_id = $1
123
132
  `, [client_id, session.customer, session.subscription, tier || 'team']);
124
133
 
134
+ if (updateResult.rowCount === 0) {
135
+ console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
136
+ throw new Error(`Client ${client_id} not found — subscription activation failed`);
137
+ }
138
+
125
139
  console.log('Checkout completed:', { client_id, tier, customer: session.customer });
126
140
  }
127
141
 
@@ -160,7 +174,11 @@ async function handleSubscriptionUpdated(subscription) {
160
174
  // Parse enterprise metadata
161
175
  const isEnterprise = tier === 'enterprise';
162
176
  const seatCountNum = seat_count ? parseInt(seat_count, 10) : null;
163
- const addonsList = addons ? JSON.parse(addons) : null;
177
+ let addonsList = null;
178
+ if (addons) {
179
+ try { addonsList = JSON.parse(addons); }
180
+ catch { console.warn('Failed to parse addons metadata in subscription update:', addons); }
181
+ }
164
182
 
165
183
  // Build update query based on what changed
166
184
  if (isEnterprise) {
@@ -424,15 +442,25 @@ async function handler(event, context) {
424
442
  console.error('Error processing webhook:', processError);
425
443
 
426
444
  // Record error
427
- await executeQuery(`
428
- UPDATE rapport.stripe_webhook_events
429
- SET handled = false,
430
- error = $2,
431
- processed_at = CURRENT_TIMESTAMP
432
- WHERE event_id = $1
433
- `, [stripeEvent.id, processError.message]);
445
+ try {
446
+ await executeQuery(`
447
+ UPDATE rapport.stripe_webhook_events
448
+ SET handled = false,
449
+ error = $2,
450
+ processed_at = CURRENT_TIMESTAMP
451
+ WHERE event_id = $1
452
+ `, [stripeEvent.id, processError.message]);
453
+ } catch (recordErr) {
454
+ console.error('Failed to record webhook error:', recordErr.message);
455
+ }
434
456
 
435
- // Still return 200 to prevent Stripe retries for business logic errors
457
+ // Return 500 for critical failures so Stripe retries
458
+ // (missing client, DB connection errors, subscription not activated)
459
+ return {
460
+ statusCode: 500,
461
+ headers: { 'Content-Type': 'application/json' },
462
+ body: JSON.stringify({ error: processError.message })
463
+ };
436
464
  }
437
465
 
438
466
  return {