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();
|
||||
}
|
||||
Reference in New Issue
Block a user