@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.
Files changed (72) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-end.js +112 -3
  4. package/hooks/session-start.js +635 -41
  5. package/hooks/subagent-start.js +150 -0
  6. package/hooks/subagent-stop.js +184 -0
  7. package/package.json +8 -7
  8. package/scripts/init-project.js +74 -33
  9. package/scripts/mcp-bridge.js +220 -0
  10. package/src/core/CorrelationAnalyzer.js +157 -0
  11. package/src/core/LLMPatternDetector.js +198 -0
  12. package/src/core/RelevanceDetector.js +123 -36
  13. package/src/core/StandardsIngestion.js +119 -18
  14. package/src/handlers/activity/activityGetMe.js +1 -1
  15. package/src/handlers/activity/activityGetTeam.js +100 -55
  16. package/src/handlers/admin/adminSetup.js +216 -0
  17. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  18. package/src/handlers/alerts/alertsGet.js +11 -11
  19. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  20. package/src/handlers/analytics/coachingGet.js +11 -11
  21. package/src/handlers/analytics/convergenceGet.js +236 -0
  22. package/src/handlers/analytics/developerScoreGet.js +41 -111
  23. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  24. package/src/handlers/company/companyUsersDelete.js +141 -0
  25. package/src/handlers/company/companyUsersGet.js +90 -0
  26. package/src/handlers/company/companyUsersPost.js +267 -0
  27. package/src/handlers/company/companyUsersPut.js +76 -0
  28. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  29. package/src/handlers/correlations/correlationsGet.js +8 -8
  30. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  31. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  32. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  33. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  34. package/src/handlers/github/githubConnectionStatus.js +1 -1
  35. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  36. package/src/handlers/github/githubPatternsReview.js +7 -36
  37. package/src/handlers/health/healthGet.js +55 -0
  38. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  39. package/src/handlers/helpers/mindmeldMcpCore.js +594 -0
  40. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  41. package/src/handlers/mcp/mcpHandler.js +569 -0
  42. package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
  43. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -0
  44. package/src/handlers/notifications/sendNotification.js +18 -18
  45. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  46. package/src/handlers/projects/projectCreate.js +124 -10
  47. package/src/handlers/projects/projectDelete.js +4 -4
  48. package/src/handlers/projects/projectGet.js +8 -8
  49. package/src/handlers/projects/projectUpdate.js +4 -4
  50. package/src/handlers/reports/aiLeverage.js +34 -30
  51. package/src/handlers/reports/engineeringInvestment.js +16 -16
  52. package/src/handlers/reports/riskForecast.js +41 -21
  53. package/src/handlers/reports/standardsRoi.js +101 -9
  54. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  55. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  56. package/src/handlers/standards/discoveriesGet.js +93 -0
  57. package/src/handlers/standards/projectStandardsGet.js +2 -2
  58. package/src/handlers/standards/projectStandardsPut.js +2 -2
  59. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  60. package/src/handlers/standards/standardsTransition.js +112 -15
  61. package/src/handlers/stripe/billingPortalPost.js +1 -1
  62. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  63. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  64. package/src/handlers/stripe/webhookPost.js +42 -14
  65. package/src/handlers/user/apiTokenCreate.js +71 -0
  66. package/src/handlers/user/apiTokenList.js +64 -0
  67. package/src/handlers/user/userSplashGet.js +90 -73
  68. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  69. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  70. package/src/handlers/users/userGet.js +15 -11
  71. package/src/handlers/webhooks/githubWebhook.js +117 -125
  72. 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
- // 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 {
@@ -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);