feat(omnl): settlement terminal, compliance gates, and HYBX integration foundation
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m12s
CI/CD Pipeline / Security Scanning (push) Successful in 2m21s
CI/CD Pipeline / Lint and Format (push) Failing after 36s
CI/CD Pipeline / Terraform Validation (push) Failing after 22s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 25s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 23s
Validation / validate-genesis (push) Successful in 26s
Validation / validate-terraform (push) Failing after 21s
Validation / validate-kubernetes (push) Failing after 9s
Validation / validate-smart-contracts (push) Failing after 8s
Validation / validate-security (push) Failing after 1m15s
Validation / validate-documentation (push) Failing after 15s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 26s
Verify Deployment / Verify Deployment (push) Failing after 56s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m12s
CI/CD Pipeline / Security Scanning (push) Successful in 2m21s
CI/CD Pipeline / Lint and Format (push) Failing after 36s
CI/CD Pipeline / Terraform Validation (push) Failing after 22s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 25s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 23s
Validation / validate-genesis (push) Successful in 26s
Validation / validate-terraform (push) Failing after 21s
Validation / validate-kubernetes (push) Failing after 9s
Validation / validate-smart-contracts (push) Failing after 8s
Validation / validate-security (push) Failing after 1m15s
Validation / validate-documentation (push) Failing after 15s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 26s
Verify Deployment / Verify Deployment (push) Failing after 56s
Add operator settlement terminal UI/API, swift-listener service, compliance idempotency gates, GRU reserve dashboards, and @dbis/integration-foundation for typed HYBX/ISO adapter contracts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createCorrelationContext, CORRELATION_ID_HEADER, REQUEST_ID_HEADER } from '@dbis/integration-foundation';
|
||||
import { appendOmnlAudit } from '../../services/omnl-audit-log';
|
||||
|
||||
/** Attach trace id and log every /omnl request (response status on finish). */
|
||||
/** Attach trace/correlation ids and log every /omnl request (response status on finish). */
|
||||
export function omnlAuditMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const traceId = (req.headers['x-omnl-trace-id'] as string) || randomUUID();
|
||||
const ctx = createCorrelationContext({
|
||||
correlationId: String(req.headers[CORRELATION_ID_HEADER] ?? req.headers['x-omnl-trace-id'] ?? ''),
|
||||
requestId: String(req.headers[REQUEST_ID_HEADER] ?? ''),
|
||||
});
|
||||
const traceId = ctx.correlationId;
|
||||
res.setHeader('X-OMNL-Trace-Id', traceId);
|
||||
res.setHeader(CORRELATION_ID_HEADER, ctx.correlationId);
|
||||
res.setHeader(REQUEST_ID_HEADER, ctx.requestId);
|
||||
const started = Date.now();
|
||||
res.on('finish', () => {
|
||||
appendOmnlAudit({
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { MockComplianceDecisionEngine } from '@dbis/integration-foundation';
|
||||
|
||||
const engine = new MockComplianceDecisionEngine();
|
||||
|
||||
const FINANCIAL_POST_PATHS = [
|
||||
'/omnl/iso20022/messages',
|
||||
];
|
||||
|
||||
/**
|
||||
* Optional compliance decision gate for financial POST routes.
|
||||
* Enable with OMNL_COMPLIANCE_GATE=1 (uses mock engine until provider integration).
|
||||
*/
|
||||
export async function omnlComplianceGateMiddleware(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
if (process.env.OMNL_COMPLIANCE_GATE !== '1') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (req.method !== 'POST') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const path = req.path.replace(/^\/api\/v1/, '');
|
||||
if (!FINANCIAL_POST_PATHS.some((p) => path.endsWith(p))) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const body = req.body as {
|
||||
messageType?: string;
|
||||
payload?: string;
|
||||
entityId?: string;
|
||||
tenantId?: string;
|
||||
amount?: string;
|
||||
currency?: string;
|
||||
riskHints?: string[];
|
||||
};
|
||||
|
||||
const result = await engine.evaluateTransaction({
|
||||
entityId: body.entityId ?? 'omnl-default',
|
||||
tenantId: body.tenantId ?? 'omnl',
|
||||
amount: body.amount ?? '0',
|
||||
currency: body.currency ?? 'USD',
|
||||
transactionType: body.messageType ?? 'iso20022.store',
|
||||
riskHints: body.riskHints,
|
||||
});
|
||||
|
||||
res.setHeader('X-OMNL-Compliance-Decision', result.decision);
|
||||
res.setHeader('X-OMNL-Audit-Event-Id', result.audit_event_id);
|
||||
|
||||
if (result.decision === 'block' || result.decision === 'reject') {
|
||||
res.status(403).json({
|
||||
error: 'compliance_blocked',
|
||||
decision: result.decision,
|
||||
reason_codes: result.reason_codes,
|
||||
audit_event_id: result.audit_event_id,
|
||||
manual_review_required: result.manual_review_required,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.decision === 'hold' || result.decision === 'review') {
|
||||
res.status(202).json({
|
||||
status: 'pending_review',
|
||||
decision: result.decision,
|
||||
reason_codes: result.reason_codes,
|
||||
audit_event_id: result.audit_event_id,
|
||||
manual_review_required: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
(req as Request & { complianceAuditId?: string }).complianceAuditId = result.audit_event_id;
|
||||
next();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import { omnlComplianceGateMiddleware } from './omnl-compliance-gate';
|
||||
import { omnlIdempotencyMiddleware } from './omnl-idempotency';
|
||||
|
||||
function mockRes(): Response & { statusCode?: number; body?: unknown; headers: Record<string, string> } {
|
||||
const headers: Record<string, string> = {};
|
||||
const res = {
|
||||
headers,
|
||||
statusCode: 200,
|
||||
body: undefined as unknown,
|
||||
setHeader(name: string, value: string) {
|
||||
headers[name.toLowerCase()] = value;
|
||||
},
|
||||
status(code: number) {
|
||||
this.statusCode = code;
|
||||
return this;
|
||||
},
|
||||
json(payload: unknown) {
|
||||
this.body = payload;
|
||||
return this;
|
||||
},
|
||||
};
|
||||
return res as Response & typeof res;
|
||||
}
|
||||
|
||||
function mockReq(overrides: Partial<Request> & { path?: string; method?: string } = {}): Request {
|
||||
return {
|
||||
method: 'POST',
|
||||
path: '/api/v1/omnl/iso20022/messages',
|
||||
headers: {},
|
||||
body: { messageType: 'pacs.008', payload: '<xml/>' },
|
||||
...overrides,
|
||||
} as Request;
|
||||
}
|
||||
|
||||
describe('omnlComplianceGateMiddleware', () => {
|
||||
const oldGate = process.env.OMNL_COMPLIANCE_GATE;
|
||||
|
||||
afterEach(() => {
|
||||
if (oldGate === undefined) delete process.env.OMNL_COMPLIANCE_GATE;
|
||||
else process.env.OMNL_COMPLIANCE_GATE = oldGate;
|
||||
});
|
||||
|
||||
it('passes through when OMNL_COMPLIANCE_GATE is unset', async () => {
|
||||
delete process.env.OMNL_COMPLIANCE_GATE;
|
||||
const next = jest.fn();
|
||||
await omnlComplianceGateMiddleware(mockReq(), mockRes(), next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets compliance headers and passes when gate enabled', async () => {
|
||||
process.env.OMNL_COMPLIANCE_GATE = '1';
|
||||
const next = jest.fn();
|
||||
const res = mockRes();
|
||||
await omnlComplianceGateMiddleware(mockReq(), res, next);
|
||||
expect(res.headers['x-omnl-compliance-decision']).toBeDefined();
|
||||
expect(res.headers['x-omnl-audit-event-id']).toBeDefined();
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips non-POST methods', async () => {
|
||||
process.env.OMNL_COMPLIANCE_GATE = '1';
|
||||
const next = jest.fn();
|
||||
await omnlComplianceGateMiddleware(mockReq({ method: 'GET' }), mockRes(), next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('omnlIdempotencyMiddleware', () => {
|
||||
it('passes when X-Idempotency-Key is absent', () => {
|
||||
const next = jest.fn();
|
||||
omnlIdempotencyMiddleware(mockReq(), mockRes(), next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns 409 on duplicate idempotency key', () => {
|
||||
const req = mockReq({
|
||||
headers: { 'x-idempotency-key': 'idem-test-duplicate-key' },
|
||||
});
|
||||
const next = jest.fn();
|
||||
omnlIdempotencyMiddleware(req, mockRes(), next);
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
|
||||
const res = mockRes();
|
||||
const next2 = jest.fn();
|
||||
omnlIdempotencyMiddleware(req, res, next2);
|
||||
expect(res.statusCode).toBe(409);
|
||||
expect(next2).not.toHaveBeenCalled();
|
||||
expect(res.body).toMatchObject({ error: 'duplicate_idempotency_key' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { deriveIdempotencyKey, IdempotencyStore } from '@dbis/integration-foundation';
|
||||
|
||||
const store = new IdempotencyStore();
|
||||
|
||||
const IDEMPOTENT_PATHS = ['/omnl/iso20022/messages'];
|
||||
|
||||
/**
|
||||
* Dedup financial POSTs when X-Idempotency-Key header is present.
|
||||
*/
|
||||
export function omnlIdempotencyMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
if (req.method !== 'POST') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const path = req.path.replace(/^\/api\/v1/, '');
|
||||
if (!IDEMPOTENT_PATHS.some((p) => path.endsWith(p))) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const headerKey = String(req.headers['x-idempotency-key'] ?? '').trim();
|
||||
if (!headerKey) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const key = deriveIdempotencyKey({
|
||||
tenantId: String(req.headers['x-tenant-id'] ?? 'omnl'),
|
||||
entityId: String(req.headers['x-entity-id'] ?? 'default'),
|
||||
operation: path,
|
||||
resourceId: headerKey,
|
||||
});
|
||||
|
||||
if (!store.record(key)) {
|
||||
res.status(409).json({ error: 'duplicate_idempotency_key', idempotencyKey: headerKey });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { Router, Request, Response } from 'express';
|
||||
import { omnlRateLimiter } from '../middleware/rate-limit';
|
||||
import { omnlSensitiveRouteGuard, omnlRequireApiKeyInProduction } from '../middleware/omnl-guards';
|
||||
import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
|
||||
import { omnlComplianceGateMiddleware } from '../middleware/omnl-compliance-gate';
|
||||
import { omnlIdempotencyMiddleware } from '../middleware/omnl-idempotency';
|
||||
import { runTripleStateReconcile } from '../../services/omnl-triple-reconcile';
|
||||
import {
|
||||
buildIfrs7Disclosure,
|
||||
@@ -31,6 +33,8 @@ const router = Router();
|
||||
router.use(omnlRateLimiter);
|
||||
router.use(omnlAuditMiddleware);
|
||||
router.use(omnlRequireApiKeyInProduction);
|
||||
router.use(omnlIdempotencyMiddleware);
|
||||
router.use(omnlComplianceGateMiddleware);
|
||||
|
||||
router.get('/omnl/reconcile/triple-state', omnlSensitiveRouteGuard, async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { omnlRateLimiter } from '../middleware/rate-limit';
|
||||
import { omnlSensitiveRouteGuard, omnlRequireApiKeyInProduction } from '../middleware/omnl-guards';
|
||||
import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
|
||||
import {
|
||||
buildSettlementTerminalContext,
|
||||
buildFullTerminalPackage,
|
||||
getTerminalArtifact,
|
||||
renderTerminalScreen,
|
||||
type TerminalArtifact,
|
||||
type TerminalScreenMode,
|
||||
} from '../../services/omnl-settlement-terminal';
|
||||
import {
|
||||
listTerminalConnectionIds,
|
||||
loadTerminalConnection,
|
||||
buildConnectionPublicSummary,
|
||||
connectionAlchemyApiKey,
|
||||
} from '../../services/omnl-terminal-connection';
|
||||
import {
|
||||
alchemyJsonRpc,
|
||||
alchemyTokenPricesByAddress,
|
||||
alchemyPrepareErc20Transfer,
|
||||
alchemyConfigured,
|
||||
type AlchemyNetwork,
|
||||
} from '../../services/omnl-alchemy-client';
|
||||
import { appendOmnlAudit } from '../../services/omnl-audit-log';
|
||||
|
||||
const router = Router();
|
||||
router.use(omnlRateLimiter);
|
||||
router.use(omnlAuditMiddleware);
|
||||
router.use(omnlRequireApiKeyInProduction);
|
||||
|
||||
function parseQueryContext(req: Request): {
|
||||
settlementRef?: string;
|
||||
officeId?: number;
|
||||
valueDate?: string;
|
||||
amountUsd?: string;
|
||||
connectionId?: string;
|
||||
} {
|
||||
const officeRaw = String(req.query.officeId ?? '').trim();
|
||||
return {
|
||||
settlementRef: String(req.query.settlementRef ?? '').trim() || undefined,
|
||||
officeId: officeRaw ? parseInt(officeRaw, 10) : undefined,
|
||||
valueDate: String(req.query.valueDate ?? '').trim() || undefined,
|
||||
amountUsd: String(req.query.amountUsd ?? '').trim() || undefined,
|
||||
connectionId: String(req.query.connectionId ?? '').trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAlchemyKey(connectionId?: string): string | undefined {
|
||||
if (!connectionId) return undefined;
|
||||
try {
|
||||
const profile = loadTerminalConnection(connectionId);
|
||||
const key = connectionAlchemyApiKey(profile);
|
||||
return key || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function parseArtifact(raw: string): TerminalArtifact {
|
||||
const v = raw.trim().toLowerCase();
|
||||
const allowed: TerminalArtifact[] = [
|
||||
'package',
|
||||
'proof-of-funds',
|
||||
'debit-note',
|
||||
'remittance-advice',
|
||||
'tokenization-sanitized',
|
||||
'conversion-swift-copy',
|
||||
];
|
||||
return (allowed.includes(v as TerminalArtifact) ? v : 'package') as TerminalArtifact;
|
||||
}
|
||||
|
||||
async function withContext(
|
||||
req: Request,
|
||||
res: Response,
|
||||
fn: (ctx: Awaited<ReturnType<typeof buildSettlementTerminalContext>>) => void | Promise<void>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const ctx = await buildSettlementTerminalContext(parseQueryContext(req));
|
||||
await fn(ctx);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
res.status(500).json({ error: msg });
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/omnl/terminal/status', omnlSensitiveRouteGuard, (req, res) => {
|
||||
const connectionId = String(req.query.connectionId ?? '').trim() || undefined;
|
||||
res.json({
|
||||
service: 'omnl-settlement-terminal',
|
||||
version: '1.1.0',
|
||||
alchemyConfigured: alchemyConfigured(resolveAlchemyKey(connectionId)),
|
||||
fineractEnv: Boolean(process.env.OMNL_FINERACT_BASE_URL && process.env.OMNL_FINERACT_PASSWORD),
|
||||
ui: '/omnl/terminal',
|
||||
connections: listTerminalConnectionIds(),
|
||||
activeConnectionId: connectionId,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/connections', omnlSensitiveRouteGuard, (_req, res) => {
|
||||
const ids = listTerminalConnectionIds();
|
||||
res.json({
|
||||
connections: ids.map((id) => {
|
||||
try {
|
||||
return buildConnectionPublicSummary(loadTerminalConnection(id));
|
||||
} catch (e) {
|
||||
return { connectionId: id, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/connections/:connectionId', omnlSensitiveRouteGuard, (req, res) => {
|
||||
try {
|
||||
const profile = loadTerminalConnection(String(req.params.connectionId));
|
||||
res.json(buildConnectionPublicSummary(profile));
|
||||
} catch (e) {
|
||||
res.status(404).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/package', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
await withContext(req, res, (ctx) => {
|
||||
res.json(buildFullTerminalPackage(ctx));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/proof-of-funds', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
await withContext(req, res, (ctx) => {
|
||||
res.json(getTerminalArtifact(ctx, 'proof-of-funds'));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/debit-note', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
await withContext(req, res, (ctx) => {
|
||||
res.json(getTerminalArtifact(ctx, 'debit-note'));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/remittance-advice', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
await withContext(req, res, (ctx) => {
|
||||
res.json(getTerminalArtifact(ctx, 'remittance-advice'));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/tokenization-sanitized', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
await withContext(req, res, (ctx) => {
|
||||
res.json(getTerminalArtifact(ctx, 'tokenization-sanitized'));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/conversion-swift-copy', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
await withContext(req, res, (ctx) => {
|
||||
res.json(getTerminalArtifact(ctx, 'conversion-swift-copy'));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/omnl/terminal/screen', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
await withContext(req, res, (ctx) => {
|
||||
const modeRaw = String(req.query.mode ?? 'black').trim().toLowerCase();
|
||||
const mode: TerminalScreenMode = modeRaw === 'color' ? 'color' : 'black';
|
||||
const artifact = parseArtifact(String(req.query.artifact ?? 'package'));
|
||||
const text = renderTerminalScreen(ctx, mode, artifact);
|
||||
const format = String(req.query.format ?? 'json').trim().toLowerCase();
|
||||
if (format === 'text' || format === 'plain') {
|
||||
res.type('text/plain; charset=utf-8').send(text);
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
mode,
|
||||
artifact,
|
||||
text,
|
||||
context: ctx,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/omnl/terminal/alchemy/rpc', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
try {
|
||||
const network = (String(req.body?.network ?? 'eth-mainnet').trim() ||
|
||||
'eth-mainnet') as AlchemyNetwork;
|
||||
const connectionId = String(req.body?.connectionId ?? req.query.connectionId ?? '').trim();
|
||||
const apiKey = resolveAlchemyKey(connectionId || undefined);
|
||||
if (!alchemyConfigured(apiKey)) {
|
||||
res.status(503).json({ error: 'Alchemy API key unset for connection' });
|
||||
return;
|
||||
}
|
||||
const method = String(req.body?.method ?? '').trim();
|
||||
const params = Array.isArray(req.body?.params) ? req.body.params : [];
|
||||
if (!method) {
|
||||
res.status(400).json({ error: 'JSON body must include method' });
|
||||
return;
|
||||
}
|
||||
const result = await alchemyJsonRpc(network, method, params, apiKey);
|
||||
appendOmnlAudit({
|
||||
category: 'api',
|
||||
action: 'alchemy_rpc',
|
||||
metadata: { network, method },
|
||||
status: 'ok',
|
||||
});
|
||||
res.json({ network, method, result });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/omnl/terminal/alchemy/prices', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
try {
|
||||
const connectionId = String(req.body?.connectionId ?? req.query.connectionId ?? '').trim();
|
||||
const apiKey = resolveAlchemyKey(connectionId || undefined);
|
||||
if (!alchemyConfigured(apiKey)) {
|
||||
res.status(503).json({ error: 'Alchemy API key unset for connection' });
|
||||
return;
|
||||
}
|
||||
const network = (String(req.body?.network ?? 'eth-mainnet').trim() ||
|
||||
'eth-mainnet') as AlchemyNetwork;
|
||||
const addresses = Array.isArray(req.body?.addresses) ? req.body.addresses.map(String) : [];
|
||||
if (!addresses.length) {
|
||||
res.status(400).json({ error: 'JSON body must include addresses: string[]' });
|
||||
return;
|
||||
}
|
||||
const data = await alchemyTokenPricesByAddress(network, addresses, apiKey);
|
||||
appendOmnlAudit({
|
||||
category: 'api',
|
||||
action: 'alchemy_prices',
|
||||
metadata: { network, count: addresses.length },
|
||||
status: 'ok',
|
||||
});
|
||||
res.json({ network, data });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/omnl/terminal/alchemy/tx-prep', omnlSensitiveRouteGuard, async (req, res) => {
|
||||
try {
|
||||
const connectionId = String(req.body?.connectionId ?? req.query.connectionId ?? '').trim();
|
||||
const apiKey = resolveAlchemyKey(connectionId || undefined);
|
||||
if (!alchemyConfigured(apiKey)) {
|
||||
res.status(503).json({ error: 'Alchemy API key unset for connection' });
|
||||
return;
|
||||
}
|
||||
const network = (String(req.body?.network ?? 'eth-mainnet').trim() ||
|
||||
'eth-mainnet') as AlchemyNetwork;
|
||||
const from = String(req.body?.from ?? '').trim();
|
||||
const to = String(req.body?.to ?? '').trim();
|
||||
const tokenAddress = String(req.body?.tokenAddress ?? '').trim();
|
||||
const amountHuman = String(req.body?.amount ?? req.body?.amountHuman ?? '').trim();
|
||||
const dryRun = req.body?.dryRun !== false;
|
||||
if (!from || !to || !tokenAddress || !amountHuman) {
|
||||
res.status(400).json({
|
||||
error: 'Required: from, to, tokenAddress, amount (human-readable token units)',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const prep = await alchemyPrepareErc20Transfer({
|
||||
network,
|
||||
from,
|
||||
to,
|
||||
tokenAddress,
|
||||
amountHuman,
|
||||
dryRun,
|
||||
apiKeyOverride: apiKey,
|
||||
});
|
||||
appendOmnlAudit({
|
||||
category: 'api',
|
||||
action: 'alchemy_tx_prep',
|
||||
metadata: { network, from, to, tokenAddress, dryRun },
|
||||
status: 'ok',
|
||||
});
|
||||
res.json(prep);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { cacheMiddleware } from '../middleware/cache';
|
||||
import {
|
||||
buildProofIndex,
|
||||
getLatestReserveCapacitySnapshot,
|
||||
refreshReserveCapacityCache,
|
||||
} from '../../services/gru-reserve-capacity';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /reserve/capacity — semi-public live reserve capacity + multi-point proof quorum.
|
||||
* Public (no OMNL API key). Cached 10s; ?refresh=1 forces recompute when live scheduler enabled.
|
||||
*/
|
||||
router.get('/reserve/capacity', cacheMiddleware(10 * 1000), async (req: Request, res: Response) => {
|
||||
try {
|
||||
const forceRefresh = req.query.refresh === '1';
|
||||
let snapshot = getLatestReserveCapacitySnapshot();
|
||||
const maxAgeSec = snapshot?.sla?.freshnessMaxAgeSeconds ?? 60;
|
||||
const stale =
|
||||
!snapshot ||
|
||||
(Date.now() - new Date(snapshot.generatedAt).getTime()) / 1000 > maxAgeSec;
|
||||
|
||||
if (forceRefresh || stale) {
|
||||
snapshot = await refreshReserveCapacityCache(
|
||||
String(req.query.set || process.env.GRU_RESERVE_CAPACITY_MINIMUM_SET || 'reserveCapacityLive'),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot) {
|
||||
res.status(503).json({
|
||||
error: 'Reserve capacity snapshot unavailable',
|
||||
hint: 'Enable GRU_RESERVE_CAPACITY_LIVE=1 or run pnpm gru:reserve-capacity:collect',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('Cache-Control', 'public, max-age=10, stale-while-revalidate=30');
|
||||
res.setHeader('X-GRU-Proof-Quorum', snapshot.proofQuorum.passed ? 'pass' : 'fail');
|
||||
res.status(200).json(snapshot);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
res.status(500).json({ error: msg });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /reserve/proof-index — status of all configured proof points (P1–P7).
|
||||
*/
|
||||
router.get('/reserve/proof-index', cacheMiddleware(15 * 1000), (_req: Request, res: Response) => {
|
||||
res.setHeader('Cache-Control', 'public, max-age=15');
|
||||
res.json(buildProofIndex());
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -21,7 +21,9 @@ import plannerV2Routes from './routes/planner-v2';
|
||||
import omnlRoutes from './routes/omnl';
|
||||
import omnlIpsasRoutes from './routes/omnl-ipsas';
|
||||
import omnlComplianceRoutes from './routes/omnl-compliance-routes';
|
||||
import omnlTerminalRoutes from './routes/omnl-terminal-routes';
|
||||
import checkpointRoutes from './routes/checkpoint';
|
||||
import reserveCapacityRoutes from './routes/reserve-capacity';
|
||||
import { MultiChainIndexer } from '../indexer/chain-indexer';
|
||||
import { OmnlEventPoller } from '../indexer/omnl-event-poller';
|
||||
import { getDatabasePool } from '../database/client';
|
||||
@@ -120,12 +122,25 @@ export class ApiServer {
|
||||
} as const;
|
||||
this.app.use('/static', express.static(publicPath, staticOpts));
|
||||
this.app.use('/omnl/static', express.static(publicPath, staticOpts));
|
||||
this.app.use('/reserve/static', express.static(publicPath, staticOpts));
|
||||
}
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// Health check
|
||||
this.app.get('/health', async (req: Request, res: Response) => {
|
||||
if (process.env.OMNL_SETTLEMENT_TERMINAL_ONLY === '1') {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
mode: 'omnl-settlement-terminal-only',
|
||||
timestamp: new Date().toISOString(),
|
||||
services: {
|
||||
database: 'skipped',
|
||||
indexer: 'disabled',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Check database connection
|
||||
const pool = getDatabasePool();
|
||||
@@ -150,6 +165,9 @@ export class ApiServer {
|
||||
|
||||
const dashboardPath = path.join(__dirname, '../../public/omnl-dashboard.html');
|
||||
const complianceConsolePath = path.join(__dirname, '../../public/omnl-compliance-console.html');
|
||||
const settlementTerminalPath = path.join(__dirname, '../../public/omnl-settlement-terminal.html');
|
||||
const reserveDashboardPath = path.join(__dirname, '../../public/reserve-dashboard.html');
|
||||
const reserveInstitutionalPath = path.join(__dirname, '../../public/reserve-institutional.html');
|
||||
|
||||
const authorizeOmnlHtml = (req: Request, res: Response): boolean => {
|
||||
const tok = process.env.OMNL_DASHBOARD_TOKEN?.trim();
|
||||
@@ -181,6 +199,34 @@ export class ApiServer {
|
||||
res.type('html').send(readFileSync(dashboardPath, 'utf8'));
|
||||
});
|
||||
|
||||
this.app.get('/omnl/terminal', (req: Request, res: Response) => {
|
||||
if (!authorizeOmnlHtml(req, res)) return;
|
||||
if (!existsSync(settlementTerminalPath)) {
|
||||
res.status(404).type('text/plain').send('omnl-settlement-terminal.html missing');
|
||||
return;
|
||||
}
|
||||
res.type('html').send(readFileSync(settlementTerminalPath, 'utf8'));
|
||||
});
|
||||
|
||||
/** Public semi-public reserve capacity dashboard (no auth). */
|
||||
this.app.get('/reserve', (_req: Request, res: Response) => {
|
||||
if (!existsSync(reserveDashboardPath)) {
|
||||
res.status(404).type('text/plain').send('reserve-dashboard.html missing');
|
||||
return;
|
||||
}
|
||||
res.type('html').send(readFileSync(reserveDashboardPath, 'utf8'));
|
||||
});
|
||||
|
||||
/** Institutional wrapper (same API; used by reserve.d-bis.org NPM upstream). */
|
||||
this.app.get('/reserve/institutional', (_req: Request, res: Response) => {
|
||||
const file = existsSync(reserveInstitutionalPath) ? reserveInstitutionalPath : reserveDashboardPath;
|
||||
if (!existsSync(file)) {
|
||||
res.status(404).type('text/plain').send('reserve dashboard missing');
|
||||
return;
|
||||
}
|
||||
res.type('html').send(readFileSync(file, 'utf8'));
|
||||
});
|
||||
|
||||
// Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts)
|
||||
const sendApiV1Catalog = (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
@@ -209,6 +255,9 @@ export class ApiServer {
|
||||
plannerRoutesPlan: '/api/v2/routes/plan',
|
||||
plannerIntentsPlan: '/api/v2/intents/plan',
|
||||
plannerInternalExecutionPlan: '/api/v2/routes/internal-execution-plan',
|
||||
reserveCapacity: '/api/v1/reserve/capacity',
|
||||
reserveProofIndex: '/api/v1/reserve/proof-index',
|
||||
reserveDashboard: '/reserve',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -228,7 +277,9 @@ export class ApiServer {
|
||||
this.app.use('/api/v1', aggregatorRouteMatrixRoutes);
|
||||
this.app.use('/api/v1', partnerPayloadRoutes);
|
||||
this.app.use('/api/v1', checkpointRoutes);
|
||||
this.app.use('/api/v1', reserveCapacityRoutes);
|
||||
this.app.use('/api/v1', omnlComplianceRoutes);
|
||||
this.app.use('/api/v1', omnlTerminalRoutes);
|
||||
this.app.use('/api/v1', omnlRoutes);
|
||||
this.app.use('/api/v1', omnlIpsasRoutes);
|
||||
this.app.use('/api/v2', plannerV2Routes);
|
||||
@@ -248,6 +299,7 @@ export class ApiServer {
|
||||
omnlOpenApi: '/api/v1/omnl/openapi.json',
|
||||
omnlCatalog: '/api/v1/omnl/catalog',
|
||||
omnlDashboard: '/omnl/dashboard',
|
||||
reserveDashboard: '/reserve',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user