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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 05:09:08 -07:00
parent 84f12c7edc
commit 064239be13
39 changed files with 1094 additions and 99 deletions

View File

@@ -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);
}

View 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');
}

View 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,
}));

View File

@@ -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`);

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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({

View 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');
}

View 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,
}),
);

View File

@@ -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`);

View File

@@ -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;

View 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;
}

View 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,
}));

View File

@@ -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() {

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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();

View 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;
}

View 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,
}),
);

View File

@@ -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;
}

View File

@@ -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;
}