feat: four super-admin keys and production customer API security
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m18s
CI/CD Pipeline / Security Scanning (push) Successful in 2m34s
CI/CD Pipeline / Lint and Format (push) Failing after 54s
CI/CD Pipeline / Terraform Validation (push) Failing after 27s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 28s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 39s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 31s
Validation / validate-genesis (push) Successful in 28s
Validation / validate-terraform (push) Failing after 31s
Validation / validate-kubernetes (push) Failing after 11s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m27s
Validation / validate-documentation (push) Failing after 21s
Verify Deployment / Verify Deployment (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m18s
CI/CD Pipeline / Security Scanning (push) Successful in 2m34s
CI/CD Pipeline / Lint and Format (push) Failing after 54s
CI/CD Pipeline / Terraform Validation (push) Failing after 27s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 28s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 39s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 31s
Validation / validate-genesis (push) Successful in 28s
Validation / validate-terraform (push) Failing after 31s
Validation / validate-kubernetes (push) Failing after 11s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m27s
Validation / validate-documentation (push) Failing after 21s
Verify Deployment / Verify Deployment (push) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,19 +8,7 @@ Object.defineProperty(exports, "exchangePort", { enumerable: true, get: function
|
||||
const settlement_1 = require("../../adapters/settlement");
|
||||
const token_aggregation_1 = require("../../adapters/token-aggregation");
|
||||
const swap_monitor_1 = require("../../store/swap-monitor");
|
||||
function apiKey(req) {
|
||||
return req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key') || undefined;
|
||||
}
|
||||
function requireKey(req, res) {
|
||||
const cfg = (0, config_1.loadExchangeConfig)();
|
||||
if (!cfg.production.requireApiKey)
|
||||
return true;
|
||||
const key = apiKey(req);
|
||||
if (key && key === process.env.OMNL_API_KEY)
|
||||
return true;
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return false;
|
||||
}
|
||||
const auth_1 = require("../../middleware/auth");
|
||||
function createExchangeRouter() {
|
||||
const router = (0, express_1.Router)();
|
||||
const cfg = (0, config_1.loadExchangeConfig)();
|
||||
@@ -36,7 +24,9 @@ function createExchangeRouter() {
|
||||
status: 'ok',
|
||||
});
|
||||
});
|
||||
router.get('/tokens', (_req, res) => {
|
||||
router.get('/tokens', (req, res) => {
|
||||
if (!(0, auth_1.requireRead)(req, res))
|
||||
return;
|
||||
const tokens = (0, config_1.loadExchangeTokens)();
|
||||
res.json({
|
||||
chainId: cfg.chainId,
|
||||
@@ -46,11 +36,15 @@ function createExchangeRouter() {
|
||||
routers: cfg.routers,
|
||||
});
|
||||
});
|
||||
router.get('/tokens/capabilities', (_req, res) => {
|
||||
router.get('/tokens/capabilities', (req, res) => {
|
||||
if (!(0, auth_1.requireRead)(req, res))
|
||||
return;
|
||||
const registry = (0, config_1.loadM2TokenRegistry)();
|
||||
res.json(registry);
|
||||
});
|
||||
router.get('/providers', async (_req, res) => {
|
||||
router.get('/providers', async (req, res) => {
|
||||
if (!(0, auth_1.requireRead)(req, res))
|
||||
return;
|
||||
try {
|
||||
res.json(await (0, token_aggregation_1.fetchProviderCapabilities)(cfg.chainId));
|
||||
}
|
||||
@@ -59,6 +53,8 @@ function createExchangeRouter() {
|
||||
}
|
||||
});
|
||||
router.get('/quote', async (req, res) => {
|
||||
if (!(0, auth_1.requireRead)(req, res))
|
||||
return;
|
||||
try {
|
||||
const tokenIn = String(req.query.tokenIn || '');
|
||||
const tokenOut = String(req.query.tokenOut || '');
|
||||
@@ -91,6 +87,8 @@ function createExchangeRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/plan', async (req, res) => {
|
||||
if (!(0, auth_1.requireTrade)(req, res))
|
||||
return;
|
||||
try {
|
||||
const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body;
|
||||
if (!tokenIn || !tokenOut || !amountIn) {
|
||||
@@ -113,6 +111,8 @@ function createExchangeRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/execute-plan', async (req, res) => {
|
||||
if (!(0, auth_1.requireTrade)(req, res))
|
||||
return;
|
||||
try {
|
||||
const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body;
|
||||
if (!tokenIn || !tokenOut || !amountIn || !recipient) {
|
||||
@@ -135,6 +135,8 @@ function createExchangeRouter() {
|
||||
}
|
||||
});
|
||||
router.get('/ledgers', async (req, res) => {
|
||||
if (!(0, auth_1.requireRead)(req, res))
|
||||
return;
|
||||
try {
|
||||
const wallet = req.query.wallet ? String(req.query.wallet) : undefined;
|
||||
const serverKey = process.env.OMNL_API_KEY;
|
||||
@@ -165,10 +167,14 @@ function createExchangeRouter() {
|
||||
res.status(502).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
router.get('/swap/monitor', (_req, res) => {
|
||||
res.json({ stats: (0, swap_monitor_1.swapStats)(), swaps: (0, swap_monitor_1.listSwaps)(Number(_req.query.limit ?? 50)) });
|
||||
router.get('/swap/monitor', (req, res) => {
|
||||
if (!(0, auth_1.requireSuperAdmin)(req, res))
|
||||
return;
|
||||
res.json({ stats: (0, swap_monitor_1.swapStats)(), swaps: (0, swap_monitor_1.listSwaps)(Number(req.query.limit ?? 50)) });
|
||||
});
|
||||
router.post('/swap/record', (req, res) => {
|
||||
if (!(0, auth_1.requireTrade)(req, res))
|
||||
return;
|
||||
const body = req.body;
|
||||
const rec = (0, swap_monitor_1.recordSwap)({
|
||||
officeId: cfg.officeId,
|
||||
@@ -188,6 +194,8 @@ function createExchangeRouter() {
|
||||
res.json(rec);
|
||||
});
|
||||
router.patch('/swap/record/:id', (req, res) => {
|
||||
if (!(0, auth_1.requireSuperAdmin)(req, res))
|
||||
return;
|
||||
const updated = (0, swap_monitor_1.updateSwap)(req.params.id, req.body);
|
||||
if (!updated) {
|
||||
res.status(404).json({ error: 'Not found' });
|
||||
@@ -196,7 +204,7 @@ function createExchangeRouter() {
|
||||
res.json(updated);
|
||||
});
|
||||
router.post('/swap/settle', async (req, res) => {
|
||||
if (!requireKey(req, res))
|
||||
if (!(0, auth_1.requireSuperAdmin)(req, res))
|
||||
return;
|
||||
if (!cfg.production.postSwapSettlementAudit) {
|
||||
res.status(403).json({ error: 'postSwapSettlementAudit disabled' });
|
||||
@@ -218,7 +226,7 @@ function createExchangeRouter() {
|
||||
recipientAddress: String(recipientAddress ?? ''),
|
||||
remittanceInfo: `DBIS Exchange M2 swap settlement Office ${cfg.officeId}`,
|
||||
tokenLineId: lineId,
|
||||
apiKey: apiKey(req),
|
||||
apiKey: (0, auth_1.apiKey)(req),
|
||||
});
|
||||
if (swapId)
|
||||
(0, swap_monitor_1.updateSwap)(String(swapId), { settlementRef: audit.settlementId, status: 'CONFIRMED' });
|
||||
@@ -229,7 +237,7 @@ function createExchangeRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/token-load', async (req, res) => {
|
||||
if (!requireKey(req, res))
|
||||
if (!(0, auth_1.requireSuperAdmin)(req, res))
|
||||
return;
|
||||
try {
|
||||
const body = req.body;
|
||||
@@ -244,7 +252,7 @@ function createExchangeRouter() {
|
||||
amount: String(body.amount ?? '0'),
|
||||
recipientAddress: String(body.recipientAddress ?? ''),
|
||||
currency: String(body.currency ?? token?.currencyCode ?? 'USD'),
|
||||
apiKey: apiKey(req) ?? process.env.OMNL_API_KEY,
|
||||
apiKey: (0, auth_1.apiKey)(req) ?? process.env.OMNL_API_KEY,
|
||||
});
|
||||
res.status(result.phase === 'FAILED' ? 422 : 200).json(result);
|
||||
}
|
||||
@@ -253,7 +261,7 @@ function createExchangeRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/transfer/internal', async (req, res) => {
|
||||
if (!requireKey(req, res))
|
||||
if (!(0, auth_1.requireSuperAdmin)(req, res))
|
||||
return;
|
||||
try {
|
||||
const body = req.body;
|
||||
@@ -264,7 +272,7 @@ function createExchangeRouter() {
|
||||
amount: String(body.amount ?? '0'),
|
||||
recipientAddress: String(body.recipientAddress ?? ''),
|
||||
senderAddress: body.senderAddress ? String(body.senderAddress) : undefined,
|
||||
apiKey: apiKey(req) ?? process.env.OMNL_API_KEY,
|
||||
apiKey: (0, auth_1.apiKey)(req) ?? process.env.OMNL_API_KEY,
|
||||
});
|
||||
res.status(result.phase === 'FAILED' ? 422 : 200).json(result);
|
||||
}
|
||||
@@ -273,7 +281,7 @@ function createExchangeRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/transfer/external', async (req, res) => {
|
||||
if (!requireKey(req, res))
|
||||
if (!(0, auth_1.requireSuperAdmin)(req, res))
|
||||
return;
|
||||
try {
|
||||
const body = req.body;
|
||||
@@ -287,7 +295,7 @@ function createExchangeRouter() {
|
||||
beneficiaryName: body.beneficiaryName ? String(body.beneficiaryName) : undefined,
|
||||
currency: body.currency ? String(body.currency) : undefined,
|
||||
remittanceInfo: body.remittanceInfo ? String(body.remittanceInfo) : undefined,
|
||||
apiKey: apiKey(req) ?? process.env.OMNL_API_KEY,
|
||||
apiKey: (0, auth_1.apiKey)(req) ?? process.env.OMNL_API_KEY,
|
||||
});
|
||||
res.status(result.phase === 'FAILED' ? 422 : 200).json(result);
|
||||
}
|
||||
|
||||
40
services/dbis-exchange/dist/middleware/auth.js
vendored
Normal file
40
services/dbis-exchange/dist/middleware/auth.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.apiKey = apiKey;
|
||||
exports.requireScope = requireScope;
|
||||
exports.requireSuperAdmin = requireSuperAdmin;
|
||||
exports.requireRead = requireRead;
|
||||
exports.requireTrade = requireTrade;
|
||||
const integration_foundation_1 = require("@dbis/integration-foundation");
|
||||
const config_1 = require("../config");
|
||||
function apiKey(req) {
|
||||
return (0, integration_foundation_1.extractApiKey)(req);
|
||||
}
|
||||
function requireScope(req, res, scope) {
|
||||
const cfg = (0, config_1.loadExchangeConfig)();
|
||||
if (!cfg.production.requireApiKey && !(0, integration_foundation_1.isProductionSecurityMode)())
|
||||
return true;
|
||||
const policy = (0, integration_foundation_1.loadCustomerSecurityPolicy)();
|
||||
if (policy.requireApiKeyAllExchangeRoutes || cfg.production.requireApiKey) {
|
||||
const principal = (0, integration_foundation_1.resolveOmnlPrincipal)(req);
|
||||
if (!principal) {
|
||||
res.status(401).json({ error: 'Unauthorized', hint: 'Valid customer or super-admin API key required' });
|
||||
return false;
|
||||
}
|
||||
if (!(0, integration_foundation_1.principalHasScope)(principal, scope)) {
|
||||
res.status(403).json({ error: 'Forbidden', scope, role: principal.role });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function requireSuperAdmin(req, res) {
|
||||
return requireScope(req, res, 'admin');
|
||||
}
|
||||
function requireRead(req, res) {
|
||||
return requireScope(req, res, 'read');
|
||||
}
|
||||
function requireTrade(req, res) {
|
||||
return requireScope(req, res, 'trade');
|
||||
}
|
||||
28
services/dbis-exchange/dist/middleware/rate-limit.js
vendored
Normal file
28
services/dbis-exchange/dist/middleware/rate-limit.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.exchangeRateLimiter = void 0;
|
||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||
const integration_foundation_1 = require("@dbis/integration-foundation");
|
||||
function asHandler(h) {
|
||||
return h;
|
||||
}
|
||||
exports.exchangeRateLimiter = asHandler((0, express_rate_limit_1.default)({
|
||||
windowMs: 60_000,
|
||||
max: (req) => {
|
||||
if (!(0, integration_foundation_1.isProductionSecurityMode)())
|
||||
return 200;
|
||||
const policy = (0, integration_foundation_1.loadCustomerSecurityPolicy)();
|
||||
const principal = (0, integration_foundation_1.resolveOmnlPrincipal)(req);
|
||||
if (!principal)
|
||||
return policy.rateLimits.anonymousPerMinute;
|
||||
if (principal.role === 'customer')
|
||||
return policy.rateLimits.customerPerMinute;
|
||||
return policy.rateLimits.superAdminPerMinute;
|
||||
},
|
||||
message: { error: 'Too many requests — rate limit exceeded' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
}));
|
||||
20
services/dbis-exchange/dist/server.js
vendored
20
services/dbis-exchange/dist/server.js
vendored
@@ -6,12 +6,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.startServer = startServer;
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const integration_foundation_1 = require("@dbis/integration-foundation");
|
||||
const exchange_1 = require("./api/routes/exchange");
|
||||
const rate_limit_1 = require("./middleware/rate-limit");
|
||||
function corsOptions() {
|
||||
const policy = (0, integration_foundation_1.loadCustomerSecurityPolicy)();
|
||||
const origins = policy.corsOrigins.filter(Boolean);
|
||||
if (!origins.length)
|
||||
return {};
|
||||
return {
|
||||
origin(origin, cb) {
|
||||
if (!origin || origins.includes(origin))
|
||||
cb(null, true);
|
||||
else
|
||||
cb(new Error('CORS not allowed'));
|
||||
},
|
||||
};
|
||||
}
|
||||
function startServer() {
|
||||
const app = (0, express_1.default)();
|
||||
app.use((0, cors_1.default)());
|
||||
app.use((0, cors_1.default)(corsOptions()));
|
||||
app.use(express_1.default.json());
|
||||
app.use('/api/v1/exchange', (0, exchange_1.createExchangeRouter)());
|
||||
app.use('/api/v1/exchange', rate_limit_1.exchangeRateLimiter, (0, exchange_1.createExchangeRouter)());
|
||||
const p = (0, exchange_1.exchangePort)();
|
||||
app.listen(p, () => {
|
||||
console.log(`DBIS Exchange (Pancake-style) on http://localhost:${p}/api/v1/exchange`);
|
||||
|
||||
41
services/dbis-exchange/package-lock.json
generated
41
services/dbis-exchange/package-lock.json
generated
@@ -8,12 +8,14 @@
|
||||
"name": "dbis-exchange",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@dbis/integration-foundation": "file:../../packages/integration-foundation",
|
||||
"@dbis/settlement-core": "file:../../packages/settlement-core",
|
||||
"axios": "^1.15.2",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.6.1",
|
||||
"ethers": "^6.16.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.5.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -25,6 +27,14 @@
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"../../packages/integration-foundation": {
|
||||
"name": "@dbis/integration-foundation",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.4.0"
|
||||
}
|
||||
},
|
||||
"../../packages/settlement-core": {
|
||||
"name": "@dbis/settlement-core",
|
||||
"version": "0.1.0",
|
||||
@@ -52,6 +62,10 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@dbis/integration-foundation": {
|
||||
"resolved": "../../packages/integration-foundation",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@dbis/settlement-core": {
|
||||
"resolved": "../../packages/settlement-core",
|
||||
"link": true
|
||||
@@ -716,6 +730,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
@@ -964,6 +996,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
"dev": "ts-node src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dbis/integration-foundation": "file:../../packages/integration-foundation",
|
||||
"@dbis/settlement-core": "file:../../packages/settlement-core",
|
||||
"axios": "^1.15.2",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.6.1",
|
||||
"ethers": "^6.16.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.5.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -24,19 +24,7 @@ import {
|
||||
fetchProviderCapabilities,
|
||||
} from '../../adapters/token-aggregation';
|
||||
import { listSwaps, recordSwap, swapStats, updateSwap } from '../../store/swap-monitor';
|
||||
|
||||
function apiKey(req: Request): string | undefined {
|
||||
return req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key') || undefined;
|
||||
}
|
||||
|
||||
function requireKey(req: Request, res: Response): boolean {
|
||||
const cfg = loadExchangeConfig();
|
||||
if (!cfg.production.requireApiKey) return true;
|
||||
const key = apiKey(req);
|
||||
if (key && key === process.env.OMNL_API_KEY) return true;
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return false;
|
||||
}
|
||||
import { apiKey, requireRead, requireSuperAdmin, requireTrade } from '../../middleware/auth';
|
||||
|
||||
export function createExchangeRouter(): Router {
|
||||
const router = Router();
|
||||
@@ -55,7 +43,8 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/tokens', (_req, res) => {
|
||||
router.get('/tokens', (req, res) => {
|
||||
if (!requireRead(req, res)) return;
|
||||
const tokens = loadExchangeTokens();
|
||||
res.json({
|
||||
chainId: cfg.chainId,
|
||||
@@ -66,12 +55,14 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/tokens/capabilities', (_req, res) => {
|
||||
router.get('/tokens/capabilities', (req, res) => {
|
||||
if (!requireRead(req, res)) return;
|
||||
const registry = loadM2TokenRegistry();
|
||||
res.json(registry);
|
||||
});
|
||||
|
||||
router.get('/providers', async (_req, res) => {
|
||||
router.get('/providers', async (req, res) => {
|
||||
if (!requireRead(req, res)) return;
|
||||
try {
|
||||
res.json(await fetchProviderCapabilities(cfg.chainId));
|
||||
} catch (e) {
|
||||
@@ -80,6 +71,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.get('/quote', async (req, res) => {
|
||||
if (!requireRead(req, res)) return;
|
||||
try {
|
||||
const tokenIn = String(req.query.tokenIn || '');
|
||||
const tokenOut = String(req.query.tokenOut || '');
|
||||
@@ -112,6 +104,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.post('/plan', async (req, res) => {
|
||||
if (!requireTrade(req, res)) return;
|
||||
try {
|
||||
const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body as Record<string, unknown>;
|
||||
if (!tokenIn || !tokenOut || !amountIn) {
|
||||
@@ -134,6 +127,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.post('/execute-plan', async (req, res) => {
|
||||
if (!requireTrade(req, res)) return;
|
||||
try {
|
||||
const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body as Record<string, unknown>;
|
||||
if (!tokenIn || !tokenOut || !amountIn || !recipient) {
|
||||
@@ -156,6 +150,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.get('/ledgers', async (req, res) => {
|
||||
if (!requireRead(req, res)) return;
|
||||
try {
|
||||
const wallet = req.query.wallet ? String(req.query.wallet) : undefined;
|
||||
const serverKey = process.env.OMNL_API_KEY;
|
||||
@@ -187,11 +182,13 @@ export function createExchangeRouter(): Router {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/swap/monitor', (_req, res) => {
|
||||
res.json({ stats: swapStats(), swaps: listSwaps(Number(_req.query.limit ?? 50)) });
|
||||
router.get('/swap/monitor', (req, res) => {
|
||||
if (!requireSuperAdmin(req, res)) return;
|
||||
res.json({ stats: swapStats(), swaps: listSwaps(Number(req.query.limit ?? 50)) });
|
||||
});
|
||||
|
||||
router.post('/swap/record', (req, res) => {
|
||||
if (!requireTrade(req, res)) return;
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const rec = recordSwap({
|
||||
officeId: cfg.officeId,
|
||||
@@ -212,6 +209,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.patch('/swap/record/:id', (req, res) => {
|
||||
if (!requireSuperAdmin(req, res)) return;
|
||||
const updated = updateSwap(req.params.id, req.body as Record<string, unknown>);
|
||||
if (!updated) {
|
||||
res.status(404).json({ error: 'Not found' });
|
||||
@@ -221,7 +219,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.post('/swap/settle', async (req, res) => {
|
||||
if (!requireKey(req, res)) return;
|
||||
if (!requireSuperAdmin(req, res)) return;
|
||||
if (!cfg.production.postSwapSettlementAudit) {
|
||||
res.status(403).json({ error: 'postSwapSettlementAudit disabled' });
|
||||
return;
|
||||
@@ -253,7 +251,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.post('/token-load', async (req, res) => {
|
||||
if (!requireKey(req, res)) return;
|
||||
if (!requireSuperAdmin(req, res)) return;
|
||||
try {
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const tokens = loadExchangeTokens();
|
||||
@@ -276,7 +274,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.post('/transfer/internal', async (req, res) => {
|
||||
if (!requireKey(req, res)) return;
|
||||
if (!requireSuperAdmin(req, res)) return;
|
||||
try {
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const result = await requestInternalTransfer({
|
||||
@@ -295,7 +293,7 @@ export function createExchangeRouter(): Router {
|
||||
});
|
||||
|
||||
router.post('/transfer/external', async (req, res) => {
|
||||
if (!requireKey(req, res)) return;
|
||||
if (!requireSuperAdmin(req, res)) return;
|
||||
try {
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const result = await requestExternalTransfer({
|
||||
|
||||
45
services/dbis-exchange/src/middleware/auth.ts
Normal file
45
services/dbis-exchange/src/middleware/auth.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
extractApiKey,
|
||||
isProductionSecurityMode,
|
||||
loadCustomerSecurityPolicy,
|
||||
principalHasScope,
|
||||
resolveOmnlPrincipal,
|
||||
} from '@dbis/integration-foundation';
|
||||
import { loadExchangeConfig } from '../config';
|
||||
|
||||
export function apiKey(req: Request): string | undefined {
|
||||
return extractApiKey(req);
|
||||
}
|
||||
|
||||
export function requireScope(req: Request, res: Response, scope: string): boolean {
|
||||
const cfg = loadExchangeConfig();
|
||||
if (!cfg.production.requireApiKey && !isProductionSecurityMode()) return true;
|
||||
|
||||
const policy = loadCustomerSecurityPolicy();
|
||||
if (policy.requireApiKeyAllExchangeRoutes || cfg.production.requireApiKey) {
|
||||
const principal = resolveOmnlPrincipal(req);
|
||||
if (!principal) {
|
||||
res.status(401).json({ error: 'Unauthorized', hint: 'Valid customer or super-admin API key required' });
|
||||
return false;
|
||||
}
|
||||
if (!principalHasScope(principal, scope)) {
|
||||
res.status(403).json({ error: 'Forbidden', scope, role: principal.role });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function requireSuperAdmin(req: Request, res: Response): boolean {
|
||||
return requireScope(req, res, 'admin');
|
||||
}
|
||||
|
||||
export function requireRead(req: Request, res: Response): boolean {
|
||||
return requireScope(req, res, 'read');
|
||||
}
|
||||
|
||||
export function requireTrade(req: Request, res: Response): boolean {
|
||||
return requireScope(req, res, 'trade');
|
||||
}
|
||||
24
services/dbis-exchange/src/middleware/rate-limit.ts
Normal file
24
services/dbis-exchange/src/middleware/rate-limit.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import type { RequestHandler } from 'express';
|
||||
import { isProductionSecurityMode, loadCustomerSecurityPolicy, resolveOmnlPrincipal } from '@dbis/integration-foundation';
|
||||
|
||||
function asHandler(h: ReturnType<typeof rateLimit>): RequestHandler {
|
||||
return h as unknown as RequestHandler;
|
||||
}
|
||||
|
||||
export const exchangeRateLimiter = asHandler(
|
||||
rateLimit({
|
||||
windowMs: 60_000,
|
||||
max: (req) => {
|
||||
if (!isProductionSecurityMode()) return 200;
|
||||
const policy = loadCustomerSecurityPolicy();
|
||||
const principal = resolveOmnlPrincipal(req);
|
||||
if (!principal) return policy.rateLimits.anonymousPerMinute;
|
||||
if (principal.role === 'customer') return policy.rateLimits.customerPerMinute;
|
||||
return policy.rateLimits.superAdminPerMinute;
|
||||
},
|
||||
message: { error: 'Too many requests — rate limit exceeded' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
}),
|
||||
);
|
||||
@@ -1,12 +1,26 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { loadCustomerSecurityPolicy } from '@dbis/integration-foundation';
|
||||
import { createExchangeRouter, exchangePort } from './api/routes/exchange';
|
||||
import { exchangeRateLimiter } from './middleware/rate-limit';
|
||||
|
||||
function corsOptions(): cors.CorsOptions {
|
||||
const policy = loadCustomerSecurityPolicy();
|
||||
const origins = policy.corsOrigins.filter(Boolean);
|
||||
if (!origins.length) return {};
|
||||
return {
|
||||
origin(origin, cb) {
|
||||
if (!origin || origins.includes(origin)) cb(null, true);
|
||||
else cb(new Error('CORS not allowed'));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function startServer(): void {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(cors(corsOptions()));
|
||||
app.use(express.json());
|
||||
app.use('/api/v1/exchange', createExchangeRouter());
|
||||
app.use('/api/v1/exchange', exchangeRateLimiter, createExchangeRouter());
|
||||
const p = exchangePort();
|
||||
app.listen(p, () => {
|
||||
console.log(`DBIS Exchange (Pancake-style) on http://localhost:${p}/api/v1/exchange`);
|
||||
|
||||
@@ -11,16 +11,7 @@ const office_scope_1 = require("../../workflows/office-scope");
|
||||
const settlement_store_1 = require("../../store/settlement-store");
|
||||
const swift_inbound_1 = require("../../workflows/swift-inbound");
|
||||
const fineract_1 = require("../../adapters/fineract");
|
||||
function requireApiKey(req, res) {
|
||||
const cfg = (0, config_1.loadConfig)();
|
||||
if (!cfg.production.requireApiKey)
|
||||
return true;
|
||||
const key = req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key');
|
||||
if (key && key === process.env.OMNL_API_KEY)
|
||||
return true;
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return false;
|
||||
}
|
||||
const auth_1 = require("../../middleware/auth");
|
||||
async function respondPublicMoneySupply(res, officeId) {
|
||||
const cfg = (0, config_1.loadConfig)();
|
||||
const allow = cfg.production.onlineBankMode || process.env.OMNL_PUBLIC_MONEY_SUPPLY === '1';
|
||||
@@ -117,7 +108,7 @@ function createSettlementRouter() {
|
||||
router.get('/public/money-supply', (_req, res) => respondPublicMoneySupply(res, officeId));
|
||||
router.get('/money-supply/public', (_req, res) => respondPublicMoneySupply(res, officeId));
|
||||
router.get('/money-supply', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
try {
|
||||
res.json(await (0, external_transfer_1.getMoneySupply)(officeId));
|
||||
@@ -127,7 +118,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.get('/money-supply/:officeIdParam', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
const requested = parseInt(req.params.officeIdParam, 10);
|
||||
if (!Number.isFinite(requested)) {
|
||||
@@ -142,7 +133,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.get('/settlements', (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
res.json({
|
||||
officeId,
|
||||
@@ -150,7 +141,7 @@ function createSettlementRouter() {
|
||||
});
|
||||
});
|
||||
router.get('/settlements/:id', (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
const rec = (0, external_transfer_1.getSettlementStatus)(req.params.id);
|
||||
if (!rec) {
|
||||
@@ -160,7 +151,7 @@ function createSettlementRouter() {
|
||||
res.json(rec);
|
||||
});
|
||||
router.post('/external-transfer', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
try {
|
||||
const merged = (0, office_scope_1.bindOffice24)(req.body);
|
||||
@@ -174,7 +165,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/swift/inbound', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
try {
|
||||
const raw = typeof req.body === 'string' ? req.body : req.body?.raw;
|
||||
@@ -190,7 +181,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/trade-finance/:instrument', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
const instrument = req.params.instrument.toUpperCase();
|
||||
if (!['SBLC', 'DLC', 'BG', 'LS'].includes(instrument)) {
|
||||
@@ -217,7 +208,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/convert/fiat-to-crypto', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
try {
|
||||
const body = req.body;
|
||||
@@ -240,7 +231,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/token-load', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
try {
|
||||
const body = req.body;
|
||||
@@ -259,7 +250,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/transfer/internal', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
try {
|
||||
const body = req.body;
|
||||
@@ -280,7 +271,7 @@ function createSettlementRouter() {
|
||||
}
|
||||
});
|
||||
router.post('/transfer/external', async (req, res) => {
|
||||
if (!requireApiKey(req, res))
|
||||
if (!(0, auth_1.requireApiKey)(req, res))
|
||||
return;
|
||||
try {
|
||||
const body = req.body;
|
||||
|
||||
20
services/settlement-middleware/dist/middleware/auth.js
vendored
Normal file
20
services/settlement-middleware/dist/middleware/auth.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.requireApiKey = requireApiKey;
|
||||
const integration_foundation_1 = require("@dbis/integration-foundation");
|
||||
const config_1 = require("../config");
|
||||
function requireApiKey(req, res) {
|
||||
const cfg = (0, config_1.loadConfig)();
|
||||
if (!cfg.production.requireApiKey)
|
||||
return true;
|
||||
const principal = (0, integration_foundation_1.resolveOmnlPrincipal)(req);
|
||||
if (!principal) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return false;
|
||||
}
|
||||
if (principal.role === 'customer') {
|
||||
res.status(403).json({ error: 'Forbidden', hint: 'Settlement APIs require super-admin credentials' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
28
services/settlement-middleware/dist/middleware/rate-limit.js
vendored
Normal file
28
services/settlement-middleware/dist/middleware/rate-limit.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.settlementRateLimiter = void 0;
|
||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||
const integration_foundation_1 = require("@dbis/integration-foundation");
|
||||
function asHandler(h) {
|
||||
return h;
|
||||
}
|
||||
exports.settlementRateLimiter = asHandler((0, express_rate_limit_1.default)({
|
||||
windowMs: 60_000,
|
||||
max: (req) => {
|
||||
if (!(0, integration_foundation_1.isProductionSecurityMode)())
|
||||
return 200;
|
||||
const policy = (0, integration_foundation_1.loadCustomerSecurityPolicy)();
|
||||
const principal = (0, integration_foundation_1.resolveOmnlPrincipal)(req);
|
||||
if (!principal)
|
||||
return policy.rateLimits.anonymousPerMinute;
|
||||
if (principal.role === 'customer')
|
||||
return policy.rateLimits.customerPerMinute;
|
||||
return policy.rateLimits.superAdminPerMinute;
|
||||
},
|
||||
message: { error: 'Too many requests — rate limit exceeded' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
}));
|
||||
20
services/settlement-middleware/dist/server.js
vendored
20
services/settlement-middleware/dist/server.js
vendored
@@ -7,14 +7,30 @@ exports.createApp = createApp;
|
||||
exports.startServer = startServer;
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const integration_foundation_1 = require("@dbis/integration-foundation");
|
||||
const settlement_1 = require("./api/routes/settlement");
|
||||
const rate_limit_1 = require("./middleware/rate-limit");
|
||||
const config_1 = require("./config");
|
||||
function corsOptions() {
|
||||
const policy = (0, integration_foundation_1.loadCustomerSecurityPolicy)();
|
||||
const origins = policy.corsOrigins.filter(Boolean);
|
||||
if (!origins.length)
|
||||
return {};
|
||||
return {
|
||||
origin(origin, cb) {
|
||||
if (!origin || origins.includes(origin))
|
||||
cb(null, true);
|
||||
else
|
||||
cb(new Error('CORS not allowed'));
|
||||
},
|
||||
};
|
||||
}
|
||||
function createApp() {
|
||||
const app = (0, express_1.default)();
|
||||
app.use((0, cors_1.default)());
|
||||
app.use((0, cors_1.default)(corsOptions()));
|
||||
app.use(express_1.default.json({ limit: '2mb' }));
|
||||
app.use(express_1.default.text({ type: 'text/plain', limit: '2mb' }));
|
||||
app.use('/api/v1/settlement', (0, settlement_1.createSettlementRouter)());
|
||||
app.use('/api/v1/settlement', rate_limit_1.settlementRateLimiter, (0, settlement_1.createSettlementRouter)());
|
||||
return app;
|
||||
}
|
||||
function startServer() {
|
||||
|
||||
28
services/settlement-middleware/package-lock.json
generated
28
services/settlement-middleware/package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.5.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -649,6 +650,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
@@ -897,6 +916,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.5.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -13,15 +13,7 @@ import { bindOffice24 } from '../../workflows/office-scope';
|
||||
import { settlementStore } from '../../store/settlement-store';
|
||||
import { processSwiftInbound } from '../../workflows/swift-inbound';
|
||||
import { fetchGlBalances, fineractOfficeReachable } from '../../adapters/fineract';
|
||||
|
||||
function requireApiKey(req: Request, res: Response): boolean {
|
||||
const cfg = loadConfig();
|
||||
if (!cfg.production.requireApiKey) return true;
|
||||
const key = req.header('Authorization')?.replace(/^Bearer\s+/i, '') || req.header('X-API-Key');
|
||||
if (key && key === process.env.OMNL_API_KEY) return true;
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return false;
|
||||
}
|
||||
import { requireApiKey } from '../../middleware/auth';
|
||||
|
||||
async function respondPublicMoneySupply(res: Response, officeId: number): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
|
||||
18
services/settlement-middleware/src/middleware/auth.ts
Normal file
18
services/settlement-middleware/src/middleware/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import { resolveOmnlPrincipal } from '@dbis/integration-foundation';
|
||||
import { loadConfig } from '../config';
|
||||
|
||||
export function requireApiKey(req: Request, res: Response): boolean {
|
||||
const cfg = loadConfig();
|
||||
if (!cfg.production.requireApiKey) return true;
|
||||
const principal = resolveOmnlPrincipal(req);
|
||||
if (!principal) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return false;
|
||||
}
|
||||
if (principal.role === 'customer') {
|
||||
res.status(403).json({ error: 'Forbidden', hint: 'Settlement APIs require super-admin credentials' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
24
services/settlement-middleware/src/middleware/rate-limit.ts
Normal file
24
services/settlement-middleware/src/middleware/rate-limit.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import type { RequestHandler } from 'express';
|
||||
import { isProductionSecurityMode, loadCustomerSecurityPolicy, resolveOmnlPrincipal } from '@dbis/integration-foundation';
|
||||
|
||||
function asHandler(h: ReturnType<typeof rateLimit>): RequestHandler {
|
||||
return h as unknown as RequestHandler;
|
||||
}
|
||||
|
||||
export const settlementRateLimiter = asHandler(
|
||||
rateLimit({
|
||||
windowMs: 60_000,
|
||||
max: (req) => {
|
||||
if (!isProductionSecurityMode()) return 200;
|
||||
const policy = loadCustomerSecurityPolicy();
|
||||
const principal = resolveOmnlPrincipal(req);
|
||||
if (!principal) return policy.rateLimits.anonymousPerMinute;
|
||||
if (principal.role === 'customer') return policy.rateLimits.customerPerMinute;
|
||||
return policy.rateLimits.superAdminPerMinute;
|
||||
},
|
||||
message: { error: 'Too many requests — rate limit exceeded' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
}),
|
||||
);
|
||||
@@ -1,14 +1,28 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { loadCustomerSecurityPolicy } from '@dbis/integration-foundation';
|
||||
import { createSettlementRouter } from './api/routes/settlement';
|
||||
import { settlementRateLimiter } from './middleware/rate-limit';
|
||||
import { port } from './config';
|
||||
|
||||
function corsOptions(): cors.CorsOptions {
|
||||
const policy = loadCustomerSecurityPolicy();
|
||||
const origins = policy.corsOrigins.filter(Boolean);
|
||||
if (!origins.length) return {};
|
||||
return {
|
||||
origin(origin, cb) {
|
||||
if (!origin || origins.includes(origin)) cb(null, true);
|
||||
else cb(new Error('CORS not allowed'));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createApp() {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(cors(corsOptions()));
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(express.text({ type: 'text/plain', limit: '2mb' }));
|
||||
app.use('/api/v1/settlement', createSettlementRouter());
|
||||
app.use('/api/v1/settlement', settlementRateLimiter, createSettlementRouter());
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { resolveOmnlPrincipal } from '@dbis/integration-foundation';
|
||||
|
||||
function extractBearerOrQuery(req: Request, key: string): boolean {
|
||||
const auth = String(req.headers.authorization || '');
|
||||
@@ -9,6 +10,8 @@ function extractBearerOrQuery(req: Request, key: string): boolean {
|
||||
}
|
||||
|
||||
function hasOmnlOperatorAuth(req: Request): boolean {
|
||||
const principal = resolveOmnlPrincipal(req);
|
||||
if (principal && principal.role !== 'customer') return true;
|
||||
const apiKey = process.env.OMNL_API_KEY?.trim();
|
||||
if (apiKey && extractBearerOrQuery(req, apiKey)) return true;
|
||||
const dashKey = process.env.OMNL_DASHBOARD_TOKEN?.trim();
|
||||
@@ -69,18 +72,14 @@ export function omnlRequireApiKeyInProduction(req: Request, res: Response, next:
|
||||
return;
|
||||
}
|
||||
const key = process.env.OMNL_API_KEY?.trim();
|
||||
if (!key) {
|
||||
if (!key && !process.env.OMNL_SUPER_ADMIN_OFFICE24_KEY) {
|
||||
res.status(503).json({
|
||||
error: 'OMNL_API_KEY required in production',
|
||||
hint: 'Set OMNL_API_KEY and pass Authorization: Bearer <key>',
|
||||
error: 'OMNL super-admin API keys required in production',
|
||||
hint: 'Run scripts/deployment/seed-omnl-super-admin-keys.sh',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (extractBearerOrQuery(req, key)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (isComplianceConsolePath(req.path) && hasOmnlOperatorAuth(req)) {
|
||||
if (hasOmnlOperatorAuth(req)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user