Skip to content

Commit 84dacbe

Browse files
author
Manuel
committed
fix: prevent scroll-to-top on plan buttons + optimize aurora orbs rendering
1 parent 98ad2b6 commit 84dacbe

28 files changed

Lines changed: 2071 additions & 3 deletions

api/dist/config/chains.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type ChainConfig = {
2+
name: string;
3+
chainId: number;
4+
rpc: string;
5+
explorer: string;
6+
contract: string;
7+
};
8+
export declare function loadChains(): Record<number, ChainConfig>;
9+
export declare function getChain(chainId: number): ChainConfig | undefined;

api/dist/config/chains.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
exports.loadChains = loadChains;
7+
exports.getChain = getChain;
8+
const fs_1 = __importDefault(require("fs"));
9+
const path_1 = __importDefault(require("path"));
10+
let cached = null;
11+
function loadChains() {
12+
if (cached)
13+
return cached;
14+
const jsonPath = path_1.default.join(__dirname, '../../config/chains.json');
15+
const raw = fs_1.default.readFileSync(jsonPath, 'utf-8');
16+
const entries = JSON.parse(raw);
17+
const result = {};
18+
for (const entry of entries) {
19+
const rpc = process.env[entry.rpcEnv] || entry.defaultRpc;
20+
const contract = process.env[entry.contractEnv] || entry.contract;
21+
result[entry.chainId] = {
22+
name: entry.name,
23+
chainId: entry.chainId,
24+
rpc,
25+
explorer: entry.explorer,
26+
contract
27+
};
28+
}
29+
cached = result;
30+
return result;
31+
}
32+
function getChain(chainId) {
33+
const chains = loadChains();
34+
return chains[chainId];
35+
}

api/dist/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare const app: import("express-serve-static-core").Express;
2+
export default app;

