Initial commit

This commit is contained in:
Michilis
2025-12-19 23:56:07 -03:00
commit 23f716255e
48 changed files with 14834 additions and 0 deletions

909
src/routes/mints.js Normal file
View File

@@ -0,0 +1,909 @@
/**
* Mint Routes
*
* Core mint discovery and management endpoints.
*
* IMPORTANT: Route order matters!
* Static routes (like /by-url/*) must come BEFORE parameterized routes (/:mint_id/*)
*/
import { Router } from 'express';
import {
getMintById,
getMintByUrl,
getMints,
getMintUrls,
submitMint,
countMintsByStatus,
resolveMint
} from '../services/MintService.js';
import { getMetadataSnapshot, getMetadataHistory, deriveFeatures } from '../services/MetadataService.js';
import { getUptime, getUptimeTimeseries, getUptimeSummary, getLatencyTimeseries } from '../services/UptimeService.js';
import { getIncidents, countIncidents, getLatestProbe } from '../services/ProbeService.js';
import { getTrustScore, getTrustScoreHistory, getTrustComparison, getTrustScoreHistoryDetailed } from '../services/TrustService.js';
import { getReviews, getReviewSummary } from '../services/ReviewService.js';
import { getPageviewStats, recordPageview, getTrendingMints, getViewsTimeseries } from '../services/PageviewService.js';
import { daysAgo } from '../utils/time.js';
import { getAllReviews, countReviews } from '../services/ReviewService.js';
import {
getMintActivity,
getRecentMints,
getUpdatedMints,
getPopularMints,
getMintStats,
getMintAvailability,
getMintCard
} from '../services/AnalyticsService.js';
const router = Router();
// ==========================================
// HELPER: Resolve mint by URL query param
// ==========================================
function getMintFromUrl(req, res) {
const { url } = req.query;
if (!url) {
res.status(400).json({ error: 'URL parameter required' });
return null;
}
const mint = getMintByUrl(url);
if (!mint) {
res.status(404).json({ error: 'Mint not found' });
return null;
}
return mint;
}
// ==========================================
// HELPER: Resolve mint middleware for ID routes
// ==========================================
function resolveMintMiddleware(req, res, next) {
const { mint_id } = req.params;
if (!mint_id) {
return res.status(400).json({ error: 'Mint ID required' });
}
const mint = resolveMint(mint_id);
if (!mint) {
return res.status(404).json({ error: 'Mint not found' });
}
req.mint = mint;
next();
}
// ==========================================
// LIST & SUBMIT (no param conflicts)
// ==========================================
/**
* GET /v1/mints
* List all mints with optional filters
*/
router.get('/', (req, res) => {
try {
const { status, limit = 100, offset = 0, sort_by, sort_order } = req.query;
const mints = getMints({
status,
limit: Math.min(parseInt(limit) || 100, 500),
offset: parseInt(offset) || 0,
sortBy: sort_by,
sortOrder: sort_order
});
// Enrich with uptime summary
const enrichedMints = mints.map(mint => ({
...mint,
...getUptimeSummary(mint.mint_id),
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
incidents_30d: countIncidents(mint.mint_id, daysAgo(30))
}));
res.json({
mints: enrichedMints,
total: enrichedMints.length,
limit: Math.min(parseInt(limit) || 100, 500),
offset: parseInt(offset) || 0
});
} catch (error) {
console.error('[API] Error listing mints:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* POST /v1/mints/submit
*/
router.post('/submit', (req, res) => {
try {
const { mint_url } = req.body;
if (!mint_url) {
return res.status(400).json({ error: 'mint_url required' });
}
const result = submitMint(mint_url);
res.status(result.success ? 201 : 400).json(result);
} catch (error) {
console.error('[API] Error submitting mint:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// ==========================================
// HOMEPAGE & DISCOVERY ENDPOINTS
// ==========================================
/**
* GET /v1/mints/activity
* Get mint ecosystem activity overview
*/
router.get('/activity', (req, res) => {
try {
const activity = getMintActivity();
res.json(activity);
} catch (error) {
console.error('[API] Error getting mint activity:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/recent
* Get recently added mints
*/
router.get('/recent', (req, res) => {
try {
const { window = '7d', limit = 20 } = req.query;
const validWindows = ['24h', '7d', '30d'];
const timeWindow = validWindows.includes(window) ? window : '7d';
const mints = getRecentMints(timeWindow, Math.min(parseInt(limit) || 20, 100));
res.json({
window: timeWindow,
mints
});
} catch (error) {
console.error('[API] Error getting recent mints:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/updated
* Get recently updated mints
*/
router.get('/updated', (req, res) => {
try {
const { limit = 20 } = req.query;
const mints = getUpdatedMints(Math.min(parseInt(limit) || 20, 100));
res.json({ mints });
} catch (error) {
console.error('[API] Error getting updated mints:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/popular
* Get popular mints by views
*/
router.get('/popular', async(req, res) => {
try {
const { window = '7d', limit = 20 } = req.query;
const validWindows = ['24h', '7d', '30d'];
const timeWindow = validWindows.includes(window) ? window : '7d';
const mints = await getPopularMints(timeWindow, Math.min(parseInt(limit) || 20, 100));
res.json({
window: timeWindow,
mints
});
} catch (error) {
console.error('[API] Error getting popular mints:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/trending
* Get trending mints by view velocity
*/
router.get('/trending', async(req, res) => {
try {
const { window = '7d', limit = 10 } = req.query;
// Validate window
const validWindows = ['24h', '7d'];
const timeWindow = validWindows.includes(window) ? window : '7d';
const trending = await getTrendingMints(
Math.min(parseInt(limit) || 10, 50),
timeWindow
);
res.json({
window: timeWindow,
mints: trending.map(m => ({
mint_id: m.mint_id,
canonical_url: m.canonical_url,
name: m.name,
icon_url: m.icon_url,
status: m.status,
trust_score: m.trust_score,
trust_level: m.trust_level,
view_count: m.view_count,
view_velocity: m.view_velocity,
unique_sessions: m.unique_sessions
}))
});
} catch (error) {
console.error('[API] Error getting trending mints:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// ==========================================
// BY-URL ROUTES (must come BEFORE :mint_id routes)
// ==========================================
/**
* GET /v1/mints/by-url
*/
router.get('/by-url', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const uptime = getUptimeSummary(mint.mint_id);
res.json({
mint_id: mint.mint_id,
canonical_url: mint.canonical_url,
urls: getMintUrls(mint.mint_id).map(u => u.url),
name: mint.name,
icon_url: mint.icon_url,
status: mint.status,
offline_since: mint.offline_since,
last_success_at: mint.last_success_at,
last_failure_at: mint.last_failure_at,
...uptime,
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
incidents_30d: countIncidents(mint.mint_id, daysAgo(30)),
trust_score: mint.trust_score,
trust_level: mint.trust_level
});
} catch (error) {
console.error('[API] Error getting mint by URL:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/urls
*/
router.get('/by-url/urls', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const urls = getMintUrls(mint.mint_id);
res.json({
canonical_url: mint.canonical_url,
urls
});
} catch (error) {
console.error('[API] Error getting mint URLs:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/metadata
*/
router.get('/by-url/metadata', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const metadata = getMetadataSnapshot(mint.mint_id);
if (!metadata) {
return res.json({ error: 'No metadata available', mint_id: mint.mint_id });
}
res.json({
name: metadata.name,
pubkey: metadata.pubkey,
version: metadata.version,
description: metadata.description,
description_long: metadata.description_long,
contact: metadata.contact,
motd: metadata.motd,
icon_url: metadata.icon_url,
urls: metadata.urls,
tos_url: metadata.tos_url,
nuts: metadata.nuts,
server_time: metadata.server_time,
last_fetched_at: metadata.last_fetched_at
});
} catch (error) {
console.error('[API] Error getting metadata:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/metadata/history
*/
router.get('/by-url/metadata/history', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const { limit = 50 } = req.query;
const history = getMetadataHistory(mint.mint_id, { limit: parseInt(limit) });
res.json({ history });
} catch (error) {
console.error('[API] Error getting metadata history:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/status
*/
router.get('/by-url/status', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const latestProbe = getLatestProbe(mint.mint_id);
res.json({
status: mint.status,
offline_since: mint.offline_since,
last_checked_at: latestProbe ? latestProbe.probed_at : null,
current_rtt_ms: latestProbe ? latestProbe.rtt_ms : null
});
} catch (error) {
console.error('[API] Error getting status:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/uptime
*/
router.get('/by-url/uptime', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const { window = '24h' } = req.query;
const uptime = getUptime(mint.mint_id, window);
if (!uptime) {
return res.json({ error: 'No uptime data available' });
}
res.json(uptime);
} catch (error) {
console.error('[API] Error getting uptime:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/uptime/timeseries
*/
router.get('/by-url/uptime/timeseries', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const { window = '24h', bucket = '1h' } = req.query;
const timeseries = getUptimeTimeseries(mint.mint_id, window, bucket);
res.json({ data: timeseries });
} catch (error) {
console.error('[API] Error getting uptime timeseries:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/incidents
*/
router.get('/by-url/incidents', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const { limit = 50 } = req.query;
const incidents = getIncidents(mint.mint_id, { limit: parseInt(limit) });
res.json({ incidents });
} catch (error) {
console.error('[API] Error getting incidents:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/trust
*/
router.get('/by-url/trust', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const trust = getTrustScore(mint.mint_id);
if (!trust) {
return res.json({
score_total: null,
score_level: 'unknown',
breakdown: null,
computed_at: null
});
}
res.json(trust);
} catch (error) {
console.error('[API] Error getting trust score:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/reviews
*/
router.get('/by-url/reviews', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const { limit = 50, offset = 0 } = req.query;
const reviews = getReviews(mint.mint_id, {
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({ reviews });
} catch (error) {
console.error('[API] Error getting reviews:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/views
*/
router.get('/by-url/views', async(req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const stats = await getPageviewStats(mint.mint_id);
res.json(stats);
} catch (error) {
console.error('[API] Error getting views:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/by-url/features
*/
router.get('/by-url/features', (req, res) => {
try {
const mint = getMintFromUrl(req, res);
if (!mint) return;
const features = deriveFeatures(mint.mint_id);
res.json(features);
} catch (error) {
console.error('[API] Error getting features:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// ==========================================
// PARAMETERIZED ROUTES (:mint_id)
// These must come AFTER /by-url/* routes
// ==========================================
/**
* GET /v1/mints/:mint_id
*/
router.get('/:mint_id', resolveMintMiddleware, (req, res) => {
try {
const { mint } = req;
// Enrich with computed fields
const uptime = getUptimeSummary(mint.mint_id);
const response = {
mint_id: mint.mint_id,
canonical_url: mint.canonical_url,
urls: getMintUrls(mint.mint_id).map(u => u.url),
name: mint.name,
icon_url: mint.icon_url,
status: mint.status,
offline_since: mint.offline_since,
last_success_at: mint.last_success_at,
last_failure_at: mint.last_failure_at,
...uptime,
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
incidents_30d: countIncidents(mint.mint_id, daysAgo(30)),
trust_score: mint.trust_score,
trust_level: mint.trust_level
};
// Record pageview
recordPageview(mint.mint_id, {
sessionId: req.headers['x-session-id'],
userAgent: req.headers['user-agent'],
referer: req.headers['referer']
});
res.json(response);
} catch (error) {
console.error('[API] Error getting mint:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/urls
*/
router.get('/:mint_id/urls', resolveMintMiddleware, (req, res) => {
try {
const urls = getMintUrls(req.mint.mint_id);
res.json({
canonical_url: req.mint.canonical_url,
urls
});
} catch (error) {
console.error('[API] Error getting mint URLs:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/metadata
*/
router.get('/:mint_id/metadata', resolveMintMiddleware, (req, res) => {
try {
const metadata = getMetadataSnapshot(req.mint.mint_id);
if (!metadata) {
return res.json({ error: 'No metadata available', mint_id: req.mint.mint_id });
}
res.json({
name: metadata.name,
pubkey: metadata.pubkey,
version: metadata.version,
description: metadata.description,
description_long: metadata.description_long,
contact: metadata.contact,
motd: metadata.motd,
icon_url: metadata.icon_url,
urls: metadata.urls,
tos_url: metadata.tos_url,
nuts: metadata.nuts,
server_time: metadata.server_time,
last_fetched_at: metadata.last_fetched_at
});
} catch (error) {
console.error('[API] Error getting metadata:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/metadata/history
*/
router.get('/:mint_id/metadata/history', resolveMintMiddleware, (req, res) => {
try {
const { limit = 50 } = req.query;
const history = getMetadataHistory(req.mint.mint_id, { limit: parseInt(limit) });
res.json({ history });
} catch (error) {
console.error('[API] Error getting metadata history:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/status
*/
router.get('/:mint_id/status', resolveMintMiddleware, (req, res) => {
try {
const { mint } = req;
const latestProbe = getLatestProbe(mint.mint_id);
res.json({
status: mint.status,
offline_since: mint.offline_since,
last_checked_at: latestProbe ? latestProbe.probed_at : null,
current_rtt_ms: latestProbe ? latestProbe.rtt_ms : null
});
} catch (error) {
console.error('[API] Error getting status:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/uptime
*/
router.get('/:mint_id/uptime', resolveMintMiddleware, (req, res) => {
try {
const { window = '24h' } = req.query;
const uptime = getUptime(req.mint.mint_id, window);
if (!uptime) {
return res.json({ error: 'No uptime data available' });
}
res.json(uptime);
} catch (error) {
console.error('[API] Error getting uptime:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/uptime/timeseries
*/
router.get('/:mint_id/uptime/timeseries', resolveMintMiddleware, (req, res) => {
try {
const { window = '24h', bucket = '1h' } = req.query;
const timeseries = getUptimeTimeseries(req.mint.mint_id, window, bucket);
res.json({ data: timeseries });
} catch (error) {
console.error('[API] Error getting uptime timeseries:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/incidents
*/
router.get('/:mint_id/incidents', resolveMintMiddleware, (req, res) => {
try {
const { limit = 50 } = req.query;
const incidents = getIncidents(req.mint.mint_id, { limit: parseInt(limit) });
res.json({ incidents });
} catch (error) {
console.error('[API] Error getting incidents:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/trust
*/
router.get('/:mint_id/trust', resolveMintMiddleware, (req, res) => {
try {
const trust = getTrustScore(req.mint.mint_id);
if (!trust) {
return res.json({
score_total: null,
score_level: 'unknown',
breakdown: null,
computed_at: null
});
}
res.json(trust);
} catch (error) {
console.error('[API] Error getting trust score:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/reviews
*/
router.get('/:mint_id/reviews', resolveMintMiddleware, (req, res) => {
try {
const { limit = 50, offset = 0 } = req.query;
const reviews = getReviews(req.mint.mint_id, {
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({ reviews });
} catch (error) {
console.error('[API] Error getting reviews:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/views
*/
router.get('/:mint_id/views', resolveMintMiddleware, async(req, res) => {
try {
const stats = await getPageviewStats(req.mint.mint_id);
res.json(stats);
} catch (error) {
console.error('[API] Error getting views:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/features
*/
router.get('/:mint_id/features', resolveMintMiddleware, (req, res) => {
try {
const features = deriveFeatures(req.mint.mint_id);
res.json(features);
} catch (error) {
console.error('[API] Error getting features:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// ==========================================
// MINT DETAIL AGGREGATED ENDPOINTS
// ==========================================
/**
* GET /v1/mints/:mint_id/stats
* Aggregated mint KPIs (single endpoint for summary cards)
*/
router.get('/:mint_id/stats', resolveMintMiddleware, async(req, res) => {
try {
const stats = await getMintStats(req.mint.mint_id);
res.json(stats);
} catch (error) {
console.error('[API] Error getting mint stats:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/latency/timeseries
* Response time history for charting
*/
router.get('/:mint_id/latency/timeseries', resolveMintMiddleware, (req, res) => {
try {
const { window = '24h', bucket = '1h' } = req.query;
const validWindows = ['24h', '7d', '30d'];
const validBuckets = ['5m', '15m', '1h'];
const timeWindow = validWindows.includes(window) ? window : '24h';
const timeBucket = validBuckets.includes(bucket) ? bucket : '1h';
const data = getLatencyTimeseries(req.mint.mint_id, timeWindow, timeBucket);
res.json({
window: timeWindow,
bucket: timeBucket,
data
});
} catch (error) {
console.error('[API] Error getting latency timeseries:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/availability
* Availability breakdown (online/degraded/offline percentages)
*/
router.get('/:mint_id/availability', resolveMintMiddleware, (req, res) => {
try {
const { window = '30d' } = req.query;
const validWindows = ['24h', '7d', '30d'];
const timeWindow = validWindows.includes(window) ? window : '30d';
const availability = getMintAvailability(req.mint.mint_id, timeWindow);
res.json(availability);
} catch (error) {
console.error('[API] Error getting availability:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/trust/history
* Trust score history with change reasons
*/
router.get('/:mint_id/trust/history', resolveMintMiddleware, (req, res) => {
try {
const { limit = 30 } = req.query;
const history = getTrustScoreHistoryDetailed(
req.mint.mint_id,
Math.min(parseInt(limit) || 30, 100)
);
res.json({ history });
} catch (error) {
console.error('[API] Error getting trust history:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/trust/compare
* Compare trust score against ecosystem benchmarks
*/
router.get('/:mint_id/trust/compare', resolveMintMiddleware, (req, res) => {
try {
const { against = 'ecosystem' } = req.query;
const validBenchmarks = ['ecosystem', 'top10', 'median'];
const benchmark = validBenchmarks.includes(against) ? against : 'ecosystem';
const comparison = getTrustComparison(req.mint.mint_id, benchmark);
res.json(comparison);
} catch (error) {
console.error('[API] Error getting trust comparison:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/reviews/summary
* Quick review overview (rating distribution, averages)
*/
router.get('/:mint_id/reviews/summary', resolveMintMiddleware, (req, res) => {
try {
const summary = getReviewSummary(req.mint.mint_id);
res.json(summary);
} catch (error) {
console.error('[API] Error getting review summary:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/views/timeseries
* Pageview history for adoption trends
*/
router.get('/:mint_id/views/timeseries', resolveMintMiddleware, async(req, res) => {
try {
const { window = '7d', bucket = '1d' } = req.query;
const validWindows = ['7d', '30d'];
const validBuckets = ['1h', '1d'];
const timeWindow = validWindows.includes(window) ? window : '7d';
const timeBucket = validBuckets.includes(bucket) ? bucket : '1d';
const data = await getViewsTimeseries(req.mint.mint_id, timeWindow, timeBucket);
res.json({
window: timeWindow,
bucket: timeBucket,
data
});
} catch (error) {
console.error('[API] Error getting views timeseries:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /v1/mints/:mint_id/card
* Optimized endpoint for grid/list views
*/
router.get('/:mint_id/card', resolveMintMiddleware, (req, res) => {
try {
const card = getMintCard(req.mint.mint_id);
if (!card) {
return res.status(404).json({ error: 'Mint not found' });
}
res.json(card);
} catch (error) {
console.error('[API] Error getting mint card:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;