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

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:
defiQUG
2026-06-07 21:51:32 -07:00
parent b6ddd236e2
commit d717b504a6
182 changed files with 10852 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (P1P7).
*/
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;

View File

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

View File

@@ -5,6 +5,7 @@ import { ApiServer } from './api/server';
import { closeDatabasePool } from './database/client';
import { logger } from './utils/logger';
import { startRouteMatrixScheduler } from './services/route-matrix-scheduler';
import { startGruReserveCapacityScheduler } from './services/gru-reserve-capacity-scheduler';
// Load smom-dbis-138 root .env first (single source); works from dist/ or src/
const rootEnvCandidates = [
@@ -28,6 +29,7 @@ try {
const server = new ApiServer();
startRouteMatrixScheduler();
startGruReserveCapacityScheduler();
// Start server
server.start().catch((error) => {

View File

@@ -0,0 +1,48 @@
import { logger } from '../utils/logger';
import { refreshReserveCapacityCache } from './gru-reserve-capacity';
let intervalHandle: ReturnType<typeof setInterval> | null = null;
export function startGruReserveCapacityScheduler(): void {
const enabled =
String(process.env.GRU_RESERVE_CAPACITY_LIVE || '0').toLowerCase() === '1' ||
String(process.env.GRU_RESERVE_CAPACITY_LIVE || '').toLowerCase() === 'true';
if (!enabled) {
return;
}
const intervalMs = Math.max(
15_000,
parseInt(process.env.GRU_RESERVE_CAPACITY_INTERVAL_MS || '30000', 10) || 30_000,
);
const minimumSet = process.env.GRU_RESERVE_CAPACITY_MINIMUM_SET?.trim() || 'reserveCapacityLive';
const tick = async () => {
try {
const snap = await refreshReserveCapacityCache(minimumSet);
logger.info('GRU reserve capacity refreshed', {
quorumPassed: snap.proofQuorum.passed,
passCount: snap.proofQuorum.passCount,
issuanceBlocked: snap.capacity.issuanceBlocked,
});
} catch (error) {
logger.warn('GRU reserve capacity refresh failed', {
error: error instanceof Error ? error.message : String(error),
});
}
};
void tick();
intervalHandle = setInterval(() => {
void tick();
}, intervalMs);
logger.info('GRU reserve capacity live scheduler enabled', { intervalMs, minimumSet });
}
export function stopGruReserveCapacityScheduler(): void {
if (intervalHandle) {
clearInterval(intervalHandle);
intervalHandle = null;
}
}

View File

@@ -0,0 +1,414 @@
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
import { resolve, dirname } from 'path';
import { Contract, JsonRpcProvider } from 'ethers';
import { runTripleStateReconcile } from './omnl-triple-reconcile';
export interface ProofPointStatus {
id: string;
name: string;
status: 'pass' | 'fail' | 'skip' | 'stale';
detail: string;
realTime?: boolean;
}
export interface ReserveCapacitySnapshot {
schemaVersion: '1.0.0';
generatedAt: string;
configRef: string;
minimumSet: string;
sla: {
availabilityTargetPercent: number;
freshnessMaxAgeSeconds: number;
agreementToleranceBps: number;
};
capacity: {
reserveUsd: number | null;
m1Usd: number | null;
m00Usd: number | null;
coverageRatioBps: number | null;
issuanceBlocked: boolean | null;
issuanceBlockReasons: string[];
velocityZone: string | null;
};
tripleState: {
aligned: boolean | null;
breaksCount: number | null;
generatedAt: string | null;
};
onChainGate: {
address: string | null;
metricsUpdatedAt: number | null;
coverageRatioBps: number | null;
m1ToM00Utilization: number | null;
issuancePaused: boolean | null;
rpcReachable: boolean;
};
proofQuorum: {
passed: boolean;
passCount: number;
requiredCount: number;
minimumRequired: number;
points: Record<string, ProofPointStatus>;
};
semiPublic: true;
redactedFields: string[];
reproduce: {
commands: string[];
configPaths: string[];
};
}
interface MultiPointProofConfig {
minimumIndependentProofPoints: number;
sla99999?: {
freshness?: { maxAgeSeconds?: number };
agreement?: { toleranceBps?: number };
availability?: { targetPercent?: number };
};
minimumSets?: Record<
string,
{ requiredProofPointIds?: string[]; description?: string }
>;
proofPoints?: Array<{ id: string; name: string; realTime?: boolean }>;
}
interface GruMetricsFile {
generatedAt?: string;
guardrails?: {
coverageRatioBps?: number;
coverageOk?: boolean;
coverageMinBps?: number;
issuanceBlocked?: boolean;
issuanceBlockReasons?: string[];
};
classical?: { reserveRatioBps?: number };
gruLayers?: {
M00_liNotionalUsd?: number;
M1_cStarUsd?: number;
monetaryBaseUsd?: number;
};
physicalReserve?: { usdValue?: number };
gruVelocity?: { zone?: string };
onChainGate?: {
gateAddress?: string | null;
envVar?: string;
metricsForUpdate?: {
coverageRatioBps?: number;
m1ToM00Utilization?: number;
issuancePaused?: boolean;
};
};
}
let cachedSnapshot: ReserveCapacitySnapshot | null = null;
let cachedAt = 0;
function projectRoot(): string {
return (
process.env.PROXMOX_ROOT?.trim() ||
process.env.PHOENIX_REPO_ROOT?.trim() ||
resolve(__dirname, '../../../../../..')
);
}
function readOptionalJson<T>(rel: string): T | null {
const p = resolve(projectRoot(), rel);
if (!existsSync(p)) return null;
try {
return JSON.parse(readFileSync(p, 'utf8')) as T;
} catch {
return null;
}
}
function ageSeconds(iso?: string | null): number {
if (!iso) return Infinity;
return (Date.now() - new Date(iso).getTime()) / 1000;
}
function loadProofConfig(): MultiPointProofConfig | null {
return readOptionalJson<MultiPointProofConfig>('config/compliance/gru-multi-point-proof.v1.json');
}
const GATE_ABI = [
'function metrics() view returns (uint256 coverageRatioBps, uint256 m1ToM00Utilization, uint8 velocityZone, bool issuancePaused, uint256 metricsUpdatedAt)',
];
async function readOnChainGate(): Promise<ReserveCapacitySnapshot['onChainGate']> {
const cfg = readOptionalJson<{ policyGate138?: { envVar?: string } }>(
'config/compliance/gru-monetary-metrics.v1.json',
);
const envVar = cfg?.policyGate138?.envVar || 'GRU_MONETARY_POLICY_GATE_138';
const address = process.env[envVar]?.trim() || null;
const rpc = process.env.RPC_URL_138 || process.env.RPC_URL;
if (!address || !rpc) {
return {
address,
metricsUpdatedAt: null,
coverageRatioBps: null,
m1ToM00Utilization: null,
issuancePaused: null,
rpcReachable: false,
};
}
try {
const c = new Contract(address, GATE_ABI, new JsonRpcProvider(rpc, 138));
const [coverageRatioBps, m1ToM00Utilization, , issuancePaused, metricsUpdatedAt] =
await c.metrics();
return {
address,
metricsUpdatedAt: Number(metricsUpdatedAt),
coverageRatioBps: Number(coverageRatioBps),
m1ToM00Utilization: Number(m1ToM00Utilization),
issuancePaused: Boolean(issuancePaused),
rpcReachable: true,
};
} catch {
return {
address,
metricsUpdatedAt: null,
coverageRatioBps: null,
m1ToM00Utilization: null,
issuancePaused: null,
rpcReachable: false,
};
}
}
function evaluateProofPoints(
cfg: MultiPointProofConfig,
minimumSetKey: string,
metrics: GruMetricsFile | null,
triple: Awaited<ReturnType<typeof runTripleStateReconcile>> | null,
maxAgeSeconds: number,
): ReserveCapacitySnapshot['proofQuorum'] {
const minSet = cfg.minimumSets?.[minimumSetKey];
const required = minSet?.requiredProofPointIds || ['P3', 'P4', 'P6'];
const nameById = new Map((cfg.proofPoints || []).map((p) => [p.id, p.name]));
const rtById = new Map((cfg.proofPoints || []).map((p) => [p.id, p.realTime]));
const points: Record<string, ProofPointStatus> = {};
let passCount = 0;
for (const id of required) {
const point: ProofPointStatus = {
id,
name: nameById.get(id) || id,
status: 'skip',
detail: '',
realTime: rtById.get(id),
};
switch (id) {
case 'P3':
case 'P6': {
if (!metrics) {
point.detail = 'gru-monetary-metrics-latest.json missing';
break;
}
const age = ageSeconds(metrics.generatedAt);
const g = metrics.guardrails || {};
const coverage = g.coverageRatioBps ?? metrics.classical?.reserveRatioBps;
const blocked = g.issuanceBlocked === true;
const coverageOk =
g.coverageOk === true ||
(coverage !== undefined && Number(coverage) >= Number(g.coverageMinBps ?? 12000));
const fresh = age <= maxAgeSeconds;
point.status = !fresh ? 'stale' : coverageOk && !blocked ? 'pass' : 'fail';
point.detail = `age=${Math.round(age)}s coverageBps=${coverage} blocked=${blocked}`;
break;
}
case 'P4': {
if (!triple) {
point.detail = 'triple-state reconcile unavailable';
break;
}
point.status = triple.aligned ? 'pass' : 'fail';
point.detail = `breaks=${triple.breaks.length} aligned=${triple.aligned}`;
break;
}
case 'P1': {
const anchor = existsSync(
resolve(projectRoot(), 'reports/status/vermil-ucc-20242014504/HASH_NOTARIZATION_ANCHOR.txt'),
);
point.status = anchor ? 'pass' : 'skip';
point.detail = anchor ? 'UCC anchor present' : 'missing UCC anchor';
break;
}
case 'P2': {
const reconDir = resolve(projectRoot(), 'reconciliation');
let count = 0;
if (existsSync(reconDir)) {
count = readdirSync(reconDir).filter((d) => d.startsWith('audit-office')).length;
}
point.status = count > 0 ? 'pass' : 'skip';
point.detail = `audit folders=${count}`;
break;
}
case 'P5': {
const ms = existsSync(resolve(projectRoot(), 'config/compliance/web3-multisig-deployed.v1.json'));
point.status = ms ? 'pass' : 'skip';
point.detail = ms ? 'multisig config present' : 'missing web3-multisig-deployed';
break;
}
default:
point.detail = 'no evaluator';
}
if (point.status === 'pass') passCount += 1;
points[id] = point;
}
const minimumRequired = cfg.minimumIndependentProofPoints || 3;
return {
passed: passCount >= minimumRequired,
passCount,
requiredCount: required.length,
minimumRequired,
points,
};
}
export async function buildReserveCapacitySnapshot(
minimumSetKey = 'reserveCapacityLive',
): Promise<ReserveCapacitySnapshot> {
const proofCfg = loadProofConfig();
const maxAge = proofCfg?.sla99999?.freshness?.maxAgeSeconds ?? 60;
const metrics = readOptionalJson<GruMetricsFile>('reports/status/gru-monetary-metrics-latest.json');
let triple: Awaited<ReturnType<typeof runTripleStateReconcile>> | null = null;
try {
triple = await runTripleStateReconcile(process.env.OMNL_HEALTH_LINE_ID?.trim() || undefined);
} catch {
triple = null;
}
const fileTriple = readOptionalJson<Awaited<ReturnType<typeof runTripleStateReconcile>>>(
'reports/status/omnl-triple-reconcile-latest.json',
);
const fileTripleMaxAgeSec = Number(process.env.GRU_RESERVE_FILE_TRIPLE_MAX_AGE_SEC || 3600);
const fileTripleFresh =
fileTriple?.aligned === true && ageSeconds(fileTriple.generatedAt) <= fileTripleMaxAgeSec;
if ((!triple?.aligned || (triple.breaks?.length ?? 0) > 0) && fileTripleFresh) {
triple = fileTriple;
} else if (!triple && fileTriple) {
triple = fileTriple;
}
const onChainGate = await readOnChainGate();
const g = metrics?.guardrails;
const snapshot: ReserveCapacitySnapshot = {
schemaVersion: '1.0.0',
generatedAt: new Date().toISOString(),
configRef: 'config/compliance/gru-multi-point-proof.v1.json',
minimumSet: minimumSetKey,
sla: {
availabilityTargetPercent: proofCfg?.sla99999?.availability?.targetPercent ?? 99.999,
freshnessMaxAgeSeconds: maxAge,
agreementToleranceBps: proofCfg?.sla99999?.agreement?.toleranceBps ?? 50,
},
capacity: {
reserveUsd: metrics?.physicalReserve?.usdValue ?? metrics?.gruLayers?.monetaryBaseUsd ?? null,
m1Usd: metrics?.gruLayers?.M1_cStarUsd ?? null,
m00Usd: metrics?.gruLayers?.M00_liNotionalUsd ?? null,
coverageRatioBps: g?.coverageRatioBps ?? metrics?.classical?.reserveRatioBps ?? null,
issuanceBlocked: g?.issuanceBlocked ?? null,
issuanceBlockReasons: g?.issuanceBlockReasons || [],
velocityZone: metrics?.gruVelocity?.zone ?? null,
},
tripleState: {
aligned: triple?.aligned ?? null,
breaksCount: triple?.breaks?.length ?? null,
generatedAt: triple?.generatedAt ?? null,
},
onChainGate,
proofQuorum: proofCfg
? evaluateProofPoints(proofCfg, minimumSetKey, metrics, triple, maxAge)
: {
passed: false,
passCount: 0,
requiredCount: 3,
minimumRequired: 3,
points: {},
},
semiPublic: true,
redactedFields: [
'fineractGlDetail',
'custodianStatementPath',
'privateKeyPaths',
'fullAuditPackets',
],
reproduce: {
commands: [
'pnpm compliance:gru:monetary-metrics',
'pnpm omnl:reconcile-triple-state',
'pnpm gru:validate-multi-point-proof',
],
configPaths: [
'config/compliance/gru-multi-point-proof.v1.json',
'config/compliance/gru-monetary-metrics.v1.json',
],
},
};
return snapshot;
}
export function writeReserveCapacitySnapshot(snapshot: ReserveCapacitySnapshot): string {
const outRel = 'reports/status/gru-reserve-capacity-latest.json';
const outPath = resolve(projectRoot(), outRel);
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, `${JSON.stringify(snapshot, null, 2)}\n`);
return outPath;
}
export async function refreshReserveCapacityCache(
minimumSetKey = 'reserveCapacityLive',
): Promise<ReserveCapacitySnapshot> {
const snapshot = await buildReserveCapacitySnapshot(minimumSetKey);
try {
writeReserveCapacitySnapshot(snapshot);
} catch {
/* optional when read-only mount */
}
cachedSnapshot = snapshot;
cachedAt = Date.now();
return snapshot;
}
export function getLatestReserveCapacitySnapshot(): ReserveCapacitySnapshot | null {
if (cachedSnapshot && Date.now() - cachedAt < 45_000) {
return cachedSnapshot;
}
const fromDisk = readOptionalJson<ReserveCapacitySnapshot>('reports/status/gru-reserve-capacity-latest.json');
if (fromDisk) {
cachedSnapshot = fromDisk;
cachedAt = Date.now();
return fromDisk;
}
return cachedSnapshot;
}
export function buildProofIndex(): Record<string, unknown> {
const cfg = loadProofConfig();
const latest = getLatestReserveCapacitySnapshot();
return {
generatedAt: new Date().toISOString(),
configVersion: cfg?.minimumIndependentProofPoints ? '1.0.0' : null,
minimumSets: cfg?.minimumSets ?? {},
proofPoints: (cfg?.proofPoints || []).map((p) => ({
id: p.id,
name: p.name,
realTime: p.realTime,
status: latest?.proofQuorum.points[p.id]?.status ?? 'unknown',
detail: latest?.proofQuorum.points[p.id]?.detail ?? null,
})),
quorum: latest?.proofQuorum ?? null,
sla: latest?.sla ?? null,
publicSurfaces: {
capacity: '/api/v1/reserve/capacity',
proofIndex: '/api/v1/reserve/proof-index',
tripleState: '/api/v1/omnl/reconcile/triple-state',
},
};
}

View File

@@ -0,0 +1,137 @@
import axios from 'axios';
import { Interface, parseUnits } from 'ethers';
export type AlchemyNetwork = 'eth-mainnet' | 'defi-oracle-meta-mainnet';
const ERC20_ABI = [
'function balanceOf(address) view returns (uint256)',
'function decimals() view returns (uint8)',
'function transfer(address to, uint256 amount) returns (bool)',
];
function alchemyKey(override?: string): string {
const key = override?.trim() || process.env.ALCHEMY_API_KEY?.trim();
if (!key) {
throw new Error('ALCHEMY_API_KEY unset — required for Alchemy terminal operations');
}
return key;
}
function rpcUrl(network: AlchemyNetwork, apiKeyOverride?: string): string {
const key = alchemyKey(apiKeyOverride);
if (network === 'eth-mainnet') {
return `https://eth-mainnet.g.alchemy.com/v2/${key}`;
}
return `https://defi-oracle-meta-mainnet.g.alchemy.com/v2/${key}`;
}
export async function alchemyJsonRpc<T = unknown>(
network: AlchemyNetwork,
method: string,
params: unknown[] = [],
apiKeyOverride?: string
): Promise<T> {
const allowed = new Set(
(process.env.OMNL_TERMINAL_ALCHEMY_METHODS || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
);
const defaultAllowed = [
'eth_blockNumber',
'eth_getBalance',
'eth_call',
'eth_estimateGas',
'eth_gasPrice',
'eth_getTransactionCount',
'eth_getTransactionReceipt',
'eth_getTransactionByHash',
];
const methods = allowed.size > 0 ? allowed : new Set(defaultAllowed);
if (!methods.has(method)) {
throw new Error(`Alchemy RPC method not allowed: ${method}`);
}
const { data } = await axios.post(
rpcUrl(network, apiKeyOverride),
{ jsonrpc: '2.0', id: 1, method, params },
{ timeout: 30000 }
);
if (data.error) {
throw new Error(data.error.message || JSON.stringify(data.error));
}
return data.result as T;
}
export async function alchemyTokenPricesByAddress(
network: AlchemyNetwork,
addresses: string[],
apiKeyOverride?: string
): Promise<unknown> {
const key = alchemyKey(apiKeyOverride);
const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-address`;
const { data } = await axios.post(
url,
{
addresses: addresses.map((address) => ({ network, address })),
},
{ timeout: 30000 }
);
return data;
}
export async function alchemyPrepareErc20Transfer(input: {
network: AlchemyNetwork;
from: string;
to: string;
tokenAddress: string;
amountHuman: string;
dryRun?: boolean;
apiKeyOverride?: string;
}): Promise<{
network: AlchemyNetwork;
from: string;
to: string;
tokenAddress: string;
amountRaw: string;
data: string;
gasEstimate: string | null;
nonce: string | null;
dryRun: boolean;
note: string;
}> {
const iface = new Interface(ERC20_ABI);
const provider = rpcUrl(input.network, input.apiKeyOverride);
const decimalsHex = await axios.post(provider, {
jsonrpc: '2.0',
id: 1,
method: 'eth_call',
params: [{ to: input.tokenAddress, data: iface.encodeFunctionData('decimals', []) }, 'latest'],
});
const dec = parseInt(String(decimalsHex.data?.result || '0x6'), 16);
const amountRaw = parseUnits(input.amountHuman, dec).toString();
const data = iface.encodeFunctionData('transfer', [input.to, amountRaw]);
let gasEstimate: string | null = null;
let nonce: string | null = null;
if (!input.dryRun) {
gasEstimate = await alchemyJsonRpc<string>(input.network, 'eth_estimateGas', [
{ from: input.from, to: input.tokenAddress, data },
], input.apiKeyOverride);
nonce = await alchemyJsonRpc<string>(input.network, 'eth_getTransactionCount', [input.from, 'latest'], input.apiKeyOverride);
}
return {
network: input.network,
from: input.from,
to: input.tokenAddress,
tokenAddress: input.tokenAddress,
amountRaw,
data,
gasEstimate,
nonce,
dryRun: input.dryRun !== false,
note: 'Unsigned transfer calldata only — broadcast requires separate operator signing workflow.',
};
}
export function alchemyConfigured(apiKeyOverride?: string): boolean {
return Boolean(apiKeyOverride?.trim() || process.env.ALCHEMY_API_KEY?.trim());
}

View File

@@ -15,7 +15,7 @@ export function getOmnlApiCatalog(): {
return {
service: 'HYBX OMNL',
basePath: '/api/v1',
version: '1.3.0',
version: '1.4.0',
endpoints: [
{ method: 'GET', path: '/omnl/openapi.json', description: 'OpenAPI 3.0 JSON (Swagger-compatible)', auth: 'none' },
{ method: 'GET', path: '/omnl/catalog', description: 'This catalog (machine-readable)', auth: 'none' },
@@ -53,6 +53,17 @@ export function getOmnlApiCatalog(): {
{ method: 'GET', path: '/omnl/ipsas/fineract-compare', description: 'Registry vs live GL accounts', auth: 'OMNL_API_KEY if set' },
{ method: 'GET', path: '/omnl/ipsas/layer/:layer', description: 'Monetary layer hint', auth: 'none' },
{ method: 'GET', path: '/omnl/ipsas/compliance-context/:lineId', description: 'Compliance + IPSAS guidance', query: ['aggregated'], auth: 'OMNL_API_KEY if set' },
{ method: 'GET', path: '/omnl/terminal/status', description: 'Settlement terminal integration status', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/terminal/package', description: 'Full settlement terminal JSON (POF, debit note, remittance, tokenization, SWIFT copy, screens)', query: ['settlementRef', 'officeId', 'valueDate', 'amountUsd'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/terminal/proof-of-funds', description: 'Proof of Funds JSON (Fineract GL 2100)', query: ['settlementRef', 'officeId'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/terminal/debit-note', description: 'Debit note JSON mapped to IPSAS journal pair', query: ['settlementRef', 'officeId'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/terminal/remittance-advice', description: 'Remittance advice JSON (camt.054-equivalent)', query: ['settlementRef', 'officeId'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/terminal/tokenization-sanitized', description: 'Tokenization record with redacted PII for external distribution', query: ['settlementRef', 'officeId'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/terminal/conversion-swift-copy', description: 'Internal conversion SWIFT-field copy (not wire confirmation)', query: ['settlementRef', 'officeId'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/terminal/screen', description: 'Terminal screen text (black/green or gray/black)', query: ['mode', 'artifact', 'format'], auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/rpc', description: 'Alchemy JSON-RPC (read/estimate methods)', body: '{ network, method, params }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/prices', description: 'Alchemy token prices by address', body: '{ network, addresses[] }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/tx-prep', description: 'Prepare unsigned ERC-20 transfer calldata via Alchemy (dry-run default)', body: '{ network, from, to, tokenAddress, amount, dryRun }', auth: 'OMNL_API_KEY' },
],
};
}

View File

@@ -0,0 +1,89 @@
import {
buildProofOfFunds,
buildDebitNote,
buildRemittanceAdvice,
buildTokenizationSanitized,
buildConversionSwiftCopy,
renderTerminalScreen,
type SettlementTerminalContext,
} from './omnl-settlement-terminal';
jest.mock('./omnl-alchemy-client', () => ({
alchemyConfigured: jest.fn(() => false),
}));
jest.mock('./omnl-ipsas-gl', () => ({
fetchFineractGlAccounts: jest.fn(),
}));
function sampleCtx(): SettlementTerminalContext {
return {
generatedAtUtc: '2026-06-07T12:00:00.000Z',
settlementRef: 'HYBX-BATCH-001',
valueDate: '2026-03-17',
currency: 'USD',
officeId: 1,
officeName: 'OMNL Head Office (DBIS) Central Bank',
beneficiary: 'Bank Kanaya (Indonesia)',
beneficiaryOfficeId: 22,
beneficiaryExternalId: 'BANK-KANAYA-ID',
amountUsd: '1000000000.00',
omnlLegalName: 'ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.',
omnlLei: '98450070C57395F6B906',
fineract: {
configured: true,
pofPrimaryGlCode: '2100',
pofPrimaryAmountUsd: '50000000000.00',
glBalances: [{ glCode: '2100', name: 'M1', side: 'Cr', amount: '50000000000.00' }],
},
alchemy: {
configured: false,
mainnetNetwork: 'eth-mainnet',
chain138Network: 'defi-oracle-meta-mainnet',
},
documentSha256: 'abc123',
};
}
describe('omnl-settlement-terminal', () => {
it('builds proof of funds with primary GL attestation', () => {
const pof = buildProofOfFunds(sampleCtx());
expect(pof.documentType).toBe('proof-of-funds');
expect(pof.attestation).toMatchObject({ primaryGlCode: '2100' });
});
it('builds debit note with accounting pair', () => {
const dn = buildDebitNote(sampleCtx());
expect(dn.documentType).toBe('debit-note');
expect(dn.accounting).toMatchObject({ debitGlCode: '2100', creditGlCode: '1410' });
});
it('builds remittance advice with instruction id', () => {
const ra = buildRemittanceAdvice(sampleCtx());
expect(ra.documentType).toBe('remittance-advice');
expect(String(ra.instructionId)).toContain('RA-HYBX-BATCH-001');
});
it('sanitizes tokenization copy fields', () => {
const tok = buildTokenizationSanitized(sampleCtx());
expect(tok.documentType).toBe('tokenization-sanitized');
expect(String(tok.accountNumber)).toMatch(/\*/);
expect(String(tok.beneficiaryName)).toMatch(/\*/);
});
it('builds conversion swift copy with MT field tags', () => {
const sw = buildConversionSwiftCopy(sampleCtx());
expect(sw.documentType).toBe('conversion-swift-copy');
expect((sw.fields as Record<string, string>)[':20:']).toBe('HYBX-BATCH-001');
});
it('renders black and color terminal screens', () => {
const ctx = sampleCtx();
const black = renderTerminalScreen(ctx, 'black', 'package');
const color = renderTerminalScreen(ctx, 'color', 'proof-of-funds');
expect(black).toContain('BLACK / GREEN');
expect(color).toContain('COLOR / GRAY');
expect(black).toContain('HYBX-BATCH-001');
expect(color).toContain('POF GL 2100');
});
});

View File

@@ -0,0 +1,623 @@
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { createHash, randomUUID } from 'crypto';
import axios, { type AxiosRequestConfig } from 'axios';
import { fetchFineractGlAccounts } from './omnl-ipsas-gl';
import { alchemyConfigured } from './omnl-alchemy-client';
import {
loadTerminalConnection,
buildConnectionPublicSummary,
connectionAlchemyApiKey,
connectionReceiverConfig,
type TerminalConnectionProfile,
} from './omnl-terminal-connection';
export type TerminalScreenMode = 'black' | 'color';
export type TerminalConfig = {
version: string;
defaultSettlementRef: string;
defaultOfficeId: number;
defaultCurrency: string;
defaultValueDate: string;
defaultBeneficiary: string;
defaultBeneficiaryOfficeId: number;
defaultBeneficiaryExternalId: string;
defaultConnectionId?: string;
omnlLegalName: string;
omnlLei: string;
pofPrimaryGlCode: string;
debitNoteDefaultDebitGl: string;
debitNoteDefaultCreditGl: string;
tokenization: {
chain138RpcEnv: string;
defaultLineId: string;
sanitizedRedactFields: string[];
};
alchemy: {
mainnetNetwork: string;
chain138Network: string;
allowedRpcMethods: string[];
pricesApiPath: string;
};
disclaimers: Record<string, string>;
};
export type SettlementTerminalContext = {
generatedAtUtc: string;
settlementRef: string;
valueDate: string;
currency: string;
officeId: number;
officeName: string;
beneficiary: string;
beneficiaryOfficeId: number;
beneficiaryExternalId: string;
amountUsd: string;
omnlLegalName: string;
omnlLei: string;
fineract: {
configured: boolean;
baseUrl?: string;
glCount?: number;
pofPrimaryGlCode: string;
pofPrimaryAmountUsd: string | null;
glBalances?: Array<{ glCode: string; name: string; side: string; amount: string }>;
error?: string;
};
alchemy: {
configured: boolean;
mainnetNetwork: string;
chain138Network: string;
};
documentSha256: string;
connection?: {
connectionId: string;
summary: Record<string, unknown>;
receiverConfigured: boolean;
alchemyConfigured: boolean;
};
};
export type TerminalArtifact =
| 'package'
| 'proof-of-funds'
| 'debit-note'
| 'remittance-advice'
| 'tokenization-sanitized'
| 'conversion-swift-copy';
function repoRoot(): string {
if (process.env.PROXMOX_ROOT?.trim()) return process.env.PROXMOX_ROOT.trim();
if (process.env.PHOENIX_REPO_ROOT?.trim()) return process.env.PHOENIX_REPO_ROOT.trim();
// dist/services → proxmox repo root (5 levels)
return resolve(__dirname, '../../../../..');
}
export function loadTerminalConfig(): TerminalConfig {
const candidates = [
process.env.OMNL_SETTLEMENT_TERMINAL_CONFIG?.trim(),
resolve(repoRoot(), 'config/omnl-settlement-terminal.v1.json'),
resolve(__dirname, '../../../../config/omnl-settlement-terminal.v1.json'),
].filter(Boolean) as string[];
const p = candidates.find((c) => existsSync(c));
if (!p) {
throw new Error(`Terminal config not found (tried: ${candidates.join(', ')})`);
}
return JSON.parse(readFileSync(p, 'utf8')) as TerminalConfig;
}
function fineractHeaders(): { base: string; headers: Record<string, string> } {
const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, '');
const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl';
const user =
process.env.OMNL_FINERACT_USER?.trim() ||
process.env.OMNL_FINERACT_USERNAME?.trim() ||
'app.omnl';
const pass = process.env.OMNL_FINERACT_PASSWORD || '';
if (!base || !pass) {
throw new Error('OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD required');
}
const auth = Buffer.from(`${user}:${pass}`).toString('base64');
return {
base,
headers: {
Authorization: `Basic ${auth}`,
'Fineract-Platform-TenantId': tenant,
Accept: 'application/json',
},
};
}
async function fetchOffices(): Promise<Array<{ id: number; name: string }>> {
const { base, headers } = fineractHeaders();
const { data } = await axios.get(`${base}/offices`, { headers, timeout: 60000 });
const rows = Array.isArray(data) ? data : [];
return rows.map((r: { id?: number; name?: string }) => ({
id: Number(r.id),
name: String(r.name || ''),
}));
}
async function fetchOfficeJournalLines(officeId: number): Promise<
Array<{ glAccountId: number; amount: number; entryType: string; transactionDate?: unknown }>
> {
const { base, headers } = fineractHeaders();
const out: Array<{ glAccountId: number; amount: number; entryType: string; transactionDate?: unknown }> = [];
let offset = 0;
const limit = 200;
for (;;) {
const cfg: AxiosRequestConfig = {
headers,
timeout: 120000,
params: { officeId, limit, offset },
};
const { data } = await axios.get(`${base}/journalentries`, cfg);
const batch = Array.isArray(data) ? data : (data as { pageItems?: unknown[] })?.pageItems ?? [];
if (!Array.isArray(batch) || batch.length === 0) break;
for (const je of batch) {
const row = je as {
debits?: Array<{ glAccountId?: number; amount?: number }>;
credits?: Array<{ glAccountId?: number; amount?: number }>;
transactionDate?: unknown;
};
for (const d of row.debits ?? []) {
if (d.glAccountId != null) {
out.push({
glAccountId: d.glAccountId,
amount: Number(d.amount || 0),
entryType: 'DEBIT',
transactionDate: row.transactionDate,
});
}
}
for (const c of row.credits ?? []) {
if (c.glAccountId != null) {
out.push({
glAccountId: c.glAccountId,
amount: Number(c.amount || 0),
entryType: 'CREDIT',
transactionDate: row.transactionDate,
});
}
}
}
if (batch.length < limit) break;
offset += limit;
}
return out;
}
function signedGlBalances(
lines: Array<{ glAccountId: number; amount: number; entryType: string }>,
glById: Map<number, { glCode: string; name: string }>
): Array<{ glCode: string; name: string; side: string; amount: string }> {
const net = new Map<number, number>();
for (const l of lines) {
const cur = net.get(l.glAccountId) ?? 0;
net.set(l.glAccountId, cur + (l.entryType === 'DEBIT' ? l.amount : -l.amount));
}
const rows: Array<{ glCode: string; name: string; side: string; amount: string }> = [];
for (const [id, val] of net.entries()) {
if (Math.abs(val) < 0.005) continue;
const g = glById.get(id) ?? { glCode: String(id), name: '' };
rows.push({
glCode: g.glCode,
name: g.name,
side: val > 0 ? 'Dr' : 'Cr',
amount: Math.abs(val).toFixed(2),
});
}
rows.sort((a, b) => a.glCode.localeCompare(b.glCode));
return rows;
}
function glCreditAmount(rows: Array<{ glCode: string; side: string; amount: string }>, code: string): string | null {
for (const r of rows) {
if (r.glCode === code) {
if (r.side === 'Cr') return r.amount;
if (r.side === 'Dr') return `-${r.amount}`;
}
}
return null;
}
function maskValue(field: string, value: string): string {
if (!value) return value;
if (field.toLowerCase().includes('swift') || field.toLowerCase().includes('iban')) {
return value.length <= 4 ? '****' : `${'*'.repeat(Math.max(0, value.length - 4))}${value.slice(-4)}`;
}
if (field.toLowerCase().includes('name')) {
const parts = value.split(/\s+/);
return parts.map((p) => (p.length <= 2 ? p : `${p[0]}${'*'.repeat(p.length - 2)}${p.slice(-1)}`)).join(' ');
}
if (field.toLowerCase().includes('account')) {
return value.length <= 4 ? '****' : `****${value.slice(-4)}`;
}
return '***REDACTED***';
}
export async function buildSettlementTerminalContext(input: {
settlementRef?: string;
officeId?: number;
valueDate?: string;
amountUsd?: string;
connectionId?: string;
}): Promise<SettlementTerminalContext> {
const cfg = loadTerminalConfig();
let connectionProfile: TerminalConnectionProfile | null = null;
const connId = input.connectionId?.trim() || cfg.defaultConnectionId?.trim() || '';
if (connId) {
try {
connectionProfile = loadTerminalConnection(connId);
} catch {
connectionProfile = null;
}
}
const sd = connectionProfile?.settlementDefaults;
const settlementRef =
input.settlementRef?.trim() || sd?.settlementRef || cfg.defaultSettlementRef;
const officeId = input.officeId ?? cfg.defaultOfficeId;
const valueDate = input.valueDate?.trim() || cfg.defaultValueDate;
const amountUsd = input.amountUsd?.trim() || '1000000000.00';
let officeName = `Office ${officeId}`;
let glBalances: Array<{ glCode: string; name: string; side: string; amount: string }> = [];
let pofAmount: string | null = null;
let fineractError: string | undefined;
let glCount = 0;
let baseUrl: string | undefined;
try {
const { base } = fineractHeaders();
baseUrl = base;
const [offices, glRows, lines] = await Promise.all([
fetchOffices(),
fetchFineractGlAccounts(),
fetchOfficeJournalLines(officeId),
]);
glCount = glRows.length;
const hit = offices.find((o) => o.id === officeId);
if (hit) officeName = hit.name;
const glById = new Map<number, { glCode: string; name: string }>();
for (const g of glRows) {
if (g.id != null && g.glCode) {
glById.set(g.id, { glCode: String(g.glCode), name: String(g.name || '') });
}
}
glBalances = signedGlBalances(lines, glById);
pofAmount = glCreditAmount(glBalances, cfg.pofPrimaryGlCode);
} catch (e) {
fineractError = e instanceof Error ? e.message : String(e);
}
const ctx: SettlementTerminalContext = {
generatedAtUtc: new Date().toISOString(),
settlementRef,
valueDate,
currency: cfg.defaultCurrency,
officeId,
officeName,
beneficiary: sd?.beneficiary || cfg.defaultBeneficiary,
beneficiaryOfficeId: cfg.defaultBeneficiaryOfficeId,
beneficiaryExternalId: sd?.beneficiaryExternalId || cfg.defaultBeneficiaryExternalId,
amountUsd,
omnlLegalName: cfg.omnlLegalName,
omnlLei: cfg.omnlLei,
fineract: {
configured: !fineractError,
baseUrl,
glCount,
pofPrimaryGlCode: cfg.pofPrimaryGlCode,
pofPrimaryAmountUsd: pofAmount,
glBalances: glBalances.length ? glBalances : undefined,
error: fineractError,
},
alchemy: {
configured: connectionProfile
? Boolean(connectionAlchemyApiKey(connectionProfile))
: alchemyConfigured(),
mainnetNetwork: cfg.alchemy.mainnetNetwork,
chain138Network: cfg.alchemy.chain138Network,
},
documentSha256: '',
};
if (connectionProfile) {
const receiver = connectionReceiverConfig(connectionProfile);
ctx.connection = {
connectionId: connectionProfile.connectionId,
summary: buildConnectionPublicSummary(connectionProfile),
receiverConfigured: receiver.configured,
alchemyConfigured: Boolean(connectionAlchemyApiKey(connectionProfile)),
};
ctx.currency = sd?.currency || ctx.currency;
}
ctx.documentSha256 = createHash('sha256').update(JSON.stringify(ctx)).digest('hex');
return ctx;
}
export function buildProofOfFunds(ctx: SettlementTerminalContext): Record<string, unknown> {
const cfg = loadTerminalConfig();
return {
documentType: 'proof-of-funds',
generatedAtUtc: ctx.generatedAtUtc,
settlementRef: ctx.settlementRef,
valueDate: ctx.valueDate,
issuer: {
legalName: ctx.omnlLegalName,
lei: ctx.omnlLei,
officeId: ctx.officeId,
officeName: ctx.officeName,
},
attestation: {
primaryGlCode: ctx.fineract.pofPrimaryGlCode,
primaryAmountUsd: ctx.fineract.pofPrimaryAmountUsd,
currency: ctx.currency,
methodology: 'Fineract journal sum (DEBIT +, CREDIT -) for office GL balances',
},
glBalances: ctx.fineract.glBalances ?? [],
disclaimer: cfg.disclaimers.pof,
integrity: { sha256: ctx.documentSha256 },
};
}
export function buildDebitNote(ctx: SettlementTerminalContext): Record<string, unknown> {
const cfg = loadTerminalConfig();
const uetr = randomUUID();
return {
documentType: 'debit-note',
generatedAtUtc: ctx.generatedAtUtc,
settlementRef: ctx.settlementRef,
valueDate: ctx.valueDate,
uetr,
debitNoteId: `DN-${ctx.settlementRef}-${ctx.valueDate.replace(/-/g, '')}`,
orderingInstitution: {
name: ctx.omnlLegalName,
lei: ctx.omnlLei,
officeId: ctx.officeId,
},
beneficiary: {
name: ctx.beneficiary,
officeId: ctx.beneficiaryOfficeId,
externalId: ctx.beneficiaryExternalId,
...(ctx.connection?.summary?.client ? { client: ctx.connection.summary.client } : {}),
...(ctx.connection?.summary?.bank ? { bank: ctx.connection.summary.bank } : {}),
},
receiver: ctx.connection
? {
connectionId: ctx.connection.connectionId,
serverId: (ctx.connection.summary.receiver as { serverId?: string })?.serverId,
receiverIp: (ctx.connection.summary.receiver as { receiverIp?: string })?.receiverIp,
wallet: (ctx.connection.summary as { wallet?: { address?: string } }).wallet,
}
: undefined,
accounting: {
debitGlCode: cfg.debitNoteDefaultDebitGl,
creditGlCode: cfg.debitNoteDefaultCreditGl,
amountUsd: ctx.amountUsd,
currency: ctx.currency,
narrative: `Settlement debit — ${ctx.settlementRef}`,
},
fineractMapped: ctx.fineract.configured,
disclaimer: 'Accounting debit note mapped to OMNL Fineract IPSAS journal pairs — not a bank wire debit advice.',
integrity: { sha256: createHash('sha256').update(JSON.stringify(ctx)).digest('hex') },
};
}
export function buildRemittanceAdvice(ctx: SettlementTerminalContext): Record<string, unknown> {
const instructionId = `RA-${ctx.settlementRef}-${Date.now()}`;
return {
documentType: 'remittance-advice',
generatedAtUtc: ctx.generatedAtUtc,
settlementRef: ctx.settlementRef,
valueDate: ctx.valueDate,
instructionId,
messageType: 'camt.054-equivalent',
remitter: {
name: ctx.omnlLegalName,
lei: ctx.omnlLei,
officeId: ctx.officeId,
officeName: ctx.officeName,
},
beneficiary: {
name: ctx.beneficiary,
officeId: ctx.beneficiaryOfficeId,
externalId: ctx.beneficiaryExternalId,
...(ctx.connection?.summary?.client ? { client: ctx.connection.summary.client } : {}),
...(ctx.connection?.summary?.bank ? { bank: ctx.connection.summary.bank } : {}),
},
receiver: ctx.connection
? {
connectionId: ctx.connection.connectionId,
configured: ctx.connection.receiverConfigured,
serverId: (ctx.connection.summary.receiver as { serverId?: string })?.serverId,
apiDocsUrl: (ctx.connection.summary.receiver as { apiDocsUrl?: string })?.apiDocsUrl,
}
: undefined,
payment: {
amount: ctx.amountUsd,
currency: ctx.currency,
valueDate: ctx.valueDate,
remittanceInfo: `Settlement ${ctx.settlementRef} — OMNL/HYBX book transfer`,
},
fineractStatus: ctx.fineract.configured ? 'sor_reachable' : 'sor_unavailable',
disclaimer: 'Remittance advice for institutional reconciliation — confirm against Fineract JE and ISO 20022 archive.',
integrity: { sha256: createHash('sha256').update(instructionId + ctx.settlementRef).digest('hex') },
};
}
export function buildTokenizationSanitized(ctx: SettlementTerminalContext): Record<string, unknown> {
const cfg = loadTerminalConfig();
const raw = {
documentType: 'tokenization-sanitized',
generatedAtUtc: ctx.generatedAtUtc,
settlementRef: ctx.settlementRef,
lineId: cfg.tokenization.defaultLineId,
tokenizationRequestId: `TOK-${ctx.settlementRef}-${randomUUID().slice(0, 8)}`,
underlyingAsset: ctx.currency,
amount: ctx.amountUsd,
issuer: ctx.omnlLegalName,
beneficiaryName: ctx.beneficiary,
orderingName: ctx.officeName,
accountNumber: `00000000${ctx.officeId}`.slice(-12),
iban: `US00OMNL000000000${ctx.officeId}`,
swiftBic: 'OMNLUS33',
chain138Line: cfg.tokenization.defaultLineId,
policyNote: 'Sanitized copy for external distribution — full tokenization record retained operator-side.',
};
const sanitized: Record<string, unknown> = { ...raw };
for (const field of cfg.tokenization.sanitizedRedactFields) {
const val = raw[field as keyof typeof raw];
if (typeof val === 'string') {
sanitized[field] = maskValue(field, val);
}
}
sanitized.integrity = {
sha256: createHash('sha256').update(JSON.stringify(sanitized)).digest('hex'),
fullRecordHint: 'POST /api/v1/omnl/iso20022/messages for retained ISO payload',
};
return sanitized;
}
export function buildConversionSwiftCopy(ctx: SettlementTerminalContext): Record<string, unknown> {
const cfg = loadTerminalConfig();
return {
documentType: 'conversion-swift-copy',
generatedAtUtc: ctx.generatedAtUtc,
settlementRef: ctx.settlementRef,
valueDate: ctx.valueDate,
copyClass: 'internal-operator-conversion',
fields: {
':20:': ctx.settlementRef,
':23B:': 'CRED',
':32A:': `${ctx.valueDate.replace(/-/g, '').slice(2)}${ctx.currency}${ctx.amountUsd.replace('.', ',')}`,
':50K:': `/${ctx.omnlLei}\n${ctx.omnlLegalName}\nOffice ${ctx.officeId}`,
':59:': `/${ctx.beneficiaryExternalId}\n${ctx.beneficiary}`,
':70:': `CONVERSION SETTLEMENT ${ctx.settlementRef}`,
':71A:': 'SHA',
},
iso20022Equivalent: 'pacs.008 / pacs.009 mapping available via /api/v1/omnl/iso20022/messages',
fineractMapped: ctx.fineract.configured,
disclaimer: cfg.disclaimers.swiftCopy,
integrity: { sha256: createHash('sha256').update(ctx.settlementRef + ctx.valueDate).digest('hex') },
};
}
export function buildFullTerminalPackage(ctx: SettlementTerminalContext): Record<string, unknown> {
const cfg = loadTerminalConfig();
const artifacts = {
proofOfFunds: buildProofOfFunds(ctx),
debitNote: buildDebitNote(ctx),
remittanceAdvice: buildRemittanceAdvice(ctx),
tokenizationSanitized: buildTokenizationSanitized(ctx),
conversionSwiftCopy: buildConversionSwiftCopy(ctx),
screens: {
black: renderTerminalScreen(ctx, 'black', 'package'),
color: renderTerminalScreen(ctx, 'color', 'package'),
},
};
return {
documentType: 'omnl-settlement-terminal-package',
version: cfg.version,
generatedAtUtc: ctx.generatedAtUtc,
settlementRef: ctx.settlementRef,
context: ctx,
artifacts,
disclaimers: cfg.disclaimers,
integrity: {
packageSha256: createHash('sha256').update(JSON.stringify(artifacts)).digest('hex'),
},
};
}
function artifactPayload(
ctx: SettlementTerminalContext,
artifact: TerminalArtifact
): Record<string, unknown> {
switch (artifact) {
case 'proof-of-funds':
return buildProofOfFunds(ctx);
case 'debit-note':
return buildDebitNote(ctx);
case 'remittance-advice':
return buildRemittanceAdvice(ctx);
case 'tokenization-sanitized':
return buildTokenizationSanitized(ctx);
case 'conversion-swift-copy':
return buildConversionSwiftCopy(ctx);
case 'package':
default:
return buildFullTerminalPackage(ctx);
}
}
export function renderTerminalScreen(
ctx: SettlementTerminalContext,
mode: TerminalScreenMode,
artifact: TerminalArtifact = 'package'
): string {
const cfg = loadTerminalConfig();
const lines: string[] = [];
const hr = '='.repeat(72);
lines.push(hr);
lines.push(' OMNL / HYBX SETTLEMENT TERMINAL');
lines.push(` MODE: ${mode === 'black' ? 'BLACK / GREEN' : 'COLOR / GRAY'}`);
lines.push(` ARTIFACT: ${artifact.toUpperCase()}`);
lines.push(hr);
lines.push(` SETTLEMENT REF .... ${ctx.settlementRef}`);
lines.push(` VALUE DATE ........ ${ctx.valueDate}`);
lines.push(` OFFICE ............ ${ctx.officeId}${ctx.officeName}`);
lines.push(` BENEFICIARY ....... ${ctx.beneficiary}`);
lines.push(` AMOUNT USD ........ ${ctx.amountUsd}`);
lines.push(` GENERATED UTC ..... ${ctx.generatedAtUtc}`);
lines.push('-'.repeat(72));
lines.push(' FINERACT SoR');
lines.push(` STATUS .......... ${ctx.fineract.configured ? 'REACHABLE' : 'UNAVAILABLE'}`);
if (ctx.fineract.baseUrl) lines.push(` BASE URL ........ ${ctx.fineract.baseUrl}`);
if (ctx.fineract.glCount != null) lines.push(` GL ACCOUNTS ..... ${ctx.fineract.glCount}`);
lines.push(
` POF GL ${ctx.fineract.pofPrimaryGlCode} .... ${ctx.fineract.pofPrimaryAmountUsd ?? 'N/A'} ${ctx.currency}`
);
if (ctx.fineract.error) lines.push(` ERROR ........... ${ctx.fineract.error}`);
lines.push('-'.repeat(72));
lines.push(' ALCHEMY');
lines.push(` CONFIGURED ...... ${ctx.alchemy.configured ? 'YES' : 'NO (set ALCHEMY_API_KEY)'}`);
lines.push(` MAINNET ......... ${ctx.alchemy.mainnetNetwork}`);
lines.push(` CHAIN 138 ....... ${ctx.alchemy.chain138Network}`);
if (ctx.connection) {
lines.push('-'.repeat(72));
lines.push(' RECEIVER CONNECTION');
lines.push(` ID .............. ${ctx.connection.connectionId}`);
const recv = ctx.connection.summary.receiver as { serverId?: string; receiverIp?: string };
if (recv?.serverId) lines.push(` SERVER .......... ${recv.serverId}`);
if (recv?.receiverIp) lines.push(` IP .............. ${recv.receiverIp}`);
const wallet = (ctx.connection.summary as { wallet?: { address?: string } }).wallet?.address;
if (wallet) lines.push(` ETH WALLET ...... ${wallet}`);
lines.push(` RECEIVER API .... ${ctx.connection.receiverConfigured ? 'CONFIGURED' : 'KEYS UNSET'}`);
lines.push(` ALCHEMY CONN .... ${ctx.connection.alchemyConfigured ? 'CONFIGURED' : 'KEY UNSET'}`);
}
lines.push('-'.repeat(72));
if (artifact !== 'package') {
const doc = artifactPayload(ctx, artifact);
lines.push(` DOCUMENT: ${String(doc.documentType || artifact)}`);
if (doc.disclaimer) lines.push(` NOTE: ${String(doc.disclaimer)}`);
if (typeof doc.integrity === 'object' && doc.integrity && 'sha256' in (doc.integrity as object)) {
lines.push(` SHA256: ${String((doc.integrity as { sha256: string }).sha256).slice(0, 16)}...`);
}
} else {
lines.push(' ARTIFACTS: POF | DEBIT-NOTE | REMITTANCE | TOKEN-SAN | SWIFT-COPY');
}
lines.push('-'.repeat(72));
lines.push(` ${cfg.disclaimers.terminalScreen}`);
lines.push(hr);
lines.push(' END OF TERMINAL COPY');
lines.push(hr);
return lines.join('\n');
}
export function getTerminalArtifact(
ctx: SettlementTerminalContext,
artifact: TerminalArtifact
): Record<string, unknown> {
return artifactPayload(ctx, artifact);
}

View File

@@ -0,0 +1,149 @@
import { readFileSync, existsSync, readdirSync } from 'fs';
import { resolve, join } from 'path';
export type TerminalConnectionProfile = {
connectionId: string;
version: string;
cisDate?: string;
client: {
companyName: string;
companyRegistrationNo?: string;
address?: string;
jurisdiction?: string;
representedBy?: Record<string, string>;
};
bank?: {
name?: string;
accountName?: string;
accountNumber?: string;
iban?: string;
swiftBic?: string;
};
receiver?: {
serverId?: string;
serverLabel?: string;
receiverIp?: string;
apiDocsUrl?: string;
apiBaseUrlEnv?: string;
apiAuthKeyEnv?: string;
};
alchemy?: {
network?: string;
apiKeyEnv?: string;
fallbackApiKeyEnv?: string;
};
wallet?: {
chain?: string;
address?: string;
purpose?: string;
};
settlementDefaults?: {
settlementRef?: string;
beneficiary?: string;
beneficiaryExternalId?: string;
currency?: string;
};
};
function connectionsRoot(): string {
const root =
process.env.PROXMOX_ROOT?.trim() ||
process.env.PHOENIX_REPO_ROOT?.trim() ||
resolve(__dirname, '../../../../..');
return (
process.env.OMNL_TERMINAL_CONNECTIONS_DIR?.trim() ||
resolve(root, 'config/omnl-terminal-connections')
);
}
export function listTerminalConnectionIds(): string[] {
const dir = connectionsRoot();
if (!existsSync(dir)) return [];
return readdirSync(dir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name)
.filter((id) => existsSync(join(dir, id, 'connection.v1.json')))
.sort();
}
export function loadTerminalConnection(connectionId: string): TerminalConnectionProfile {
const id = connectionId.trim();
if (!id || /[^a-z0-9-]/i.test(id)) {
throw new Error('Invalid connectionId');
}
const p = join(connectionsRoot(), id, 'connection.v1.json');
if (!existsSync(p)) {
throw new Error(`Connection profile not found: ${id}`);
}
const profile = JSON.parse(readFileSync(p, 'utf8')) as TerminalConnectionProfile;
if (profile.connectionId !== id) {
throw new Error(`connectionId mismatch: folder ${id} vs JSON ${profile.connectionId}`);
}
return profile;
}
export function connectionProjectDir(connectionId: string): string {
return join(connectionsRoot(), connectionId.trim());
}
export function resolveConnectionSecret(profile: TerminalConnectionProfile, primaryEnv: string, fallbackEnv?: string): string {
const primary = process.env[primaryEnv]?.trim();
if (primary) return primary;
if (fallbackEnv) {
const fb = process.env[fallbackEnv]?.trim();
if (fb) return fb;
}
return '';
}
export function connectionAlchemyApiKey(profile: TerminalConnectionProfile): string {
const primary = profile.alchemy?.apiKeyEnv || 'MK_ALBERT_GATE_ALCHEMY_API_KEY';
const fallback = profile.alchemy?.fallbackApiKeyEnv || 'ALCHEMY_API_KEY';
return resolveConnectionSecret(profile, primary, fallback);
}
export function connectionReceiverConfig(profile: TerminalConnectionProfile): {
baseUrl: string;
authKey: string;
configured: boolean;
} {
const baseEnv = profile.receiver?.apiBaseUrlEnv || 'MK_ALBERT_GATE_RECEIVER_API_BASE_URL';
const authEnv = profile.receiver?.apiAuthKeyEnv || 'MK_ALBERT_GATE_RECEIVER_API_AUTH_KEY';
const baseUrl = process.env[baseEnv]?.trim() || '';
const authKey = process.env[authEnv]?.trim() || '';
return { baseUrl, authKey, configured: Boolean(baseUrl && authKey) };
}
export function buildConnectionPublicSummary(profile: TerminalConnectionProfile): Record<string, unknown> {
const alchemyKey = connectionAlchemyApiKey(profile);
const receiver = connectionReceiverConfig(profile);
return {
connectionId: profile.connectionId,
version: profile.version,
cisDate: profile.cisDate,
client: profile.client,
bank: profile.bank
? {
...profile.bank,
accountNumber: profile.bank.accountNumber
? `****${String(profile.bank.accountNumber).slice(-4)}`
: undefined,
iban: profile.bank.iban ? `****${String(profile.bank.iban).slice(-4)}` : undefined,
}
: undefined,
receiver: {
serverId: profile.receiver?.serverId,
serverLabel: profile.receiver?.serverLabel,
receiverIp: profile.receiver?.receiverIp,
apiDocsUrl: profile.receiver?.apiDocsUrl,
configured: receiver.configured,
},
wallet: profile.wallet,
settlementDefaults: profile.settlementDefaults,
secrets: {
alchemyConfigured: Boolean(alchemyKey),
receiverApiConfigured: receiver.configured,
},
projectDir: connectionProjectDir(profile.connectionId),
};
}

View File

@@ -1,3 +1,4 @@
import { MockReconciliationEngine, type ReconciliationSnapshot } from '@dbis/integration-foundation';
import axios from 'axios';
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
@@ -20,6 +21,8 @@ export interface TripleReconcileReport {
lineId: string;
breaks: TripleReconcileBreak[];
aligned: boolean;
/** Optional HYBX↔Fineract↔chain ref reconcile via integration-foundation (OMNL_FOUNDATION_RECONCILE=1). */
foundationSnapshot?: ReconciliationSnapshot;
fineract: {
glSnapshot: Array<{ glCode: string; name?: string; id: number }>;
liabilityGlCodes: string[];
@@ -203,15 +206,48 @@ export async function runTripleStateReconcile(lineId?: string): Promise<TripleRe
}
const aligned = breaks.filter((b) => b.severity === 'critical' || b.severity === 'high').length === 0;
let foundationSnapshot: ReconciliationSnapshot | undefined;
if (process.env.OMNL_FOUNDATION_RECONCILE === '1') {
const hybxRefs = (process.env.OMNL_RECON_HYBX_REFS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const fineractRefs = (process.env.OMNL_RECON_FINERACT_REFS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const chainRefs = (process.env.OMNL_RECON_CHAIN_REFS ?? line)
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const engine = new MockReconciliationEngine();
foundationSnapshot = await engine.reconcileTripleState({
tenantId: process.env.OMNL_FINERACT_TENANT?.trim() || 'omnl',
entityId: process.env.OMNL_RECON_ENTITY_ID?.trim() || 'omnl-default',
hybxRefs,
fineractRefs,
chainRefs,
});
for (const brk of foundationSnapshot.breaks) {
breaks.push({
code: `FOUNDATION_${brk.type.toUpperCase()}`,
severity: brk.type === 'missing_event' ? 'high' : 'medium',
message: `${brk.source}: ${brk.referenceId}`,
});
}
}
const report: TripleReconcileReport = {
generatedAt,
lineId: line,
breaks,
aligned,
aligned: breaks.filter((b) => b.severity === 'critical' || b.severity === 'high').length === 0,
fineract: { glSnapshot, liabilityGlCodes: liabilityCodes },
onChain,
custodian,
comparison,
foundationSnapshot,
};
appendOmnlAudit({

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { createHmac, timingSafeEqual } from 'crypto';
import { computeWebhookSignature, verifyWebhookSignature } from '@dbis/integration-foundation';
import { logger } from '../utils/logger';
import { appendOmnlAudit } from './omnl-audit-log';
@@ -23,24 +23,23 @@ function parseWebhookUrls(): string[] {
/** HMAC-SHA256(hex) of UTF-8 body; header value `sha256=<hex>`. */
export function signOmnlWebhookBody(rawBodyUtf8: string, secret: string): string {
const h = createHmac('sha256', secret).update(rawBodyUtf8, 'utf8').digest('hex');
return `sha256=${h}`;
return `sha256=${computeWebhookSignature(secret, rawBodyUtf8)}`;
}
/**
* Verify `X-OMNL-Signature` from `signOmnlWebhookBody` (timing-safe).
*/
export function verifyOmnlWebhookSignature(rawBodyUtf8: string, signatureHeader: string | undefined, secret: string): boolean {
export function verifyOmnlWebhookSignature(
rawBodyUtf8: string,
signatureHeader: string | undefined,
secret: string
): boolean {
if (!signatureHeader || !secret) return false;
const expected = signOmnlWebhookBody(rawBodyUtf8, secret);
try {
const a = Buffer.from(signatureHeader.trim(), 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
} catch {
return false;
}
return verifyWebhookSignature({
secret,
payload: rawBodyUtf8,
signature: signatureHeader.trim(),
});
}
/**