api/dist/index.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
const express_1 = __importDefault(require("express"));
7+
const cors_1 = __importDefault(require("cors"));
8+
const helmet_1 = __importDefault(require("helmet"));
9+
const morgan_1 = __importDefault(require("morgan"));
10+
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
11+
const dotenv_1 = __importDefault(require("dotenv"));
12+
const auth_1 = require("./routes/auth");
13+
const transfer_1 = require("./routes/transfer");
14+
const webhook_1 = require("./routes/webhook");
15+
const admin_1 = require("./routes/admin");
16+
const usecases_1 = require("./routes/usecases");
17+
const billing_1 = require("./routes/billing");
18+
const apiKey_1 = require("./middleware/apiKey");
19+
const hmac_1 = require("./middleware/hmac");
20+
const errorHandler_1 = require("./middleware/errorHandler");
21+
dotenv_1.default.config();
22+
const app = (0, express_1.default)();
23+
const PORT = process.env.PORT || 3000;
24+
// Security middleware
25+
app.use((0, helmet_1.default)({
26+
contentSecurityPolicy: {
27+
directives: {
28+
defaultSrc: ["'self'"],
29+
scriptSrc: ["'self'"],
30+
styleSrc: ["'self'", "'unsafe-inline'"],
31+
connectSrc: ["'self'"],
32+
frameAncestors: ["'none'"]
33+
}
34+
},
35+
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
36+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
37+
}));
38+
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').filter(o => o && o !== '*');
39+
app.use((0, cors_1.default)({
40+
origin: allowedOrigins.length > 0 ? allowedOrigins : (process.env.NODE_ENV === 'production' ? false : true),
41+
credentials: true,
42+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
43+
allowedHeaders: ['Content-Type', 'Authorization', 'x-reverso-signature', 'x-reverso-timestamp', 'x-reverso-nonce']
44+
}));
45+
app.use((0, morgan_1.default)('combined'));
46+
// Raw body for Stripe webhook signature verification
47+
app.use('/api/v1/billing/webhook', express_1.default.raw({ type: 'application/json', limit: '1mb' }), (req, _res, next) => {
48+
req.rawBody = req.body;
49+
req.body = JSON.parse(req.body.toString());
50+
next();
51+
});
52+
app.use(express_1.default.json({ limit: '10mb' }));
53+
// Global rate limit
54+
const globalLimiter = (0, express_rate_limit_1.default)({
55+
windowMs: 15 * 60 * 1000, // 15 minutes
56+
max: 1000, // max 1000 requests per 15 min
57+
message: { error: 'Too many requests, please try again later' }
58+
});
59+
app.use(globalLimiter);
60+
// Health check (no auth)
61+
app.get('/health', (req, res) => {
62+
res.json({
63+
status: 'ok',
64+
version: '1.0.0',
65+
timestamp: new Date().toISOString()
66+
});
67+
});
68+
// API Documentation
69+
app.get('/', (req, res) => {
70+
res.json({
71+
name: 'REVERSO Enterprise API',
72+
version: '1.0.0',
73+
documentation: 'https://reverso.one/api',
74+
endpoints: {
75+
auth: '/api/v1/auth',
76+
transfers: '/api/v1/transfers',
77+
webhooks: '/api/v1/webhooks',
78+
admin: '/api/v1/admin'
79+
},
80+
plans: {
81+
starter: { price: '$99/month', txLimit: 100, features: ['API Access', 'Email Support'] },
82+
business: { price: '$499/month', txLimit: -1, features: ['Unlimited TX', 'Dashboard', 'Priority Support'] },
83+
enterprise: { price: '$2000/month', txLimit: -1, features: ['White-label', 'SLA', '24/7 Support', 'Custom Integration', 'Usecase APIs'] }
84+
}
85+
});
86+
});
87+
// Public routes
88+
app.use('/api/v1/auth', auth_1.authRouter);
89+
app.use('/api/v1/billing', billing_1.billingRouter);
90+
// Protected routes (require API key + HMAC)
91+
app.use('/api/v1/transfers', apiKey_1.apiKeyMiddleware, hmac_1.hmacMiddleware, transfer_1.transferRouter);
92+
app.use('/api/v1/webhooks', apiKey_1.apiKeyMiddleware, hmac_1.hmacMiddleware, webhook_1.webhookRouter);
93+
app.use('/api/v1/admin', apiKey_1.apiKeyMiddleware, hmac_1.hmacMiddleware, admin_1.adminRouter);
94+
app.use('/api/v1/usecases', apiKey_1.apiKeyMiddleware, hmac_1.hmacMiddleware, usecases_1.usecaseRouter);
95+
// Error handling
96+
app.use(errorHandler_1.errorHandler);
97+
// 404 handler
98+
app.use((req, res) => {
99+
res.status(404).json({ error: 'Endpoint not found' });
100+
});
101+
app.listen(PORT, () => {
102+
console.log(`
103+
╔═══════════════════════════════════════════════════════════╗
104+
║ ║
105+
║ 🔄 REVERSO Enterprise API v1.0.0 ║
106+
║ ║
107+
║ Server running on port ${PORT}
108+
║ Documentation: https://reverso.one/api ║
109+
║ ║
110+
║ Plans: ║
111+
║ • Starter: $99/mo - 100 tx/month ║
112+
║ • Business: $499/mo - Unlimited tx ║
113+
║ • Enterprise: $2000/mo - White-label + SLA ║
114+
║ ║
115+
╚═══════════════════════════════════════════════════════════╝
116+
`);
117+
// Self-ping every 14 minutes to keep Render free tier awake
118+
const SELF_URL = process.env.RENDER_EXTERNAL_URL || `http://localhost:${PORT}`;
119+
setInterval(() => {
120+
fetch(`${SELF_URL}/health`).catch(() => { });
121+
}, 14 * 60 * 1000);
122+
});
123+
exports.default = app;

