@equilateral_ai/mindmeld 3.3.1 → 3.5.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-end.js +112 -3
- package/hooks/session-start.js +635 -41
- 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/mindmeldMcpCore.js +594 -0
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -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 +15 -11
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
|
@@ -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
|
-
//
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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'}/
|
|
122
|
-
cancel_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/
|
|
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.
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
//
|
|
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 {
|
|
@@ -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);
|