api/dist/middleware/apiKey.d.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { ApiKey, ApiPlan, PLAN_CONFIG } from '../types';
3+
export interface AuthenticatedRequest extends Request {
4+
apiKey?: ApiKey;
5+
}
6+
/**
7+
* API Key middleware - validates and tracks usage
8+
*/
9+
export declare function apiKeyMiddleware(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
10+
/**
11+
* Increment transaction counter for API key
12+
*/
13+
export declare function incrementTxCount(apiKeyId: string): void;
14+
/**
15+
* Create new API key
16+
*/
17+
export declare function createApiKey(userId: string, plan: ApiPlan, webhookUrl?: string, allowedOrigins?: string[]): Promise<ApiKey>;
18+
/**
19+
* Get API key by ID
20+
*/
21+
export declare function getApiKey(id: string): ApiKey | undefined;
22+
/**
23+
* Revoke API key
24+
*/
25+
export declare function revokeApiKey(id: string): boolean;
26+
/**
27+
* Check if plan has feature
28+
*/
29+
export declare function hasFeature(plan: ApiPlan, feature: keyof typeof PLAN_CONFIG.starter): boolean;

api/dist/middleware/apiKey.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
exports.apiKeyMiddleware = apiKeyMiddleware;
7+
exports.incrementTxCount = incrementTxCount;
8+
exports.createApiKey = createApiKey;
9+
exports.getApiKey = getApiKey;
10+
exports.revokeApiKey = revokeApiKey;
11+
exports.hasFeature = hasFeature;
12+
const uuid_1 = require("uuid");
13+
const bcryptjs_1 = __importDefault(require("bcryptjs"));
14+
const types_1 = require("../types");
15+
// In-memory store (replace with MongoDB/PostgreSQL in production)
16+
const apiKeys = new Map();
17+
// Simple per-key rate limiter (requests per minute)
18+
const rateWindows = new Map();
19+
const REQUESTS_PER_MINUTE = 300; // adjust per plan if needed
20+
/**
21+
* API Key middleware - validates and tracks usage
22+
*/
23+
async function apiKeyMiddleware(req, res, next) {
24+
try {
25+
const authHeader = req.headers.authorization;
26+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
27+
return res.status(401).json({
28+
error: 'Missing or invalid API key',
29+
code: 'UNAUTHORIZED'
30+
});
31+
}
32+
const apiKeyValue = authHeader.substring(7); // Remove 'Bearer '
33+
// Find API key (hash compare only)
34+
let foundKey;
35+
for (const [, key] of apiKeys) {
36+
const isValid = await bcryptjs_1.default.compare(apiKeyValue, key.hashedKey);
37+
if (isValid) {
38+
foundKey = key;
39+
break;
40+
}
41+
}
42+
if (!foundKey) {
43+
return res.status(401).json({
44+
error: 'Invalid API key',
45+
code: 'INVALID_API_KEY'
46+
});
47+
}
48+
if (!foundKey.isActive) {
49+
return res.status(403).json({
50+
error: 'API key is disabled',
51+
code: 'API_KEY_DISABLED'
52+
});
53+
}
54+
if (foundKey.expiresAt < new Date()) {
55+
return res.status(403).json({
56+
error: 'API key has expired',
57+
code: 'API_KEY_EXPIRED'
58+
});
59+
}
60+
// Check rate limit based on plan usage cap
61+
const planConfig = types_1.PLAN_CONFIG[foundKey.plan];
62+
if (planConfig.txLimit !== -1 && foundKey.txUsed >= planConfig.txLimit) {
63+
return res.status(429).json({
64+
error: 'Monthly transaction limit exceeded',
65+
code: 'TX_LIMIT_EXCEEDED',
66+
details: {
67+
plan: foundKey.plan,
68+
limit: planConfig.txLimit,
69+
used: foundKey.txUsed,
70+
upgrade: 'Contact sales@reverso.one to upgrade'
71+
}
72+
});
73+
}
74+
// Per-key rate limit (burst control, 1-minute window)
75+
const now = Date.now();
76+
const windowStart = now - 60_000;
77+
const window = rateWindows.get(foundKey.id);
78+
if (!window || window.start < windowStart) {
79+
rateWindows.set(foundKey.id, { start: now, count: 1 });
80+
}
81+
else {
82+
if (window.count >= REQUESTS_PER_MINUTE) {
83+
return res.status(429).json({
84+
error: 'Rate limit exceeded for API key',
85+
code: 'RATE_LIMITED'
86+
});
87+
}
88+
window.count += 1;
89+
rateWindows.set(foundKey.id, window);
90+
}
91+
// Check origin if configured
92+
if (foundKey.allowedOrigins.length > 0 && !foundKey.allowedOrigins.includes('*')) {
93+
const origin = req.headers.origin;
94+
if (origin && !foundKey.allowedOrigins.includes(origin)) {
95+
return res.status(403).json({
96+
error: 'Origin not allowed',
97+
code: 'ORIGIN_FORBIDDEN'
98+
});
99+
}
100+
}
101+
req.apiKey = foundKey;
102+
next();
103+
}
104+
catch (error) {
105+
console.error('API Key middleware error:', error);
106+
res.status(500).json({
107+
error: 'Authentication error',
108+
code: 'AUTH_ERROR'
109+
});
110+
}
111+
}
112+
/**
113+
* Increment transaction counter for API key
114+
*/
115+
function incrementTxCount(apiKeyId) {
116+
const key = apiKeys.get(apiKeyId);
117+
if (key) {
118+
key.txUsed++;
119+
apiKeys.set(apiKeyId, key);
120+
}
121+
}
122+
/**
123+
* Create new API key
124+
*/
125+
async function createApiKey(userId, plan, webhookUrl, allowedOrigins = []) {
126+
const rawKey = `rsk_${plan}_${(0, uuid_1.v4)().replace(/-/g, '')}`;
127+
const hashedKey = await bcryptjs_1.default.hash(rawKey, 10);
128+
const signingSecret = `sig_${(0, uuid_1.v4)().replace(/-/g, '')}`;
129+
const apiKey = {
130+
id: (0, uuid_1.v4)(),
131+
key: rawKey, // Only returned once at creation
132+
hashedKey,
133+
signingSecret,
134+
userId,
135+
plan,
136+
txLimit: types_1.PLAN_CONFIG[plan].txLimit,
137+
txUsed: 0,
138+
webhookUrl,
139+
allowedOrigins,
140+
createdAt: new Date(),
141+
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
142+
isActive: true
143+
};
144+
// Store only hashed value to avoid keeping raw key in memory
145+
apiKeys.set(apiKey.id, { ...apiKey, key: '' });
146+
return apiKey;
147+
}
148+
/**
149+
* Get API key by ID
150+
*/
151+
function getApiKey(id) {
152+
return apiKeys.get(id);
153+
}
154+
/**
155+
* Revoke API key
156+
*/
157+
function revokeApiKey(id) {
158+
const key = apiKeys.get(id);
159+
if (key) {
160+
key.isActive = false;
161+
apiKeys.set(id, key);
162+
return true;
163+
}
164+
return false;
165+
}
166+
/**
167+
* Check if plan has feature
168+
*/
169+
function hasFeature(plan, feature) {
170+
return !!types_1.PLAN_CONFIG[plan][feature];
171+
}
172+
// Create demo API key on startup
173+
(async () => {
174+
const demoKey = await createApiKey('demo-user', 'business', undefined, ['*']);
175+
console.log(`\n📌 Demo API Key: ${demoKey.key}\n`);
176+
})();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
export interface ApiError extends Error {
3+
statusCode?: number;
4+
code?: string;
5+
}
6+
export declare function errorHandler(err: ApiError, req: Request, res: Response, next: NextFunction): void;
7+
export declare function asyncHandler(fn: Function): (req: Request, res: Response, next: NextFunction) => void;
8+
export declare class HttpError extends Error {
9+
statusCode: number;
10+
code: string;
11+
constructor(message: string, statusCode?: number, code?: string);
12+
}
13+
export declare const BadRequest: (message: string, code?: string) => HttpError;
14+
export declare const Unauthorized: (message?: string, code?: string) => HttpError;
15+
export declare const Forbidden: (message?: string, code?: string) => HttpError;
16+
export declare const NotFound: (message?: string, code?: string) => HttpError;
17+
export declare const TooManyRequests: (message?: string, code?: string) => HttpError;

0 commit comments

Comments
 (0)