feat(omnl): settlement terminal, compliance gates, and HYBX integration foundation
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 <[email protected]>
This commit is contained in:
defiQUG
2026-06-07 21:51:32 -07:00
co-authored by Cursor
parent b6ddd236e2
commit d717b504a6
182 changed files with 10852 additions and 40 deletions
+7
View File
@@ -10,6 +10,13 @@ terraform.tfvars
kubeconfig
*.kubeconfig
# Python virtualenvs
.venv/
# OMNL terminal connection secrets / CIS source PDFs
config/omnl-terminal-connections/**/source/
config/omnl-terminal-connections/**/.env
# Keys and Secrets
*.key
*.pem
+42
View File
@@ -0,0 +1,42 @@
{
"$schema": "OMNL settlement terminal — operator document defaults",
"version": "1.0.0",
"defaultSettlementRef": "HYBX-BATCH-001",
"defaultOfficeId": 1,
"defaultCurrency": "USD",
"defaultValueDate": "2026-03-17",
"defaultBeneficiary": "Bank Kanaya (Indonesia)",
"defaultBeneficiaryOfficeId": 22,
"defaultBeneficiaryExternalId": "BANK-KANAYA-ID",
"defaultConnectionId": "mk-albert-gate-limited",
"omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.",
"omnlLei": "98450070C57395F6B906",
"pofPrimaryGlCode": "2100",
"debitNoteDefaultDebitGl": "2100",
"debitNoteDefaultCreditGl": "1410",
"tokenization": {
"chain138RpcEnv": "RPC_URL_138",
"defaultLineId": "USD-M1",
"sanitizedRedactFields": ["accountNumber", "iban", "swiftBic", "beneficiaryName", "orderingName"]
},
"alchemy": {
"mainnetNetwork": "eth-mainnet",
"chain138Network": "defi-oracle-meta-mainnet",
"allowedRpcMethods": [
"eth_blockNumber",
"eth_getBalance",
"eth_call",
"eth_estimateGas",
"eth_gasPrice",
"eth_getTransactionCount",
"eth_getTransactionReceipt",
"eth_getTransactionByHash"
],
"pricesApiPath": "/prices/v1/tokens/by-address"
},
"disclaimers": {
"terminalScreen": "Operator terminal copy bound to Fineract SoR and ISO 20022 store — not a SWIFT NET transmission.",
"pof": "M1 central-bank liability stock (GL 2100) — not unrestricted cash or nostro.",
"swiftCopy": "Internal conversion/remittance copy for audit — not MT103/MT202 wire confirmation."
}
}
@@ -0,0 +1,12 @@
# MK ALBERT GATE LIMITED — first receiver connection (from CIS 2026-05-29)
# Copy to .env in this directory; never commit .env
# Alchemy (Ethereum mainnet) — from CIS API ENDPOINT
MK_ALBERT_GATE_ALCHEMY_API_KEY=
# Receiver payment gateway — from CIS API Base URL + Auth Key
MK_ALBERT_GATE_RECEIVER_API_BASE_URL=https://banktransfer.devmindgroup.com/api
MK_ALBERT_GATE_RECEIVER_API_AUTH_KEY=
# Optional: override project dir (defaults to this folder)
# OMNL_TERMINAL_CONNECTION_DIR=/home/intlc/projects/proxmox/config/omnl-terminal-connections/mk-albert-gate-limited
@@ -0,0 +1,46 @@
# MK Albert Gate Limited — settlement terminal connection
First API + receiver profile for the OMNL/HYBX settlement terminal, sourced from the CIS (Client Information Sheet) dated **2026-05-29**.
## Project directory
```
config/omnl-terminal-connections/mk-albert-gate-limited/
├── connection.v1.json # Public metadata (no secrets)
├── .env.example # Secret env var names
├── .env # Operator copy (gitignored)
└── source/ # CIS PDF (gitignored)
```
## Terminal usage
```bash
# Load connection in API calls
curl -sS "http://127.0.0.1:3000/api/v1/omnl/terminal/package?connectionId=mk-albert-gate-limited"
# UI with connection pre-selected
/omnl/terminal?connectionId=mk-albert-gate-limited
```
## Secrets (operator only)
Set in this directory's `.env` or repo root `.env`:
| Variable | CIS field |
|----------|-----------|
| `MK_ALBERT_GATE_ALCHEMY_API_KEY` | Alchemy API ENDPOINT key |
| `MK_ALBERT_GATE_RECEIVER_API_BASE_URL` | API Base URL |
| `MK_ALBERT_GATE_RECEIVER_API_AUTH_KEY` | API Auth Key (TLS/SSL) |
## Receiver
| Field | Value |
|-------|--------|
| Server | DEV-CORE-PAY-GW-01 (Receiving Gateway) |
| IP | 15.204.109.60 |
| Wallet | `0xb3B416BdE671c256aAbFB56358baD91D46BB9c39` |
| Bank | UBS London — IBAN `GB26UBSW23232354681401`, SWIFT `UBSWGB2LXXX` |
## Session index
Operator session pointer: `terminals/mk-albert-gate-limited.session.json` (repo `reports/status/`).
@@ -0,0 +1,59 @@
{
"$schema": "OMNL settlement terminal — receiver / API connection profile (no secrets)",
"connectionId": "mk-albert-gate-limited",
"version": "1.0.0",
"cisDate": "2026-05-29",
"cisSource": "source/(CIS) CLIENT INFORMATION SHEET MK ALBERT GATE LIMITED..pdf",
"client": {
"companyName": "MK ALBERT GATE LIMITED",
"companyRegistrationNo": "102271",
"address": "2nd Floor International House, 41 The Parade, St Helier, Jersey JE2 3QQ",
"jurisdiction": "JE",
"representedBy": {
"name": "Khashoggi Mot Asem Almot Azbellahh",
"title": "CEO",
"passportNo": "T075922",
"passportCountry": "SA",
"passportIssue": "2021-04-30",
"passportExpiry": "2031-03-31"
}
},
"bank": {
"name": "UBS London UK",
"address": "AG London UK",
"accountName": "MK ALBERT GATE LIMITED",
"accountNumber": "23232354681401",
"iban": "GB26UBSW23232354681401",
"swiftBic": "UBSWGB2LXXX"
},
"receiver": {
"serverId": "DEV-CORE-PAY-GW-01",
"serverLabel": "Receiving Gateway",
"receiverIp": "15.204.109.60",
"protocol": "HTTPS REST API JSON",
"port": 443,
"tls": true,
"apiBaseUrlEnv": "MK_ALBERT_GATE_RECEIVER_API_BASE_URL",
"apiAuthKeyEnv": "MK_ALBERT_GATE_RECEIVER_API_AUTH_KEY",
"apiDocsUrl": "https://banktransfer.devmindgroup.com/api/docs"
},
"alchemy": {
"network": "eth-mainnet",
"rpcUrlTemplate": "https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}",
"apiKeyEnv": "MK_ALBERT_GATE_ALCHEMY_API_KEY",
"fallbackApiKeyEnv": "ALCHEMY_API_KEY"
},
"wallet": {
"chain": "ethereum",
"address": "0xb3B416BdE671c256aAbFB56358baD91D46BB9c39",
"purpose": "Receive tokens and collectibles on Ethereum mainnet"
},
"settlementDefaults": {
"settlementRef": "MK-ALBERT-GATE-20260529",
"beneficiary": "MK ALBERT GATE LIMITED",
"beneficiaryExternalId": "MK-ALBERT-GATE-LTD",
"currency": "USD"
},
"envFile": ".env",
"projectDirEnv": "OMNL_TERMINAL_CONNECTION_DIR"
}
+2 -2
View File
@@ -37,7 +37,7 @@ class BesuCactiConnector:
response.raise_for_status()
return response.json()
def deploy_contract(self, contract_abi: list, contract_bytecode: str, constructor_args: list = None) -> Dict[str, Any]:
def deploy_contract(self, contract_abi: list, contract_bytecode: str, constructor_args: Optional[list] = None) -> Dict[str, Any]:
"""Deploy contract via Cacti"""
url = f"{self.cactus_api_url}/api/v1/plugins/ledger-connector/besu/deploy-contract"
data = {
@@ -49,7 +49,7 @@ class BesuCactiConnector:
response.raise_for_status()
return response.json()
def invoke_contract(self, contract_address: str, contract_abi: list, method: str, args: list = None) -> Dict[str, Any]:
def invoke_contract(self, contract_address: str, contract_abi: list, method: str, args: Optional[list] = None) -> Dict[str, Any]:
"""Invoke contract method via Cacti"""
url = f"{self.cactus_api_url}/api/v1/plugins/ledger-connector/besu/invoke-contract"
data = {
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @notice Batch ERC-20 drops for EI matrix mainnet top-ups (cWUSDT / cWUSDC).
* @dev Multicall3 transferFrom is not viable on mainnet cW* (allowance does not persist for MC3).
* Operator prefunds this contract, then calls dropPrefunded() — ~100 recipients per tx.
* msg.sender must be owner (deployer EOA).
*/
contract EiMatrixMainnetBatchDrop {
using SafeERC20 for IERC20;
address public immutable owner;
event BatchDrop(address indexed token, address indexed operator, uint256 recipientCount, uint256 totalAmount);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "EiMatrixBatchDrop: not owner");
_;
}
/// @dev Transfer `amounts[i]` from this contract's token balance to `recipients[i]`.
function dropPrefunded(
address token,
address[] calldata recipients,
uint256[] calldata amounts
) external onlyOwner {
uint256 n = recipients.length;
require(n == amounts.length, "EiMatrixBatchDrop: length mismatch");
uint256 total;
for (uint256 i; i < n; ) {
uint256 amount = amounts[i];
total += amount;
IERC20(token).safeTransfer(recipients[i], amount);
unchecked {
++i;
}
}
emit BatchDrop(token, msg.sender, n, total);
}
/// @dev Pull total from owner via transferFrom then distribute (one tx if allowance to this contract works).
function pullAndDrop(
address token,
address[] calldata recipients,
uint256[] calldata amounts
) external onlyOwner {
uint256 n = recipients.length;
require(n == amounts.length, "EiMatrixBatchDrop: length mismatch");
uint256 total;
for (uint256 i; i < n; ) {
total += amounts[i];
unchecked {
++i;
}
}
IERC20(token).safeTransferFrom(owner, address(this), total);
for (uint256 i; i < n; ) {
IERC20(token).safeTransfer(recipients[i], amounts[i]);
unchecked {
++i;
}
}
emit BatchDrop(token, msg.sender, n, total);
}
/// @dev Recover stray tokens (operator only).
function sweep(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).safeTransfer(to, amount);
}
}
+3 -2
View File
@@ -23,12 +23,13 @@ DEPLOYMENT_LOG_DIR = os.path.join(os.path.dirname(__file__), '../../logs/deploym
os.makedirs(DEPLOYMENT_LOG_DIR, exist_ok=True)
def load_environments() -> Dict[str, Any]:
def load_environments() -> List[Dict[str, Any]]:
"""Load environments configuration from YAML file"""
try:
with open(ENVIRONMENTS_FILE, 'r') as f:
config = yaml.safe_load(f)
return config.get('environments', [])
environments = config.get('environments', []) if isinstance(config, dict) else []
return environments if isinstance(environments, list) else []
except Exception as e:
print(f"Error loading environments: {e}")
return []
+1 -1
View File
@@ -156,7 +156,7 @@ def get_deployment_status(environment_name: str) -> Dict[str, Any]:
return base_status
def get_metrics(environment_name: str, hours: int = 24) -> List[Dict[str, Any]]:
def get_metrics(environment_name: str, hours: int = 24) -> Dict[str, List[Dict[str, Any]]]:
"""Get metrics for an environment over time"""
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
@@ -0,0 +1,23 @@
export type ActorType = 'user' | 'service' | 'system' | 'operator';
export type AuditEvent = {
audit_event_id: string;
timestamp: string;
actor_type: ActorType;
actor_id: string;
tenant_id: string;
entity_id: string;
action: string;
resource_type: string;
resource_id: string;
correlation_id?: string;
request_id?: string;
before_hash?: string;
after_hash?: string;
decision?: string;
reason_code?: string;
metadata_redacted: Record<string, unknown>;
};
export declare function redactMetadata(metadata: Record<string, unknown>): Record<string, unknown>;
export declare function createAuditEvent(partial: Omit<AuditEvent, 'metadata_redacted' | 'timestamp'> & {
metadata?: Record<string, unknown>;
}): AuditEvent;
@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.redactMetadata = redactMetadata;
exports.createAuditEvent = createAuditEvent;
const SENSITIVE_KEY_PATTERNS = [
/password/i,
/secret/i,
/token/i,
/api[_-]?key/i,
/private[_-]?key/i,
/credential/i,
/ssn/i,
/iban/i,
/email/i,
/phone/i,
/address/i,
];
const REDACTED = '[REDACTED]';
function isSensitiveKey(key) {
return SENSITIVE_KEY_PATTERNS.some((p) => p.test(key));
}
function redactMetadata(metadata) {
const out = {};
for (const [key, value] of Object.entries(metadata)) {
if (isSensitiveKey(key)) {
out[key] = REDACTED;
continue;
}
if (value && typeof value === 'object' && !Array.isArray(value)) {
out[key] = redactMetadata(value);
}
else {
out[key] = value;
}
}
return out;
}
function createAuditEvent(partial) {
const { metadata, ...rest } = partial;
return {
...rest,
timestamp: new Date().toISOString(),
metadata_redacted: redactMetadata(metadata ?? {}),
};
}
@@ -0,0 +1,33 @@
export type ComplianceDecision = 'allow' | 'block' | 'review' | 'hold' | 'reject';
export type ComplianceEvaluationInput = {
entityId: string;
tenantId: string;
counterpartyId?: string;
amount: string;
currency: string;
transactionType: string;
riskHints?: string[];
};
export type ComplianceEvaluationResult = {
decision: ComplianceDecision;
risk_score: number;
reason_codes: string[];
evidence_refs: string[];
expires_at?: string;
manual_review_required: boolean;
audit_event_id: string;
};
export interface ComplianceDecisionEngine {
evaluateTransaction(input: ComplianceEvaluationInput): Promise<ComplianceEvaluationResult>;
evaluateEntity(entityId: string, tenantId: string): Promise<ComplianceEvaluationResult>;
evaluateCounterparty(counterpartyId: string, tenantId: string): Promise<ComplianceEvaluationResult>;
}
export declare class MockComplianceDecisionEngine implements ComplianceDecisionEngine {
private readonly blockAmountThreshold;
constructor(options?: {
blockAmountThreshold?: number;
});
evaluateTransaction(input: ComplianceEvaluationInput): Promise<ComplianceEvaluationResult>;
evaluateEntity(entityId: string, tenantId: string): Promise<ComplianceEvaluationResult>;
evaluateCounterparty(counterpartyId: string, tenantId: string): Promise<ComplianceEvaluationResult>;
}
@@ -0,0 +1,90 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockComplianceDecisionEngine = void 0;
let auditCounter = 0;
function nextAuditId() {
auditCounter += 1;
return `audit-mock-${auditCounter}`;
}
class MockComplianceDecisionEngine {
blockAmountThreshold;
constructor(options) {
this.blockAmountThreshold = options?.blockAmountThreshold ?? 1_000_000;
}
async evaluateTransaction(input) {
const amount = Number(input.amount);
const hints = input.riskHints ?? [];
if (hints.includes('sanctions_hit')) {
return {
decision: 'block',
risk_score: 100,
reason_codes: ['SANCTIONS_HIT'],
evidence_refs: ['mock-evidence-sanctions'],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
if (hints.includes('pep_match')) {
return {
decision: 'review',
risk_score: 75,
reason_codes: ['PEP_MATCH'],
evidence_refs: ['mock-evidence-pep'],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
if (amount >= this.blockAmountThreshold) {
return {
decision: 'hold',
risk_score: 60,
reason_codes: ['AMOUNT_THRESHOLD'],
evidence_refs: ['mock-evidence-threshold'],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
return {
decision: 'allow',
risk_score: 10,
reason_codes: ['CLEAR'],
evidence_refs: [],
manual_review_required: false,
audit_event_id: nextAuditId(),
};
}
async evaluateEntity(entityId, tenantId) {
if (entityId.startsWith('blocked-')) {
return {
decision: 'reject',
risk_score: 90,
reason_codes: ['ENTITY_BLOCKED'],
evidence_refs: [`mock-entity-${tenantId}-${entityId}`],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
return {
decision: 'allow',
risk_score: 5,
reason_codes: ['ENTITY_CLEAR'],
evidence_refs: [],
manual_review_required: false,
audit_event_id: nextAuditId(),
};
}
async evaluateCounterparty(counterpartyId, tenantId) {
if (counterpartyId.includes('high-risk')) {
return {
decision: 'review',
risk_score: 70,
reason_codes: ['COUNTERPARTY_HIGH_RISK'],
evidence_refs: [`mock-cp-${tenantId}`],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
return this.evaluateEntity(counterpartyId, tenantId);
}
}
exports.MockComplianceDecisionEngine = MockComplianceDecisionEngine;
@@ -0,0 +1,30 @@
export type EntityType = 'central_bank' | 'commercial_bank' | 'emi' | 'payment_institution' | 'custodian' | 'broker_dealer' | 'other';
export type RegulatoryStatus = 'licensed' | 'pending' | 'suspended' | 'offboarded';
export type RiskTier = 'low' | 'medium' | 'high' | 'prohibited';
export type EntityCapability = 'payment_initiation' | 'settlement' | 'treasury' | 'digital_asset' | 'reporting' | 'identity' | 'reconciliation';
export type RegulatedEntity = {
entityId: string;
tenantId: string;
legalName: string;
jurisdiction: string;
lei?: string;
regulatoryStatus: RegulatoryStatus;
entityType: EntityType;
riskTier: RiskTier;
enabledCapabilities: EntityCapability[];
credentialRef?: string;
contractualDocRefs?: string[];
limits?: {
dailyAmountLimit?: string;
perTransactionLimit?: string;
currency?: string;
};
offboardedAt?: string;
metadata?: Record<string, string>;
};
export type RegulatedEntityRegistry = {
entities: RegulatedEntity[];
getEntity(entityId: string, tenantId: string): RegulatedEntity | undefined;
};
export declare function createInMemoryEntityRegistry(entities: RegulatedEntity[]): RegulatedEntityRegistry;
export declare function assertTenantIsolation(actorTenantId: string, resourceTenantId: string): void;
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInMemoryEntityRegistry = createInMemoryEntityRegistry;
exports.assertTenantIsolation = assertTenantIsolation;
function createInMemoryEntityRegistry(entities) {
const index = new Map();
for (const e of entities) {
index.set(`${e.tenantId}:${e.entityId}`, e);
}
return {
entities,
getEntity(entityId, tenantId) {
return index.get(`${tenantId}:${entityId}`);
},
};
}
function assertTenantIsolation(actorTenantId, resourceTenantId) {
if (actorTenantId !== resourceTenantId) {
throw new Error('Tenant isolation violation');
}
}
@@ -0,0 +1,20 @@
import type { HybxClient } from './HybxClient';
import type { HybxPaymentRequest, HybxPaymentResponse, HybxSettlementEvent, HybxWebhookPayload } from './types';
import { type HybxConfig } from './config';
/**
* HTTP HYBX client — sandbox/staging only until official API spec is available.
* Does not implement request signing; paths are placeholders.
*/
export declare class HttpHybxClient implements HybxClient {
readonly environment: string;
private readonly config;
constructor(options?: {
config?: HybxConfig;
testMode?: boolean;
});
private postJson;
initiatePayment(request: HybxPaymentRequest): Promise<HybxPaymentResponse>;
getPaymentStatus(paymentId: string): Promise<HybxPaymentResponse>;
listSettlementEvents(since?: string): Promise<HybxSettlementEvent[]>;
parseWebhookPayload(body: string): HybxWebhookPayload;
}
@@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpHybxClient = void 0;
const config_1 = require("./config");
/**
* HTTP HYBX client — sandbox/staging only until official API spec is available.
* Does not implement request signing; paths are placeholders.
*/
class HttpHybxClient {
environment;
config;
constructor(options) {
this.config = options?.config ?? (0, config_1.loadHybxConfig)({ testMode: options?.testMode ?? true });
this.environment = this.config.environment;
if (this.config.environment === 'production') {
throw new Error('HttpHybxClient refuses production until official HYBX spec is integrated');
}
}
async postJson(path, body) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
try {
const res = await fetch(`${this.config.baseUrl}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.config.apiKey,
Authorization: `Bearer ${this.config.clientSecret}`,
},
body: JSON.stringify(body),
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`HYBX HTTP ${res.status}: ${await res.text()}`);
}
return (await res.json());
}
finally {
clearTimeout(timeout);
}
}
async initiatePayment(request) {
return this.postJson('/v1/payments', request);
}
async getPaymentStatus(paymentId) {
const res = await fetch(`${this.config.baseUrl}/v1/payments/${paymentId}`, {
headers: { 'X-API-Key': this.config.apiKey },
});
if (!res.ok)
throw new Error(`HYBX HTTP ${res.status}`);
return (await res.json());
}
async listSettlementEvents(since) {
const q = since ? `?since=${encodeURIComponent(since)}` : '';
const res = await fetch(`${this.config.baseUrl}/v1/settlement-events${q}`, {
headers: { 'X-API-Key': this.config.apiKey },
});
if (!res.ok)
throw new Error(`HYBX HTTP ${res.status}`);
return (await res.json());
}
parseWebhookPayload(body) {
const parsed = JSON.parse(body);
return {
eventType: String(parsed.eventType ?? 'unknown'),
eventId: String(parsed.eventId ?? ''),
timestamp: String(parsed.timestamp ?? new Date().toISOString()),
data: parsed.data ?? parsed,
};
}
}
exports.HttpHybxClient = HttpHybxClient;
@@ -0,0 +1,12 @@
import type { HybxPaymentRequest, HybxPaymentResponse, HybxSettlementEvent, HybxWebhookPayload } from './types';
/**
* HYBX API client contract.
* Official endpoint paths, auth scheme, and signing rules require HYBX operator documentation.
*/
export interface HybxClient {
readonly environment: string;
initiatePayment(request: HybxPaymentRequest): Promise<HybxPaymentResponse>;
getPaymentStatus(paymentId: string): Promise<HybxPaymentResponse>;
listSettlementEvents(since?: string): Promise<HybxSettlementEvent[]>;
parseWebhookPayload(body: string): HybxWebhookPayload;
}
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
import type { HybxClient } from './HybxClient';
import type { HybxPaymentRequest, HybxPaymentResponse, HybxSettlementEvent, HybxWebhookPayload } from './types';
export declare class MockHybxClient implements HybxClient {
readonly environment: string;
constructor(environment?: string);
initiatePayment(request: HybxPaymentRequest): Promise<HybxPaymentResponse>;
getPaymentStatus(paymentId: string): Promise<HybxPaymentResponse>;
listSettlementEvents(): Promise<HybxSettlementEvent[]>;
parseWebhookPayload(body: string): HybxWebhookPayload;
}
@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockHybxClient = void 0;
const payments = new Map();
class MockHybxClient {
environment;
constructor(environment = 'sandbox') {
this.environment = environment;
}
async initiatePayment(request) {
const paymentId = `mock-pay-${request.idempotencyKey.slice(0, 16)}`;
const existing = payments.get(request.idempotencyKey);
if (existing)
return existing;
const response = {
paymentId,
status: 'accepted',
uetr: `mock-uetr-${paymentId}`,
messageId: `mock-msg-${paymentId}`,
createdAt: new Date().toISOString(),
rawReference: request.endToEndId,
};
payments.set(request.idempotencyKey, response);
payments.set(paymentId, response);
return response;
}
async getPaymentStatus(paymentId) {
const found = payments.get(paymentId);
if (!found) {
return {
paymentId,
status: 'pending',
createdAt: new Date().toISOString(),
};
}
return found;
}
async listSettlementEvents() {
return Array.from(payments.values()).map((p) => ({
eventId: `evt-${p.paymentId}`,
paymentId: p.paymentId,
status: p.status,
settlementDate: new Date().toISOString().slice(0, 10),
timestamp: new Date().toISOString(),
}));
}
parseWebhookPayload(body) {
const parsed = JSON.parse(body);
return {
eventType: String(parsed.eventType ?? 'payment.status'),
eventId: String(parsed.eventId ?? `wh-${Date.now()}`),
timestamp: String(parsed.timestamp ?? new Date().toISOString()),
data: parsed.data ?? parsed,
};
}
}
exports.MockHybxClient = MockHybxClient;
+20
View File
@@ -0,0 +1,20 @@
import type { HybxEnvironment } from './types';
export type HybxConfig = {
environment: HybxEnvironment;
baseUrl: string;
clientId: string;
clientSecret: string;
apiKey: string;
mtlsCertPath?: string;
mtlsKeyPath?: string;
webhookSecret: string;
requestTimeoutMs: number;
maxRetries: number;
};
export type LoadHybxConfigOptions = {
/** When true, reject production-like base URLs (for local/test). */
testMode?: boolean;
env?: NodeJS.ProcessEnv;
};
export declare function loadHybxConfig(options?: LoadHybxConfigOptions): HybxConfig;
export declare function validateHybxConfigForLocalTest(config: HybxConfig): string[];
+66
View File
@@ -0,0 +1,66 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadHybxConfig = loadHybxConfig;
exports.validateHybxConfigForLocalTest = validateHybxConfigForLocalTest;
const PLACEHOLDER_PATTERNS = /^(placeholder|changeme|example|test|dummy|xxx*)$/i;
const PRODUCTION_URL_PATTERNS = /hybxfinance\.io|hybx\.(prod|production)/i;
function requireEnv(name, value) {
if (!value || value.trim() === '') {
throw new Error(`Missing required environment variable: ${name}`);
}
return value.trim();
}
function parseEnvironment(raw) {
const v = (raw ?? 'sandbox').toLowerCase();
if (v === 'sandbox' || v === 'staging' || v === 'production')
return v;
throw new Error(`Invalid HYBX_ENVIRONMENT: ${raw}`);
}
function loadHybxConfig(options = {}) {
const env = options.env ?? process.env;
const environment = parseEnvironment(env.HYBX_ENVIRONMENT);
const baseUrl = requireEnv('HYBX_BASE_URL', env.HYBX_BASE_URL);
if (options.testMode && PRODUCTION_URL_PATTERNS.test(baseUrl)) {
throw new Error('HYBX_BASE_URL appears production-like; use sandbox URL in test mode');
}
if (environment === 'production' && options.testMode) {
throw new Error('HYBX_ENVIRONMENT=production is not allowed in test mode');
}
const clientId = requireEnv('HYBX_CLIENT_ID', env.HYBX_CLIENT_ID);
const clientSecret = requireEnv('HYBX_CLIENT_SECRET', env.HYBX_CLIENT_SECRET);
const apiKey = requireEnv('HYBX_API_KEY', env.HYBX_API_KEY);
const webhookSecret = requireEnv('HYBX_WEBHOOK_SECRET', env.HYBX_WEBHOOK_SECRET);
for (const [key, val] of [
['HYBX_CLIENT_ID', clientId],
['HYBX_CLIENT_SECRET', clientSecret],
['HYBX_API_KEY', apiKey],
['HYBX_WEBHOOK_SECRET', webhookSecret],
]) {
if (!PLACEHOLDER_PATTERNS.test(val) && options.testMode && environment === 'sandbox') {
// Allow non-placeholder values in sandbox test mode but warn via validation helper
void key;
}
}
return {
environment,
baseUrl: baseUrl.replace(/\/$/, ''),
clientId,
clientSecret,
apiKey,
mtlsCertPath: env.HYBX_MTLS_CERT_PATH?.trim() || undefined,
mtlsKeyPath: env.HYBX_MTLS_KEY_PATH?.trim() || undefined,
webhookSecret,
requestTimeoutMs: Number(env.HYBX_REQUEST_TIMEOUT_MS ?? 30_000),
maxRetries: Number(env.HYBX_MAX_RETRIES ?? 3),
};
}
function validateHybxConfigForLocalTest(config) {
const warnings = [];
if (config.baseUrl.startsWith('http://')) {
warnings.push('HYBX_BASE_URL uses plain HTTP');
}
if (config.environment === 'production') {
warnings.push('HYBX_ENVIRONMENT is production');
}
return warnings;
}
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,49 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = require("node:test");
const strict_1 = __importDefault(require("node:assert/strict"));
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
/** Paths used by HttpHybxClient (must match placeholder OpenAPI). */
const HTTP_CLIENT_PATHS = [
{ method: 'POST', path: '/v1/payments' },
{ method: 'GET', pathPrefix: '/v1/payments/' },
{ method: 'GET', path: '/v1/settlement-events' },
];
function repoRootFromTestDir() {
// dist/hybx → proxmox root (5 levels up)
return (0, node_path_1.resolve)(__dirname, '../../../../..');
}
function loadPlaceholderOpenApi() {
const openApiPath = (0, node_path_1.resolve)(repoRootFromTestDir(), 'docs/api/openapi-hybx-integration-placeholder.yaml');
strict_1.default.ok((0, node_fs_1.existsSync)(openApiPath), `placeholder OpenAPI missing: ${openApiPath}`);
return (0, node_fs_1.readFileSync)(openApiPath, 'utf8');
}
(0, node_test_1.describe)('HYBX placeholder OpenAPI contract', () => {
(0, node_test_1.it)('declares HttpHybxClient payment and settlement paths', () => {
const spec = loadPlaceholderOpenApi();
for (const entry of HTTP_CLIENT_PATHS) {
if (entry.path) {
strict_1.default.match(spec, new RegExp(`^\\s+${entry.path.replace(/\//g, '\\/')}:\\s*$`, 'm'));
}
if (entry.pathPrefix) {
strict_1.default.match(spec, /\/v1\/payments\/\{paymentId\}:/);
}
}
});
(0, node_test_1.it)('defines initiatePayment and getPaymentStatus operationIds', () => {
const spec = loadPlaceholderOpenApi();
strict_1.default.match(spec, /operationId:\s*initiatePayment/);
strict_1.default.match(spec, /operationId:\s*getPaymentStatus/);
strict_1.default.match(spec, /operationId:\s*listSettlementEvents/);
});
(0, node_test_1.it)('requires HybxPaymentRequest core fields', () => {
const spec = loadPlaceholderOpenApi();
for (const field of ['idempotencyKey', 'entityId', 'tenantId', 'amount', 'currency']) {
strict_1.default.match(spec, new RegExp(`${field}:\\s*\\{`));
}
});
});
+38
View File
@@ -0,0 +1,38 @@
/** Placeholder HYBX API types — require official HYBX documentation for production. */
export type HybxEnvironment = 'sandbox' | 'staging' | 'production';
export type HybxPaymentStatus = 'pending' | 'accepted' | 'rejected' | 'settled' | 'returned';
export type HybxPaymentRequest = {
idempotencyKey: string;
entityId: string;
tenantId: string;
amount: string;
currency: string;
debtorAccountRef: string;
creditorAccountRef: string;
endToEndId?: string;
purposeCode?: string;
remittanceInformation?: string;
metadata?: Record<string, unknown>;
};
export type HybxPaymentResponse = {
paymentId: string;
status: HybxPaymentStatus;
uetr?: string;
messageId?: string;
createdAt: string;
rawReference?: string;
};
export type HybxSettlementEvent = {
eventId: string;
paymentId: string;
status: HybxPaymentStatus;
settlementDate?: string;
reasonCode?: string;
timestamp: string;
};
export type HybxWebhookPayload = {
eventType: string;
eventId: string;
timestamp: string;
data: Record<string, unknown>;
};
+3
View File
@@ -0,0 +1,3 @@
"use strict";
/** Placeholder HYBX API types — require official HYBX documentation for production. */
Object.defineProperty(exports, "__esModule", { value: true });
+14
View File
@@ -0,0 +1,14 @@
export * from './hybx/types';
export * from './hybx/HybxClient';
export * from './hybx/MockHybxClient';
export * from './hybx/config';
export * from './iso20022/CanonicalFinancialEvent';
export * from './entities/RegulatedEntity';
export * from './compliance/ComplianceDecisionEngine';
export * from './audit/AuditEvent';
export * from './resilience/idempotency';
export * from './observability/correlation';
export * from './webhooks/verifySignature';
export * from './hybx/HttpHybxClient';
export * from './reconciliation/ReconciliationStatus';
export * from './resilience/circuitBreaker';
+30
View File
@@ -0,0 +1,30 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./hybx/types"), exports);
__exportStar(require("./hybx/HybxClient"), exports);
__exportStar(require("./hybx/MockHybxClient"), exports);
__exportStar(require("./hybx/config"), exports);
__exportStar(require("./iso20022/CanonicalFinancialEvent"), exports);
__exportStar(require("./entities/RegulatedEntity"), exports);
__exportStar(require("./compliance/ComplianceDecisionEngine"), exports);
__exportStar(require("./audit/AuditEvent"), exports);
__exportStar(require("./resilience/idempotency"), exports);
__exportStar(require("./observability/correlation"), exports);
__exportStar(require("./webhooks/verifySignature"), exports);
__exportStar(require("./hybx/HttpHybxClient"), exports);
__exportStar(require("./reconciliation/ReconciliationStatus"), exports);
__exportStar(require("./resilience/circuitBreaker"), exports);
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,231 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = require("node:test");
const strict_1 = __importDefault(require("node:assert/strict"));
const MockHybxClient_1 = require("./hybx/MockHybxClient");
const config_1 = require("./hybx/config");
const ComplianceDecisionEngine_1 = require("./compliance/ComplianceDecisionEngine");
const AuditEvent_1 = require("./audit/AuditEvent");
const idempotency_1 = require("./resilience/idempotency");
const CanonicalFinancialEvent_1 = require("./iso20022/CanonicalFinancialEvent");
const RegulatedEntity_1 = require("./entities/RegulatedEntity");
const verifySignature_1 = require("./webhooks/verifySignature");
const HttpHybxClient_1 = require("./hybx/HttpHybxClient");
const ReconciliationStatus_1 = require("./reconciliation/ReconciliationStatus");
const circuitBreaker_1 = require("./resilience/circuitBreaker");
(0, node_test_1.describe)('MockHybxClient', () => {
(0, node_test_1.it)('returns typed mock payment response', async () => {
const client = new MockHybxClient_1.MockHybxClient('sandbox');
const res = await client.initiatePayment({
idempotencyKey: 'key-001',
entityId: 'ent-1',
tenantId: 'tenant-a',
amount: '100.00',
currency: 'USD',
debtorAccountRef: 'debtor-1',
creditorAccountRef: 'creditor-1',
});
strict_1.default.equal(res.status, 'accepted');
strict_1.default.ok(res.paymentId.startsWith('mock-pay-'));
strict_1.default.ok(res.uetr);
});
});
(0, node_test_1.describe)('loadHybxConfig', () => {
(0, node_test_1.it)('loads sandbox placeholders', () => {
const cfg = (0, config_1.loadHybxConfig)({
testMode: true,
env: {
HYBX_ENVIRONMENT: 'sandbox',
HYBX_BASE_URL: 'https://hybx-sandbox.example.invalid',
HYBX_CLIENT_ID: 'placeholder',
HYBX_CLIENT_SECRET: 'placeholder',
HYBX_API_KEY: 'placeholder',
HYBX_WEBHOOK_SECRET: 'placeholder',
},
});
strict_1.default.equal(cfg.environment, 'sandbox');
});
(0, node_test_1.it)('rejects production in test mode', () => {
strict_1.default.throws(() => (0, config_1.loadHybxConfig)({
testMode: true,
env: { HYBX_ENVIRONMENT: 'production', HYBX_BASE_URL: 'https://x' },
}));
});
});
(0, node_test_1.describe)('MockComplianceDecisionEngine', () => {
(0, node_test_1.it)('blocks sanctions hits', async () => {
const engine = new ComplianceDecisionEngine_1.MockComplianceDecisionEngine();
const res = await engine.evaluateTransaction({
entityId: 'e1',
tenantId: 't1',
amount: '10',
currency: 'USD',
transactionType: 'payment',
riskHints: ['sanctions_hit'],
});
strict_1.default.equal(res.decision, 'block');
});
(0, node_test_1.it)('reviews PEP matches', async () => {
const engine = new ComplianceDecisionEngine_1.MockComplianceDecisionEngine();
const res = await engine.evaluateTransaction({
entityId: 'e1',
tenantId: 't1',
amount: '10',
currency: 'USD',
transactionType: 'payment',
riskHints: ['pep_match'],
});
strict_1.default.equal(res.decision, 'review');
});
});
(0, node_test_1.describe)('audit redaction', () => {
(0, node_test_1.it)('removes secret-like and PII-like fields', () => {
const redacted = (0, AuditEvent_1.redactMetadata)({
api_key: 'sk-test-dummy',
email: '[email protected]',
amount: '100',
});
strict_1.default.equal(redacted.api_key, '[REDACTED]');
strict_1.default.equal(redacted.email, '[REDACTED]');
strict_1.default.equal(redacted.amount, '100');
});
(0, node_test_1.it)('creates audit event with redacted metadata', () => {
const evt = (0, AuditEvent_1.createAuditEvent)({
audit_event_id: 'a1',
actor_type: 'service',
actor_id: 'svc-1',
tenant_id: 't1',
entity_id: 'e1',
action: 'payment.initiated',
resource_type: 'payment',
resource_id: 'p1',
metadata: { client_secret: 'dummy-secret' },
});
strict_1.default.equal(evt.metadata_redacted.client_secret, '[REDACTED]');
});
});
(0, node_test_1.describe)('idempotency', () => {
(0, node_test_1.it)('returns stable keys for identical inputs', () => {
const input = {
tenantId: 't1',
entityId: 'e1',
operation: 'payment',
payload: { amount: '10' },
};
strict_1.default.equal((0, idempotency_1.deriveIdempotencyKey)(input), (0, idempotency_1.deriveIdempotencyKey)(input));
});
(0, node_test_1.it)('returns distinct keys for distinct inputs', () => {
const a = (0, idempotency_1.deriveIdempotencyKey)({
tenantId: 't1',
entityId: 'e1',
operation: 'payment',
});
const b = (0, idempotency_1.deriveIdempotencyKey)({
tenantId: 't2',
entityId: 'e1',
operation: 'payment',
});
strict_1.default.notEqual(a, b);
});
});
(0, node_test_1.describe)('CanonicalFinancialEvent', () => {
const valid = {
schema_version: '1.0',
message_id: 'msg-1',
end_to_end_id: 'e2e-1',
debtor: { id: 'd1' },
creditor: { id: 'c1' },
amount: '100.00',
currency: 'USD',
source_system: 'hybx',
target_system: 'omnl',
entity_id: 'e1',
tenant_id: 't1',
};
(0, node_test_1.it)('validates required fields', () => {
strict_1.default.equal((0, CanonicalFinancialEvent_1.validateCanonicalFinancialEvent)(valid).valid, true);
strict_1.default.equal((0, CanonicalFinancialEvent_1.validateCanonicalFinancialEvent)({}).valid, false);
});
});
(0, node_test_1.describe)('tenant isolation', () => {
(0, node_test_1.it)('throws on tenant mismatch', () => {
strict_1.default.throws(() => (0, RegulatedEntity_1.assertTenantIsolation)('t1', 't2'));
});
});
(0, node_test_1.describe)('HttpHybxClient', () => {
(0, node_test_1.it)('refuses production environment', () => {
strict_1.default.throws(() => new HttpHybxClient_1.HttpHybxClient({
testMode: false,
config: {
environment: 'production',
baseUrl: 'https://hybx.example.invalid',
clientId: 'p',
clientSecret: 'p',
apiKey: 'p',
webhookSecret: 'p',
requestTimeoutMs: 1000,
maxRetries: 1,
},
}), /refuses production/);
});
(0, node_test_1.it)('parses webhook payload shape', () => {
const client = new HttpHybxClient_1.HttpHybxClient({
testMode: true,
config: {
environment: 'sandbox',
baseUrl: 'https://hybx-sandbox.example.invalid',
clientId: 'placeholder',
clientSecret: 'placeholder',
apiKey: 'placeholder',
webhookSecret: 'placeholder',
requestTimeoutMs: 1000,
maxRetries: 1,
},
});
const parsed = client.parseWebhookPayload(JSON.stringify({ eventType: 'settlement.completed', eventId: 'evt-1', data: { amount: '100' } }));
strict_1.default.equal(parsed.eventType, 'settlement.completed');
strict_1.default.equal(parsed.eventId, 'evt-1');
strict_1.default.equal(parsed.data.amount, '100');
});
});
(0, node_test_1.describe)('MockReconciliationEngine', () => {
(0, node_test_1.it)('detects missing fineract refs', async () => {
const engine = new ReconciliationStatus_1.MockReconciliationEngine();
const snap = await engine.reconcileTripleState({
tenantId: 't1',
entityId: 'e1',
hybxRefs: ['ref-1'],
fineractRefs: [],
chainRefs: ['ref-1'],
});
strict_1.default.equal(snap.status, 'breaks_found');
strict_1.default.ok(snap.breaks.length >= 1);
});
});
(0, node_test_1.describe)('CircuitBreaker', () => {
(0, node_test_1.it)('opens after threshold failures', async () => {
const cb = new circuitBreaker_1.CircuitBreaker({ failureThreshold: 2, resetTimeoutMs: 60_000 });
const fail = () => cb.execute(async () => { throw new Error('fail'); });
await strict_1.default.rejects(fail);
await strict_1.default.rejects(fail);
strict_1.default.equal(cb.getState(), 'open');
});
});
(0, node_test_1.describe)('webhook verification', () => {
(0, node_test_1.it)('verifies HMAC signature', () => {
const secret = 'test-webhook-secret-dummy';
const payload = '{"event":"payment.settled"}';
const sig = (0, verifySignature_1.computeWebhookSignature)(secret, payload);
strict_1.default.equal((0, verifySignature_1.verifyWebhookSignature)({ secret, payload, signature: sig }), true);
strict_1.default.equal((0, verifySignature_1.verifyWebhookSignature)({ secret, payload, signature: 'bad' }), false);
});
(0, node_test_1.it)('verifies sha256= prefixed OMNL-style signatures', () => {
const secret = 'omnl-webhook-secret';
const payload = '{"event":"ReserveCommitted"}';
const sig = `sha256=${(0, verifySignature_1.computeWebhookSignature)(secret, payload)}`;
strict_1.default.equal((0, verifySignature_1.verifyWebhookSignature)({ secret, payload, signature: sig }), true);
});
});
@@ -0,0 +1,49 @@
/** Extended canonical financial event aligned to ISO 20022 concepts (not full XML). */
export type PartyAccount = {
id: string;
name?: string;
iban?: string;
bic?: string;
address?: string;
};
export type FinancialParty = {
id: string;
name?: string;
lei?: string;
jurisdiction?: string;
account?: PartyAccount;
};
export type ComplianceDecisionRef = 'allow' | 'block' | 'review' | 'hold' | 'reject' | 'pending';
export type CanonicalFinancialEvent = {
schema_version: string;
message_id: string;
end_to_end_id: string;
transaction_id?: string;
uetr?: string;
debtor: FinancialParty;
creditor: FinancialParty;
debtor_agent?: FinancialParty;
creditor_agent?: FinancialParty;
debtor_account?: PartyAccount;
creditor_account?: PartyAccount;
amount: string;
currency: string;
settlement_date?: string;
purpose_code?: string;
remittance_information?: string;
status?: string;
reason_code?: string;
source_system: string;
target_system: string;
entity_id: string;
tenant_id: string;
risk_score?: number;
compliance_decision?: ComplianceDecisionRef;
audit_event_id?: string;
raw_reference?: string;
};
export type CanonicalValidationResult = {
valid: boolean;
errors: string[];
};
export declare function validateCanonicalFinancialEvent(event: Partial<CanonicalFinancialEvent>): CanonicalValidationResult;
@@ -0,0 +1,39 @@
"use strict";
/** Extended canonical financial event aligned to ISO 20022 concepts (not full XML). */
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateCanonicalFinancialEvent = validateCanonicalFinancialEvent;
const REQUIRED_FIELDS = [
'schema_version',
'message_id',
'end_to_end_id',
'debtor',
'creditor',
'amount',
'currency',
'source_system',
'target_system',
'entity_id',
'tenant_id',
];
function validateCanonicalFinancialEvent(event) {
const errors = [];
for (const field of REQUIRED_FIELDS) {
const value = event[field];
if (value === undefined || value === null || value === '') {
errors.push(`Missing required field: ${field}`);
}
}
if (event.debtor && !event.debtor.id) {
errors.push('debtor.id is required');
}
if (event.creditor && !event.creditor.id) {
errors.push('creditor.id is required');
}
if (event.amount !== undefined && !/^\d+(\.\d+)?$/.test(event.amount)) {
errors.push('amount must be a numeric string');
}
if (event.currency !== undefined && !/^[A-Z]{3}$/.test(event.currency)) {
errors.push('currency must be a 3-letter ISO 4217 code');
}
return { valid: errors.length === 0, errors };
}
@@ -0,0 +1,12 @@
export declare const CORRELATION_ID_HEADER = "x-correlation-id";
export declare const REQUEST_ID_HEADER = "x-request-id";
export declare function generateCorrelationId(): string;
export declare function generateRequestId(): string;
export type CorrelationContext = {
correlationId: string;
requestId: string;
tenantId?: string;
entityId?: string;
};
export declare function createCorrelationContext(partial?: Partial<CorrelationContext>): CorrelationContext;
export declare function correlationHeaders(ctx: CorrelationContext): Record<string, string>;
@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.REQUEST_ID_HEADER = exports.CORRELATION_ID_HEADER = void 0;
exports.generateCorrelationId = generateCorrelationId;
exports.generateRequestId = generateRequestId;
exports.createCorrelationContext = createCorrelationContext;
exports.correlationHeaders = correlationHeaders;
const crypto_1 = require("crypto");
exports.CORRELATION_ID_HEADER = 'x-correlation-id';
exports.REQUEST_ID_HEADER = 'x-request-id';
function generateCorrelationId() {
return (0, crypto_1.randomUUID)();
}
function generateRequestId() {
return `req-${(0, crypto_1.randomUUID)()}`;
}
function orGenerated(value, fallback) {
const v = value?.trim();
return v ? v : fallback();
}
function createCorrelationContext(partial) {
return {
correlationId: orGenerated(partial?.correlationId, generateCorrelationId),
requestId: orGenerated(partial?.requestId, generateRequestId),
tenantId: partial?.tenantId,
entityId: partial?.entityId,
};
}
function correlationHeaders(ctx) {
const headers = {
[exports.CORRELATION_ID_HEADER]: ctx.correlationId,
[exports.REQUEST_ID_HEADER]: ctx.requestId,
};
if (ctx.tenantId)
headers['x-tenant-id'] = ctx.tenantId;
if (ctx.entityId)
headers['x-entity-id'] = ctx.entityId;
return headers;
}
@@ -0,0 +1,39 @@
export type ReconciliationBreakType = 'amount_mismatch' | 'missing_event' | 'duplicate_event' | 'status_mismatch' | 'ledger_drift' | 'chain_drift';
export type ReconciliationBreak = {
breakId: string;
type: ReconciliationBreakType;
source: 'hybx' | 'fineract' | 'chain' | 'internal';
referenceId: string;
expected?: string;
actual?: string;
detectedAt: string;
};
export type ReconciliationSnapshot = {
snapshotId: string;
tenantId: string;
entityId: string;
asOf: string;
hybxEventCount: number;
fineractPostingCount: number;
chainSettlementCount: number;
breaks: ReconciliationBreak[];
status: 'matched' | 'breaks_found' | 'pending';
};
export interface ReconciliationEngine {
reconcileTripleState(input: {
tenantId: string;
entityId: string;
hybxRefs: string[];
fineractRefs: string[];
chainRefs: string[];
}): Promise<ReconciliationSnapshot>;
}
export declare class MockReconciliationEngine implements ReconciliationEngine {
reconcileTripleState(input: {
tenantId: string;
entityId: string;
hybxRefs: string[];
fineractRefs: string[];
chainRefs: string[];
}): Promise<ReconciliationSnapshot>;
}
@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockReconciliationEngine = void 0;
class MockReconciliationEngine {
async reconcileTripleState(input) {
const breaks = [];
const hybxSet = new Set(input.hybxRefs);
const fineractSet = new Set(input.fineractRefs);
const chainSet = new Set(input.chainRefs);
for (const ref of input.hybxRefs) {
if (!fineractSet.has(ref)) {
breaks.push({
breakId: `brk-fineract-${ref}`,
type: 'missing_event',
source: 'fineract',
referenceId: ref,
detectedAt: new Date().toISOString(),
});
}
if (!chainSet.has(ref)) {
breaks.push({
breakId: `brk-chain-${ref}`,
type: 'missing_event',
source: 'chain',
referenceId: ref,
detectedAt: new Date().toISOString(),
});
}
}
for (const ref of input.fineractRefs) {
if (!hybxSet.has(ref)) {
breaks.push({
breakId: `brk-hybx-${ref}`,
type: 'missing_event',
source: 'hybx',
referenceId: ref,
detectedAt: new Date().toISOString(),
});
}
}
return {
snapshotId: `recon-${Date.now()}`,
tenantId: input.tenantId,
entityId: input.entityId,
asOf: new Date().toISOString(),
hybxEventCount: input.hybxRefs.length,
fineractPostingCount: input.fineractRefs.length,
chainSettlementCount: input.chainRefs.length,
breaks,
status: breaks.length === 0 ? 'matched' : 'breaks_found',
};
}
}
exports.MockReconciliationEngine = MockReconciliationEngine;
@@ -0,0 +1,16 @@
export type CircuitState = 'closed' | 'open' | 'half_open';
export type CircuitBreakerOptions = {
failureThreshold: number;
resetTimeoutMs: number;
};
export declare class CircuitBreaker {
private failures;
private state;
private openedAt;
private readonly options;
constructor(options?: Partial<CircuitBreakerOptions>);
getState(): CircuitState;
execute<T>(fn: () => Promise<T>): Promise<T>;
private onSuccess;
private onFailure;
}
@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CircuitBreaker = void 0;
class CircuitBreaker {
failures = 0;
state = 'closed';
openedAt = 0;
options;
constructor(options) {
this.options = {
failureThreshold: options?.failureThreshold ?? 5,
resetTimeoutMs: options?.resetTimeoutMs ?? 30_000,
};
}
getState() {
if (this.state === 'open' && Date.now() - this.openedAt >= this.options.resetTimeoutMs) {
this.state = 'half_open';
}
return this.state;
}
async execute(fn) {
const state = this.getState();
if (state === 'open') {
throw new Error('Circuit breaker open');
}
try {
const result = await fn();
this.onSuccess();
return result;
}
catch (e) {
this.onFailure();
throw e;
}
}
onSuccess() {
this.failures = 0;
this.state = 'closed';
}
onFailure() {
this.failures += 1;
if (this.failures >= this.options.failureThreshold) {
this.state = 'open';
this.openedAt = Date.now();
}
}
}
exports.CircuitBreaker = CircuitBreaker;
@@ -0,0 +1,13 @@
export type IdempotencyInput = {
tenantId: string;
entityId: string;
operation: string;
resourceId?: string;
payload?: Record<string, unknown>;
};
export declare function deriveIdempotencyKey(input: IdempotencyInput): string;
export declare class IdempotencyStore {
private readonly keys;
has(key: string): boolean;
record(key: string): boolean;
}
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdempotencyStore = void 0;
exports.deriveIdempotencyKey = deriveIdempotencyKey;
const crypto_1 = require("crypto");
function deriveIdempotencyKey(input) {
const canonical = JSON.stringify({
tenantId: input.tenantId,
entityId: input.entityId,
operation: input.operation,
resourceId: input.resourceId ?? '',
payload: input.payload ?? {},
});
return (0, crypto_1.createHash)('sha256').update(canonical).digest('hex');
}
class IdempotencyStore {
keys = new Set();
has(key) {
return this.keys.has(key);
}
record(key) {
if (this.keys.has(key))
return false;
this.keys.add(key);
return true;
}
}
exports.IdempotencyStore = IdempotencyStore;
@@ -0,0 +1,9 @@
export type WebhookVerifyOptions = {
secret: string;
payload: string;
signature: string;
algorithm?: 'sha256' | 'sha512';
encoding?: 'hex' | 'base64';
};
export declare function computeWebhookSignature(secret: string, payload: string, algorithm?: 'sha256' | 'sha512', encoding?: 'hex' | 'base64'): string;
export declare function verifyWebhookSignature(options: WebhookVerifyOptions): boolean;
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeWebhookSignature = computeWebhookSignature;
exports.verifyWebhookSignature = verifyWebhookSignature;
const crypto_1 = require("crypto");
function computeWebhookSignature(secret, payload, algorithm = 'sha256', encoding = 'hex') {
return (0, crypto_1.createHmac)(algorithm, secret).update(payload, 'utf8').digest(encoding);
}
function verifyWebhookSignature(options) {
const { secret, payload, signature, algorithm = 'sha256', encoding = 'hex' } = options;
if (!secret || !signature)
return false;
const expected = computeWebhookSignature(secret, payload, algorithm, encoding);
const provided = signature.startsWith('sha256=') || signature.startsWith('sha512=')
? signature.split('=')[1] ?? ''
: signature;
try {
const a = Buffer.from(expected, encoding);
const b = Buffer.from(provided, encoding);
if (a.length !== b.length)
return false;
return (0, crypto_1.timingSafeEqual)(a, b);
}
catch {
return false;
}
}
@@ -0,0 +1,17 @@
{
"name": "@dbis/integration-foundation",
"version": "0.1.0",
"private": true,
"description": "HYBX integration foundation: adapter interfaces, ISO 20022 canonical events, compliance, audit, resilience utilities",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"test": "node --test dist/integration-foundation.test.js dist/hybx/openapi-placeholder-contract.test.js"
},
"devDependencies": {
"@types/node": "^20.11.0",
"typescript": "^5.4.0"
}
}
@@ -0,0 +1,71 @@
export type ActorType = 'user' | 'service' | 'system' | 'operator';
export type AuditEvent = {
audit_event_id: string;
timestamp: string;
actor_type: ActorType;
actor_id: string;
tenant_id: string;
entity_id: string;
action: string;
resource_type: string;
resource_id: string;
correlation_id?: string;
request_id?: string;
before_hash?: string;
after_hash?: string;
decision?: string;
reason_code?: string;
metadata_redacted: Record<string, unknown>;
};
const SENSITIVE_KEY_PATTERNS = [
/password/i,
/secret/i,
/token/i,
/api[_-]?key/i,
/private[_-]?key/i,
/credential/i,
/ssn/i,
/iban/i,
/email/i,
/phone/i,
/address/i,
];
const REDACTED = '[REDACTED]';
function isSensitiveKey(key: string): boolean {
return SENSITIVE_KEY_PATTERNS.some((p) => p.test(key));
}
export function redactMetadata(
metadata: Record<string, unknown>
): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(metadata)) {
if (isSensitiveKey(key)) {
out[key] = REDACTED;
continue;
}
if (value && typeof value === 'object' && !Array.isArray(value)) {
out[key] = redactMetadata(value as Record<string, unknown>);
} else {
out[key] = value;
}
}
return out;
}
export function createAuditEvent(
partial: Omit<AuditEvent, 'metadata_redacted' | 'timestamp'> & {
metadata?: Record<string, unknown>;
}
): AuditEvent {
const { metadata, ...rest } = partial;
return {
...rest,
timestamp: new Date().toISOString(),
metadata_redacted: redactMetadata(metadata ?? {}),
};
}
@@ -0,0 +1,140 @@
export type ComplianceDecision =
| 'allow'
| 'block'
| 'review'
| 'hold'
| 'reject';
export type ComplianceEvaluationInput = {
entityId: string;
tenantId: string;
counterpartyId?: string;
amount: string;
currency: string;
transactionType: string;
riskHints?: string[];
};
export type ComplianceEvaluationResult = {
decision: ComplianceDecision;
risk_score: number;
reason_codes: string[];
evidence_refs: string[];
expires_at?: string;
manual_review_required: boolean;
audit_event_id: string;
};
export interface ComplianceDecisionEngine {
evaluateTransaction(input: ComplianceEvaluationInput): Promise<ComplianceEvaluationResult>;
evaluateEntity(entityId: string, tenantId: string): Promise<ComplianceEvaluationResult>;
evaluateCounterparty(
counterpartyId: string,
tenantId: string
): Promise<ComplianceEvaluationResult>;
}
let auditCounter = 0;
function nextAuditId(): string {
auditCounter += 1;
return `audit-mock-${auditCounter}`;
}
export class MockComplianceDecisionEngine implements ComplianceDecisionEngine {
private readonly blockAmountThreshold: number;
constructor(options?: { blockAmountThreshold?: number }) {
this.blockAmountThreshold = options?.blockAmountThreshold ?? 1_000_000;
}
async evaluateTransaction(
input: ComplianceEvaluationInput
): Promise<ComplianceEvaluationResult> {
const amount = Number(input.amount);
const hints = input.riskHints ?? [];
if (hints.includes('sanctions_hit')) {
return {
decision: 'block',
risk_score: 100,
reason_codes: ['SANCTIONS_HIT'],
evidence_refs: ['mock-evidence-sanctions'],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
if (hints.includes('pep_match')) {
return {
decision: 'review',
risk_score: 75,
reason_codes: ['PEP_MATCH'],
evidence_refs: ['mock-evidence-pep'],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
if (amount >= this.blockAmountThreshold) {
return {
decision: 'hold',
risk_score: 60,
reason_codes: ['AMOUNT_THRESHOLD'],
evidence_refs: ['mock-evidence-threshold'],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
return {
decision: 'allow',
risk_score: 10,
reason_codes: ['CLEAR'],
evidence_refs: [],
manual_review_required: false,
audit_event_id: nextAuditId(),
};
}
async evaluateEntity(
entityId: string,
tenantId: string
): Promise<ComplianceEvaluationResult> {
if (entityId.startsWith('blocked-')) {
return {
decision: 'reject',
risk_score: 90,
reason_codes: ['ENTITY_BLOCKED'],
evidence_refs: [`mock-entity-${tenantId}-${entityId}`],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
return {
decision: 'allow',
risk_score: 5,
reason_codes: ['ENTITY_CLEAR'],
evidence_refs: [],
manual_review_required: false,
audit_event_id: nextAuditId(),
};
}
async evaluateCounterparty(
counterpartyId: string,
tenantId: string
): Promise<ComplianceEvaluationResult> {
if (counterpartyId.includes('high-risk')) {
return {
decision: 'review',
risk_score: 70,
reason_codes: ['COUNTERPARTY_HIGH_RISK'],
evidence_refs: [`mock-cp-${tenantId}`],
manual_review_required: true,
audit_event_id: nextAuditId(),
};
}
return this.evaluateEntity(counterpartyId, tenantId);
}
}
@@ -0,0 +1,71 @@
export type EntityType =
| 'central_bank'
| 'commercial_bank'
| 'emi'
| 'payment_institution'
| 'custodian'
| 'broker_dealer'
| 'other';
export type RegulatoryStatus = 'licensed' | 'pending' | 'suspended' | 'offboarded';
export type RiskTier = 'low' | 'medium' | 'high' | 'prohibited';
export type EntityCapability =
| 'payment_initiation'
| 'settlement'
| 'treasury'
| 'digital_asset'
| 'reporting'
| 'identity'
| 'reconciliation';
export type RegulatedEntity = {
entityId: string;
tenantId: string;
legalName: string;
jurisdiction: string;
lei?: string;
regulatoryStatus: RegulatoryStatus;
entityType: EntityType;
riskTier: RiskTier;
enabledCapabilities: EntityCapability[];
credentialRef?: string;
contractualDocRefs?: string[];
limits?: {
dailyAmountLimit?: string;
perTransactionLimit?: string;
currency?: string;
};
offboardedAt?: string;
metadata?: Record<string, string>;
};
export type RegulatedEntityRegistry = {
entities: RegulatedEntity[];
getEntity(entityId: string, tenantId: string): RegulatedEntity | undefined;
};
export function createInMemoryEntityRegistry(
entities: RegulatedEntity[]
): RegulatedEntityRegistry {
const index = new Map<string, RegulatedEntity>();
for (const e of entities) {
index.set(`${e.tenantId}:${e.entityId}`, e);
}
return {
entities,
getEntity(entityId, tenantId) {
return index.get(`${tenantId}:${entityId}`);
},
};
}
export function assertTenantIsolation(
actorTenantId: string,
resourceTenantId: string
): void {
if (actorTenantId !== resourceTenantId) {
throw new Error('Tenant isolation violation');
}
}
@@ -0,0 +1,79 @@
import type { HybxClient } from './HybxClient';
import type {
HybxPaymentRequest,
HybxPaymentResponse,
HybxSettlementEvent,
HybxWebhookPayload,
} from './types';
import { loadHybxConfig, type HybxConfig } from './config';
/**
* HTTP HYBX client — sandbox/staging only until official API spec is available.
* Does not implement request signing; paths are placeholders.
*/
export class HttpHybxClient implements HybxClient {
readonly environment: string;
private readonly config: HybxConfig;
constructor(options?: { config?: HybxConfig; testMode?: boolean }) {
this.config = options?.config ?? loadHybxConfig({ testMode: options?.testMode ?? true });
this.environment = this.config.environment;
if (this.config.environment === 'production') {
throw new Error('HttpHybxClient refuses production until official HYBX spec is integrated');
}
}
private async postJson<T>(path: string, body: unknown): Promise<T> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
try {
const res = await fetch(`${this.config.baseUrl}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.config.apiKey,
Authorization: `Bearer ${this.config.clientSecret}`,
},
body: JSON.stringify(body),
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`HYBX HTTP ${res.status}: ${await res.text()}`);
}
return (await res.json()) as T;
} finally {
clearTimeout(timeout);
}
}
async initiatePayment(request: HybxPaymentRequest): Promise<HybxPaymentResponse> {
return this.postJson<HybxPaymentResponse>('/v1/payments', request);
}
async getPaymentStatus(paymentId: string): Promise<HybxPaymentResponse> {
const res = await fetch(`${this.config.baseUrl}/v1/payments/${paymentId}`, {
headers: { 'X-API-Key': this.config.apiKey },
});
if (!res.ok) throw new Error(`HYBX HTTP ${res.status}`);
return (await res.json()) as HybxPaymentResponse;
}
async listSettlementEvents(since?: string): Promise<HybxSettlementEvent[]> {
const q = since ? `?since=${encodeURIComponent(since)}` : '';
const res = await fetch(`${this.config.baseUrl}/v1/settlement-events${q}`, {
headers: { 'X-API-Key': this.config.apiKey },
});
if (!res.ok) throw new Error(`HYBX HTTP ${res.status}`);
return (await res.json()) as HybxSettlementEvent[];
}
parseWebhookPayload(body: string): HybxWebhookPayload {
const parsed = JSON.parse(body) as Record<string, unknown>;
return {
eventType: String(parsed.eventType ?? 'unknown'),
eventId: String(parsed.eventId ?? ''),
timestamp: String(parsed.timestamp ?? new Date().toISOString()),
data: (parsed.data as Record<string, unknown>) ?? parsed,
};
}
}
@@ -0,0 +1,22 @@
import type {
HybxPaymentRequest,
HybxPaymentResponse,
HybxSettlementEvent,
HybxWebhookPayload,
} from './types';
/**
* HYBX API client contract.
* Official endpoint paths, auth scheme, and signing rules require HYBX operator documentation.
*/
export interface HybxClient {
readonly environment: string;
initiatePayment(request: HybxPaymentRequest): Promise<HybxPaymentResponse>;
getPaymentStatus(paymentId: string): Promise<HybxPaymentResponse>;
listSettlementEvents(since?: string): Promise<HybxSettlementEvent[]>;
parseWebhookPayload(body: string): HybxWebhookPayload;
}
@@ -0,0 +1,67 @@
import type { HybxClient } from './HybxClient';
import type {
HybxPaymentRequest,
HybxPaymentResponse,
HybxSettlementEvent,
HybxWebhookPayload,
} from './types';
const payments = new Map<string, HybxPaymentResponse>();
export class MockHybxClient implements HybxClient {
readonly environment: string;
constructor(environment = 'sandbox') {
this.environment = environment;
}
async initiatePayment(request: HybxPaymentRequest): Promise<HybxPaymentResponse> {
const paymentId = `mock-pay-${request.idempotencyKey.slice(0, 16)}`;
const existing = payments.get(request.idempotencyKey);
if (existing) return existing;
const response: HybxPaymentResponse = {
paymentId,
status: 'accepted',
uetr: `mock-uetr-${paymentId}`,
messageId: `mock-msg-${paymentId}`,
createdAt: new Date().toISOString(),
rawReference: request.endToEndId,
};
payments.set(request.idempotencyKey, response);
payments.set(paymentId, response);
return response;
}
async getPaymentStatus(paymentId: string): Promise<HybxPaymentResponse> {
const found = payments.get(paymentId);
if (!found) {
return {
paymentId,
status: 'pending',
createdAt: new Date().toISOString(),
};
}
return found;
}
async listSettlementEvents(): Promise<HybxSettlementEvent[]> {
return Array.from(payments.values()).map((p) => ({
eventId: `evt-${p.paymentId}`,
paymentId: p.paymentId,
status: p.status,
settlementDate: new Date().toISOString().slice(0, 10),
timestamp: new Date().toISOString(),
}));
}
parseWebhookPayload(body: string): HybxWebhookPayload {
const parsed = JSON.parse(body) as Record<string, unknown>;
return {
eventType: String(parsed.eventType ?? 'payment.status'),
eventId: String(parsed.eventId ?? `wh-${Date.now()}`),
timestamp: String(parsed.timestamp ?? new Date().toISOString()),
data: (parsed.data as Record<string, unknown>) ?? parsed,
};
}
}
@@ -0,0 +1,91 @@
import type { HybxEnvironment } from './types';
export type HybxConfig = {
environment: HybxEnvironment;
baseUrl: string;
clientId: string;
clientSecret: string;
apiKey: string;
mtlsCertPath?: string;
mtlsKeyPath?: string;
webhookSecret: string;
requestTimeoutMs: number;
maxRetries: number;
};
const PLACEHOLDER_PATTERNS = /^(placeholder|changeme|example|test|dummy|xxx*)$/i;
const PRODUCTION_URL_PATTERNS = /hybxfinance\.io|hybx\.(prod|production)/i;
function requireEnv(name: string, value: string | undefined): string {
if (!value || value.trim() === '') {
throw new Error(`Missing required environment variable: ${name}`);
}
return value.trim();
}
function parseEnvironment(raw: string | undefined): HybxEnvironment {
const v = (raw ?? 'sandbox').toLowerCase();
if (v === 'sandbox' || v === 'staging' || v === 'production') return v;
throw new Error(`Invalid HYBX_ENVIRONMENT: ${raw}`);
}
export type LoadHybxConfigOptions = {
/** When true, reject production-like base URLs (for local/test). */
testMode?: boolean;
env?: NodeJS.ProcessEnv;
};
export function loadHybxConfig(options: LoadHybxConfigOptions = {}): HybxConfig {
const env = options.env ?? process.env;
const environment = parseEnvironment(env.HYBX_ENVIRONMENT);
const baseUrl = requireEnv('HYBX_BASE_URL', env.HYBX_BASE_URL);
if (options.testMode && PRODUCTION_URL_PATTERNS.test(baseUrl)) {
throw new Error('HYBX_BASE_URL appears production-like; use sandbox URL in test mode');
}
if (environment === 'production' && options.testMode) {
throw new Error('HYBX_ENVIRONMENT=production is not allowed in test mode');
}
const clientId = requireEnv('HYBX_CLIENT_ID', env.HYBX_CLIENT_ID);
const clientSecret = requireEnv('HYBX_CLIENT_SECRET', env.HYBX_CLIENT_SECRET);
const apiKey = requireEnv('HYBX_API_KEY', env.HYBX_API_KEY);
const webhookSecret = requireEnv('HYBX_WEBHOOK_SECRET', env.HYBX_WEBHOOK_SECRET);
for (const [key, val] of [
['HYBX_CLIENT_ID', clientId],
['HYBX_CLIENT_SECRET', clientSecret],
['HYBX_API_KEY', apiKey],
['HYBX_WEBHOOK_SECRET', webhookSecret],
] as const) {
if (!PLACEHOLDER_PATTERNS.test(val) && options.testMode && environment === 'sandbox') {
// Allow non-placeholder values in sandbox test mode but warn via validation helper
void key;
}
}
return {
environment,
baseUrl: baseUrl.replace(/\/$/, ''),
clientId,
clientSecret,
apiKey,
mtlsCertPath: env.HYBX_MTLS_CERT_PATH?.trim() || undefined,
mtlsKeyPath: env.HYBX_MTLS_KEY_PATH?.trim() || undefined,
webhookSecret,
requestTimeoutMs: Number(env.HYBX_REQUEST_TIMEOUT_MS ?? 30_000),
maxRetries: Number(env.HYBX_MAX_RETRIES ?? 3),
};
}
export function validateHybxConfigForLocalTest(config: HybxConfig): string[] {
const warnings: string[] = [];
if (config.baseUrl.startsWith('http://')) {
warnings.push('HYBX_BASE_URL uses plain HTTP');
}
if (config.environment === 'production') {
warnings.push('HYBX_ENVIRONMENT is production');
}
return warnings;
}
@@ -0,0 +1,50 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
/** Paths used by HttpHybxClient (must match placeholder OpenAPI). */
const HTTP_CLIENT_PATHS = [
{ method: 'POST', path: '/v1/payments' },
{ method: 'GET', pathPrefix: '/v1/payments/' },
{ method: 'GET', path: '/v1/settlement-events' },
];
function repoRootFromTestDir(): string {
// dist/hybx → proxmox root (5 levels up)
return resolve(__dirname, '../../../../..');
}
function loadPlaceholderOpenApi(): string {
const openApiPath = resolve(repoRootFromTestDir(), 'docs/api/openapi-hybx-integration-placeholder.yaml');
assert.ok(existsSync(openApiPath), `placeholder OpenAPI missing: ${openApiPath}`);
return readFileSync(openApiPath, 'utf8');
}
describe('HYBX placeholder OpenAPI contract', () => {
it('declares HttpHybxClient payment and settlement paths', () => {
const spec = loadPlaceholderOpenApi();
for (const entry of HTTP_CLIENT_PATHS) {
if (entry.path) {
assert.match(spec, new RegExp(`^\\s+${entry.path.replace(/\//g, '\\/')}:\\s*$`, 'm'));
}
if (entry.pathPrefix) {
assert.match(spec, /\/v1\/payments\/\{paymentId\}:/);
}
}
});
it('defines initiatePayment and getPaymentStatus operationIds', () => {
const spec = loadPlaceholderOpenApi();
assert.match(spec, /operationId:\s*initiatePayment/);
assert.match(spec, /operationId:\s*getPaymentStatus/);
assert.match(spec, /operationId:\s*listSettlementEvents/);
});
it('requires HybxPaymentRequest core fields', () => {
const spec = loadPlaceholderOpenApi();
for (const field of ['idempotencyKey', 'entityId', 'tenantId', 'amount', 'currency']) {
assert.match(spec, new RegExp(`${field}:\\s*\\{`));
}
});
});
@@ -0,0 +1,44 @@
/** Placeholder HYBX API types — require official HYBX documentation for production. */
export type HybxEnvironment = 'sandbox' | 'staging' | 'production';
export type HybxPaymentStatus = 'pending' | 'accepted' | 'rejected' | 'settled' | 'returned';
export type HybxPaymentRequest = {
idempotencyKey: string;
entityId: string;
tenantId: string;
amount: string;
currency: string;
debtorAccountRef: string;
creditorAccountRef: string;
endToEndId?: string;
purposeCode?: string;
remittanceInformation?: string;
metadata?: Record<string, unknown>;
};
export type HybxPaymentResponse = {
paymentId: string;
status: HybxPaymentStatus;
uetr?: string;
messageId?: string;
createdAt: string;
rawReference?: string;
};
export type HybxSettlementEvent = {
eventId: string;
paymentId: string;
status: HybxPaymentStatus;
settlementDate?: string;
reasonCode?: string;
timestamp: string;
};
export type HybxWebhookPayload = {
eventType: string;
eventId: string;
timestamp: string;
data: Record<string, unknown>;
};
@@ -0,0 +1,14 @@
export * from './hybx/types';
export * from './hybx/HybxClient';
export * from './hybx/MockHybxClient';
export * from './hybx/config';
export * from './iso20022/CanonicalFinancialEvent';
export * from './entities/RegulatedEntity';
export * from './compliance/ComplianceDecisionEngine';
export * from './audit/AuditEvent';
export * from './resilience/idempotency';
export * from './observability/correlation';
export * from './webhooks/verifySignature';
export * from './hybx/HttpHybxClient';
export * from './reconciliation/ReconciliationStatus';
export * from './resilience/circuitBreaker';
@@ -0,0 +1,261 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { MockHybxClient } from './hybx/MockHybxClient';
import { loadHybxConfig } from './hybx/config';
import { MockComplianceDecisionEngine } from './compliance/ComplianceDecisionEngine';
import { createAuditEvent, redactMetadata } from './audit/AuditEvent';
import { deriveIdempotencyKey } from './resilience/idempotency';
import {
validateCanonicalFinancialEvent,
type CanonicalFinancialEvent,
} from './iso20022/CanonicalFinancialEvent';
import { assertTenantIsolation } from './entities/RegulatedEntity';
import { verifyWebhookSignature, computeWebhookSignature } from './webhooks/verifySignature';
import { HttpHybxClient } from './hybx/HttpHybxClient';
import { MockReconciliationEngine } from './reconciliation/ReconciliationStatus';
import { CircuitBreaker } from './resilience/circuitBreaker';
describe('MockHybxClient', () => {
it('returns typed mock payment response', async () => {
const client = new MockHybxClient('sandbox');
const res = await client.initiatePayment({
idempotencyKey: 'key-001',
entityId: 'ent-1',
tenantId: 'tenant-a',
amount: '100.00',
currency: 'USD',
debtorAccountRef: 'debtor-1',
creditorAccountRef: 'creditor-1',
});
assert.equal(res.status, 'accepted');
assert.ok(res.paymentId.startsWith('mock-pay-'));
assert.ok(res.uetr);
});
});
describe('loadHybxConfig', () => {
it('loads sandbox placeholders', () => {
const cfg = loadHybxConfig({
testMode: true,
env: {
HYBX_ENVIRONMENT: 'sandbox',
HYBX_BASE_URL: 'https://hybx-sandbox.example.invalid',
HYBX_CLIENT_ID: 'placeholder',
HYBX_CLIENT_SECRET: 'placeholder',
HYBX_API_KEY: 'placeholder',
HYBX_WEBHOOK_SECRET: 'placeholder',
},
});
assert.equal(cfg.environment, 'sandbox');
});
it('rejects production in test mode', () => {
assert.throws(() =>
loadHybxConfig({
testMode: true,
env: { HYBX_ENVIRONMENT: 'production', HYBX_BASE_URL: 'https://x' },
})
);
});
});
describe('MockComplianceDecisionEngine', () => {
it('blocks sanctions hits', async () => {
const engine = new MockComplianceDecisionEngine();
const res = await engine.evaluateTransaction({
entityId: 'e1',
tenantId: 't1',
amount: '10',
currency: 'USD',
transactionType: 'payment',
riskHints: ['sanctions_hit'],
});
assert.equal(res.decision, 'block');
});
it('reviews PEP matches', async () => {
const engine = new MockComplianceDecisionEngine();
const res = await engine.evaluateTransaction({
entityId: 'e1',
tenantId: 't1',
amount: '10',
currency: 'USD',
transactionType: 'payment',
riskHints: ['pep_match'],
});
assert.equal(res.decision, 'review');
});
});
describe('audit redaction', () => {
it('removes secret-like and PII-like fields', () => {
const redacted = redactMetadata({
api_key: 'sk-test-dummy',
email: '[email protected]',
amount: '100',
});
assert.equal(redacted.api_key, '[REDACTED]');
assert.equal(redacted.email, '[REDACTED]');
assert.equal(redacted.amount, '100');
});
it('creates audit event with redacted metadata', () => {
const evt = createAuditEvent({
audit_event_id: 'a1',
actor_type: 'service',
actor_id: 'svc-1',
tenant_id: 't1',
entity_id: 'e1',
action: 'payment.initiated',
resource_type: 'payment',
resource_id: 'p1',
metadata: { client_secret: 'dummy-secret' },
});
assert.equal(evt.metadata_redacted.client_secret, '[REDACTED]');
});
});
describe('idempotency', () => {
it('returns stable keys for identical inputs', () => {
const input = {
tenantId: 't1',
entityId: 'e1',
operation: 'payment',
payload: { amount: '10' },
};
assert.equal(deriveIdempotencyKey(input), deriveIdempotencyKey(input));
});
it('returns distinct keys for distinct inputs', () => {
const a = deriveIdempotencyKey({
tenantId: 't1',
entityId: 'e1',
operation: 'payment',
});
const b = deriveIdempotencyKey({
tenantId: 't2',
entityId: 'e1',
operation: 'payment',
});
assert.notEqual(a, b);
});
});
describe('CanonicalFinancialEvent', () => {
const valid: CanonicalFinancialEvent = {
schema_version: '1.0',
message_id: 'msg-1',
end_to_end_id: 'e2e-1',
debtor: { id: 'd1' },
creditor: { id: 'c1' },
amount: '100.00',
currency: 'USD',
source_system: 'hybx',
target_system: 'omnl',
entity_id: 'e1',
tenant_id: 't1',
};
it('validates required fields', () => {
assert.equal(validateCanonicalFinancialEvent(valid).valid, true);
assert.equal(validateCanonicalFinancialEvent({}).valid, false);
});
});
describe('tenant isolation', () => {
it('throws on tenant mismatch', () => {
assert.throws(() => assertTenantIsolation('t1', 't2'));
});
});
describe('HttpHybxClient', () => {
it('refuses production environment', () => {
assert.throws(
() =>
new HttpHybxClient({
testMode: false,
config: {
environment: 'production',
baseUrl: 'https://hybx.example.invalid',
clientId: 'p',
clientSecret: 'p',
apiKey: 'p',
webhookSecret: 'p',
requestTimeoutMs: 1000,
maxRetries: 1,
},
}),
/refuses production/
);
});
it('parses webhook payload shape', () => {
const client = new HttpHybxClient({
testMode: true,
config: {
environment: 'sandbox',
baseUrl: 'https://hybx-sandbox.example.invalid',
clientId: 'placeholder',
clientSecret: 'placeholder',
apiKey: 'placeholder',
webhookSecret: 'placeholder',
requestTimeoutMs: 1000,
maxRetries: 1,
},
});
const parsed = client.parseWebhookPayload(
JSON.stringify({ eventType: 'settlement.completed', eventId: 'evt-1', data: { amount: '100' } })
);
assert.equal(parsed.eventType, 'settlement.completed');
assert.equal(parsed.eventId, 'evt-1');
assert.equal((parsed.data as { amount: string }).amount, '100');
});
});
describe('MockReconciliationEngine', () => {
it('detects missing fineract refs', async () => {
const engine = new MockReconciliationEngine();
const snap = await engine.reconcileTripleState({
tenantId: 't1',
entityId: 'e1',
hybxRefs: ['ref-1'],
fineractRefs: [],
chainRefs: ['ref-1'],
});
assert.equal(snap.status, 'breaks_found');
assert.ok(snap.breaks.length >= 1);
});
});
describe('CircuitBreaker', () => {
it('opens after threshold failures', async () => {
const cb = new CircuitBreaker({ failureThreshold: 2, resetTimeoutMs: 60_000 });
const fail = () => cb.execute(async () => { throw new Error('fail'); });
await assert.rejects(fail);
await assert.rejects(fail);
assert.equal(cb.getState(), 'open');
});
});
describe('webhook verification', () => {
it('verifies HMAC signature', () => {
const secret = 'test-webhook-secret-dummy';
const payload = '{"event":"payment.settled"}';
const sig = computeWebhookSignature(secret, payload);
assert.equal(
verifyWebhookSignature({ secret, payload, signature: sig }),
true
);
assert.equal(
verifyWebhookSignature({ secret, payload, signature: 'bad' }),
false
);
});
it('verifies sha256= prefixed OMNL-style signatures', () => {
const secret = 'omnl-webhook-secret';
const payload = '{"event":"ReserveCommitted"}';
const sig = `sha256=${computeWebhookSignature(secret, payload)}`;
assert.equal(verifyWebhookSignature({ secret, payload, signature: sig }), true);
});
});
@@ -0,0 +1,97 @@
/** Extended canonical financial event aligned to ISO 20022 concepts (not full XML). */
export type PartyAccount = {
id: string;
name?: string;
iban?: string;
bic?: string;
address?: string;
};
export type FinancialParty = {
id: string;
name?: string;
lei?: string;
jurisdiction?: string;
account?: PartyAccount;
};
export type ComplianceDecisionRef = 'allow' | 'block' | 'review' | 'hold' | 'reject' | 'pending';
export type CanonicalFinancialEvent = {
schema_version: string;
message_id: string;
end_to_end_id: string;
transaction_id?: string;
uetr?: string;
debtor: FinancialParty;
creditor: FinancialParty;
debtor_agent?: FinancialParty;
creditor_agent?: FinancialParty;
debtor_account?: PartyAccount;
creditor_account?: PartyAccount;
amount: string;
currency: string;
settlement_date?: string;
purpose_code?: string;
remittance_information?: string;
status?: string;
reason_code?: string;
source_system: string;
target_system: string;
entity_id: string;
tenant_id: string;
risk_score?: number;
compliance_decision?: ComplianceDecisionRef;
audit_event_id?: string;
raw_reference?: string;
};
export type CanonicalValidationResult = {
valid: boolean;
errors: string[];
};
const REQUIRED_FIELDS: (keyof CanonicalFinancialEvent)[] = [
'schema_version',
'message_id',
'end_to_end_id',
'debtor',
'creditor',
'amount',
'currency',
'source_system',
'target_system',
'entity_id',
'tenant_id',
];
export function validateCanonicalFinancialEvent(
event: Partial<CanonicalFinancialEvent>
): CanonicalValidationResult {
const errors: string[] = [];
for (const field of REQUIRED_FIELDS) {
const value = event[field];
if (value === undefined || value === null || value === '') {
errors.push(`Missing required field: ${field}`);
}
}
if (event.debtor && !event.debtor.id) {
errors.push('debtor.id is required');
}
if (event.creditor && !event.creditor.id) {
errors.push('creditor.id is required');
}
if (event.amount !== undefined && !/^\d+(\.\d+)?$/.test(event.amount)) {
errors.push('amount must be a numeric string');
}
if (event.currency !== undefined && !/^[A-Z]{3}$/.test(event.currency)) {
errors.push('currency must be a 3-letter ISO 4217 code');
}
return { valid: errors.length === 0, errors };
}
@@ -0,0 +1,18 @@
# ISO 20022 message family mappings (canonical layer)
Lightweight mapping from ISO 20022 families to `@dbis/integration-foundation` `CanonicalFinancialEvent`.
Full XML generation and XSD validation are **future work**.
| ISO family | Direction | Canonical fields populated |
|------------|-----------|---------------------------|
| pain.001 | Outbound initiation | message_id, end_to_end_id, debtor, creditor, amount, currency, purpose_code, remittance_information |
| pain.002 | Status report | message_id, status, reason_code, end_to_end_id |
| pacs.008 | FI-to-FI credit transfer | message_id, end_to_end_id, uetr, debtor/creditor agents & accounts, amount, currency, settlement_date |
| pacs.009 | FI-to-FI cover payment | message_id, end_to_end_id, uetr, debtor_agent, creditor_agent, amount, currency |
| pacs.002 | Payment status | message_id, status, reason_code, end_to_end_id, uetr |
| pacs.004 | Payment return | message_id, end_to_end_id, reason_code, amount, currency |
| camt.052 | Intraday report | message_id, settlement_date, account balances (raw_reference) |
| camt.053 | Statement | message_id, settlement_date, raw_reference |
| camt.054 | Debit/credit notification | message_id, end_to_end_id, amount, currency, remittance_information |
**Unsupported in v0.1:** Full party address decomposition, charge bearer, structured remittance arrays, official schema validation.
@@ -0,0 +1,45 @@
import { randomUUID } from 'crypto';
export const CORRELATION_ID_HEADER = 'x-correlation-id';
export const REQUEST_ID_HEADER = 'x-request-id';
export function generateCorrelationId(): string {
return randomUUID();
}
export function generateRequestId(): string {
return `req-${randomUUID()}`;
}
export type CorrelationContext = {
correlationId: string;
requestId: string;
tenantId?: string;
entityId?: string;
};
function orGenerated(value: string | undefined, fallback: () => string): string {
const v = value?.trim();
return v ? v : fallback();
}
export function createCorrelationContext(
partial?: Partial<CorrelationContext>
): CorrelationContext {
return {
correlationId: orGenerated(partial?.correlationId, generateCorrelationId),
requestId: orGenerated(partial?.requestId, generateRequestId),
tenantId: partial?.tenantId,
entityId: partial?.entityId,
};
}
export function correlationHeaders(ctx: CorrelationContext): Record<string, string> {
const headers: Record<string, string> = {
[CORRELATION_ID_HEADER]: ctx.correlationId,
[REQUEST_ID_HEADER]: ctx.requestId,
};
if (ctx.tenantId) headers['x-tenant-id'] = ctx.tenantId;
if (ctx.entityId) headers['x-entity-id'] = ctx.entityId;
return headers;
}
@@ -0,0 +1,99 @@
export type ReconciliationBreakType =
| 'amount_mismatch'
| 'missing_event'
| 'duplicate_event'
| 'status_mismatch'
| 'ledger_drift'
| 'chain_drift';
export type ReconciliationBreak = {
breakId: string;
type: ReconciliationBreakType;
source: 'hybx' | 'fineract' | 'chain' | 'internal';
referenceId: string;
expected?: string;
actual?: string;
detectedAt: string;
};
export type ReconciliationSnapshot = {
snapshotId: string;
tenantId: string;
entityId: string;
asOf: string;
hybxEventCount: number;
fineractPostingCount: number;
chainSettlementCount: number;
breaks: ReconciliationBreak[];
status: 'matched' | 'breaks_found' | 'pending';
};
export interface ReconciliationEngine {
reconcileTripleState(input: {
tenantId: string;
entityId: string;
hybxRefs: string[];
fineractRefs: string[];
chainRefs: string[];
}): Promise<ReconciliationSnapshot>;
}
export class MockReconciliationEngine implements ReconciliationEngine {
async reconcileTripleState(input: {
tenantId: string;
entityId: string;
hybxRefs: string[];
fineractRefs: string[];
chainRefs: string[];
}): Promise<ReconciliationSnapshot> {
const breaks: ReconciliationBreak[] = [];
const hybxSet = new Set(input.hybxRefs);
const fineractSet = new Set(input.fineractRefs);
const chainSet = new Set(input.chainRefs);
for (const ref of input.hybxRefs) {
if (!fineractSet.has(ref)) {
breaks.push({
breakId: `brk-fineract-${ref}`,
type: 'missing_event',
source: 'fineract',
referenceId: ref,
detectedAt: new Date().toISOString(),
});
}
if (!chainSet.has(ref)) {
breaks.push({
breakId: `brk-chain-${ref}`,
type: 'missing_event',
source: 'chain',
referenceId: ref,
detectedAt: new Date().toISOString(),
});
}
}
for (const ref of input.fineractRefs) {
if (!hybxSet.has(ref)) {
breaks.push({
breakId: `brk-hybx-${ref}`,
type: 'missing_event',
source: 'hybx',
referenceId: ref,
detectedAt: new Date().toISOString(),
});
}
}
return {
snapshotId: `recon-${Date.now()}`,
tenantId: input.tenantId,
entityId: input.entityId,
asOf: new Date().toISOString(),
hybxEventCount: input.hybxRefs.length,
fineractPostingCount: input.fineractRefs.length,
chainSettlementCount: input.chainRefs.length,
breaks,
status: breaks.length === 0 ? 'matched' : 'breaks_found',
};
}
}
@@ -0,0 +1,55 @@
export type CircuitState = 'closed' | 'open' | 'half_open';
export type CircuitBreakerOptions = {
failureThreshold: number;
resetTimeoutMs: number;
};
export class CircuitBreaker {
private failures = 0;
private state: CircuitState = 'closed';
private openedAt = 0;
private readonly options: CircuitBreakerOptions;
constructor(options?: Partial<CircuitBreakerOptions>) {
this.options = {
failureThreshold: options?.failureThreshold ?? 5,
resetTimeoutMs: options?.resetTimeoutMs ?? 30_000,
};
}
getState(): CircuitState {
if (this.state === 'open' && Date.now() - this.openedAt >= this.options.resetTimeoutMs) {
this.state = 'half_open';
}
return this.state;
}
async execute<T>(fn: () => Promise<T>): Promise<T> {
const state = this.getState();
if (state === 'open') {
throw new Error('Circuit breaker open');
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (e) {
this.onFailure();
throw e;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = 'closed';
}
private onFailure(): void {
this.failures += 1;
if (this.failures >= this.options.failureThreshold) {
this.state = 'open';
this.openedAt = Date.now();
}
}
}
@@ -0,0 +1,34 @@
import { createHash } from 'crypto';
export type IdempotencyInput = {
tenantId: string;
entityId: string;
operation: string;
resourceId?: string;
payload?: Record<string, unknown>;
};
export function deriveIdempotencyKey(input: IdempotencyInput): string {
const canonical = JSON.stringify({
tenantId: input.tenantId,
entityId: input.entityId,
operation: input.operation,
resourceId: input.resourceId ?? '',
payload: input.payload ?? {},
});
return createHash('sha256').update(canonical).digest('hex');
}
export class IdempotencyStore {
private readonly keys = new Set<string>();
has(key: string): boolean {
return this.keys.has(key);
}
record(key: string): boolean {
if (this.keys.has(key)) return false;
this.keys.add(key);
return true;
}
}
@@ -0,0 +1,37 @@
import { createHmac, timingSafeEqual } from 'crypto';
export type WebhookVerifyOptions = {
secret: string;
payload: string;
signature: string;
algorithm?: 'sha256' | 'sha512';
encoding?: 'hex' | 'base64';
};
export function computeWebhookSignature(
secret: string,
payload: string,
algorithm: 'sha256' | 'sha512' = 'sha256',
encoding: 'hex' | 'base64' = 'hex'
): string {
return createHmac(algorithm, secret).update(payload, 'utf8').digest(encoding);
}
export function verifyWebhookSignature(options: WebhookVerifyOptions): boolean {
const { secret, payload, signature, algorithm = 'sha256', encoding = 'hex' } = options;
if (!secret || !signature) return false;
const expected = computeWebhookSignature(secret, payload, algorithm, encoding);
const provided = signature.startsWith('sha256=') || signature.startsWith('sha512=')
? signature.split('=')[1] ?? ''
: signature;
try {
const a = Buffer.from(expected, encoding);
const b = Buffer.from(provided, encoding);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
} catch {
return false;
}
}
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
+12 -1
View File
@@ -16,7 +16,18 @@ import base64
# Import base configuration tool
sys.path.insert(0, str(Path(__file__).parent))
from configure_network import NetworkConfigurator, Colors, print_header, print_success, print_error, print_warning, print_info
from configure_network import (
NetworkConfigurator,
Colors,
print_header,
print_success,
print_error,
print_warning,
print_info,
input_yes_no,
input_int,
input_with_default,
)
class AdvancedNetworkConfigurator(NetworkConfigurator):
"""Extended network configurator with advanced options"""
+4 -1
View File
@@ -135,7 +135,10 @@ class ConfigurationValidator:
if not self._validate_cidr(subnet_cidr):
self.errors.append(f"Invalid {subnet_name} subnet: {subnet_cidr}. Must be valid CIDR notation")
else:
subnet_network, subnet_mask = self._parse_cidr(subnet_cidr)
parsed_subnet = self._parse_cidr(subnet_cidr)
if parsed_subnet is None:
continue
subnet_network, subnet_mask = parsed_subnet
if subnet_network and not self._is_subnet_of(subnet_network, subnet_mask, vnet_network, vnet_mask):
self.errors.append(f"{subnet_name} subnet {subnet_cidr} is not within VNet {vnet_address}")
if subnet_mask < vnet_mask:
+5 -3
View File
@@ -80,7 +80,7 @@ def input_with_default(prompt: str, default: str = "", validate_func=None) -> st
continue
return value
def input_int(prompt: str, default: int = 0, min_val: int = None, max_val: int = None) -> int:
def input_int(prompt: str, default: int = 0, min_val: Optional[int] = None, max_val: Optional[int] = None) -> int:
"""Get integer input with validation"""
while True:
try:
@@ -161,7 +161,7 @@ def validate_chain_id(chain_id: str) -> bool:
class NetworkConfigurator:
def __init__(self, project_root: Path):
self.project_root = project_root
self.config = {}
self.config: Dict[str, Any] = {}
self.backup_dir = project_root / ".config-backup"
def backup_existing_files(self):
@@ -252,12 +252,14 @@ class NetworkConfigurator:
# Subnets
print_info("\nSubnet Configuration")
self.config['network']['subnets'] = {
network_cfg: Dict[str, Any] = dict(self.config['network'])
network_cfg['subnets'] = {
'validators': input_with_default("Validators subnet (CIDR)", "10.0.1.0/24", validate_cidr),
'sentries': input_with_default("Sentries subnet (CIDR)", "10.0.2.0/24", validate_cidr),
'rpc': input_with_default("RPC subnet (CIDR)", "10.0.3.0/24", validate_cidr),
'aks': input_with_default("AKS subnet (CIDR)", "10.0.4.0/24", validate_cidr),
}
self.config['network'] = network_cfg
# Node counts
print_info("\nNode Configuration")
+48
View File
@@ -0,0 +1,48 @@
"""Import shim for configure-network.py (hyphenated filename)."""
from __future__ import annotations
import importlib.util
from pathlib import Path
from typing import Any, Callable
_impl_path = Path(__file__).with_name("configure-network.py")
_spec = importlib.util.spec_from_file_location("_configure_network_impl", _impl_path)
if _spec is None or _spec.loader is None:
raise ImportError(f"Cannot load {_impl_path}")
_impl = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_impl)
NetworkConfigurator = _impl.NetworkConfigurator
Colors = _impl.Colors
print_header: Callable[..., None] = _impl.print_header
print_success: Callable[..., None] = _impl.print_success
print_error: Callable[..., None] = _impl.print_error
print_warning: Callable[..., None] = _impl.print_warning
print_info: Callable[..., None] = _impl.print_info
input_with_default: Callable[..., str] = _impl.input_with_default
input_int: Callable[..., int] = _impl.input_int
input_yes_no: Callable[..., bool] = _impl.input_yes_no
input_hex: Callable[..., str] = _impl.input_hex
validate_ip: Callable[[str], bool] = _impl.validate_ip
validate_cidr: Callable[[str], bool] = _impl.validate_cidr
validate_port: Callable[[str], bool] = _impl.validate_port
validate_chain_id: Callable[[str], bool] = _impl.validate_chain_id
__all__ = [
"NetworkConfigurator",
"Colors",
"print_header",
"print_success",
"print_error",
"print_warning",
"print_info",
"input_with_default",
"input_int",
"input_yes_no",
"input_hex",
"validate_ip",
"validate_cidr",
"validate_port",
"validate_chain_id",
]
+17
View File
@@ -0,0 +1,17 @@
"""Import shim for configure-network-validation.py (hyphenated filename)."""
from __future__ import annotations
import importlib.util
from pathlib import Path
_impl_path = Path(__file__).with_name("configure-network-validation.py")
_spec = importlib.util.spec_from_file_location("_configure_network_validation_impl", _impl_path)
if _spec is None or _spec.loader is None:
raise ImportError(f"Cannot load {_impl_path}")
_impl = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_impl)
ConfigurationValidator = _impl.ConfigurationValidator
ValidationError = _impl.ValidationError
__all__ = ["ConfigurationValidator", "ValidationError"]
@@ -9,6 +9,7 @@ import re
import sys
from collections import Counter
from pathlib import Path
from typing import Any, cast
EXCLUDED_PARTS = {"artifacts", "broadcast", "cache", "lib", "node_modules", "out"}
@@ -96,8 +97,10 @@ def create_report(repo_root: Path) -> dict[str, object]:
}
def print_text(report: dict[str, object]) -> None:
summary = report["summary"]
def print_text(report: dict[str, Any]) -> None:
summary = cast(dict[str, int], report["summary"])
unreachable_by_bucket = cast(dict[str, int], report["unreachableByBucket"])
unreachable_contracts = cast(list[str], report["unreachableContracts"])
print(f"repo root: {report['repoRoot']}")
print(f"contracts total: {summary['contractsTotal']}")
print(f"tests/scripts roots total: {summary['rootsTotal']}")
@@ -106,11 +109,11 @@ def print_text(report: dict[str, object]) -> None:
print(f"contracts with no local inbound refs: {summary['contractsWithNoLocalInboundRefs']}")
print()
print("Unreachable by top-level bucket:")
for bucket, count in report["unreachableByBucket"].items():
for bucket, count in unreachable_by_bucket.items():
print(f" {bucket}: {count}")
print()
print("Archive candidates (unreachable from current tests/scripts):")
for contract in report["unreachableContracts"]:
for contract in unreachable_contracts:
print(f" {contract}")
print()
print("Note: unreachable does not prove safe deletion; it only means this repo's current Solidity tests/scripts do not import the file.")
+1 -1
View File
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
class BridgeMonitor:
"""Main bridge monitoring service"""
def __init__(self, config_path: str = None):
def __init__(self, config_path: Optional[str] = None):
"""Initialize bridge monitor with configuration"""
self.config = self._load_config(config_path)
self.chain138_w3 = self._init_web3(self.config['chain138_rpc'])
@@ -191,7 +191,9 @@ class ISO20022Parser:
"""Get text from XML element"""
try:
element = root.find(xpath, self.NAMESPACES)
return element.text if element is not None else default
if element is None:
return default
return element.text if element.text is not None else default
except Exception:
return default
@@ -10,7 +10,7 @@ import logging
import time
from typing import List, Optional
from decimal import Decimal
from dataclasses import dataclass
from dataclasses import dataclass, field
from web3 import Web3
from web3.middleware import geth_poa_middleware
from eth_account import Account
@@ -57,7 +57,7 @@ class OracleConfig:
private_key: str
heartbeat: int = 60 # seconds
deviation_threshold: float = 0.5 # percentage
data_sources: List[DataSource] = None
data_sources: List[DataSource] = field(default_factory=list)
gas_price: Optional[int] = None
gas_limit: int = 100000
max_priority_fee: Optional[int] = None
@@ -11,7 +11,7 @@ import logging
import time
from typing import List, Optional
from decimal import Decimal
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from web3 import Web3
from web3.middleware import geth_poa_middleware
@@ -63,7 +63,7 @@ class OracleConfig:
private_key: str
heartbeat: int = 60 # seconds
deviation_threshold: float = 0.5 # percentage
data_sources: List[DataSource] = None
data_sources: List[DataSource] = field(default_factory=list)
gas_price: Optional[int] = None
gas_limit: int = 100000
max_priority_fee: Optional[int] = None
@@ -81,13 +81,13 @@ class CircuitBreaker:
self.failure_threshold = failure_threshold
self.timeout = timeout
self.failure_count = 0
self.last_failure_time = None
self.last_failure_time: Optional[float] = None
self.state = CircuitBreakerState.CLOSED
def call(self, func, *args, **kwargs):
"""Execute function with circuit breaker protection"""
if self.state == CircuitBreakerState.OPEN:
if time.time() - self.last_failure_time > self.timeout:
if self.last_failure_time is not None and time.time() - self.last_failure_time > self.timeout:
self.state = CircuitBreakerState.HALF_OPEN
logger.info("Circuit breaker: Moving to HALF_OPEN state")
else:
+10
View File
@@ -4,6 +4,16 @@
cd "$(dirname "$0")"
PROJECT_ROOT="$(cd ../.. && pwd)"
PROFILE="${1:-}"
# Production relays run on Proxmox (/opt/smom-dbis-138), not operator workstation checkouts.
if [[ -f "$PROJECT_ROOT/../../scripts/lib/chainlink-production-placement.sh" ]]; then
# shellcheck source=../../scripts/lib/chainlink-production-placement.sh
source "$PROJECT_ROOT/../../scripts/lib/chainlink-production-placement.sh"
require_ccip_relay_on_proxmox || exit 1
elif [[ "$(pwd)" == *"/projects/"*"/services/relay"* ]] && [[ "${CCIP_RELAY_ALLOW_LOCAL:-0}" != "1" ]]; then
echo "ERROR: CCIP relay must run on Proxmox r630-01 (/opt/smom-dbis-138/services/relay). Set CCIP_RELAY_ALLOW_LOCAL=1 for dev." >&2
exit 1
fi
SKIP_ENV_LOCAL="${RELAY_SKIP_ENV_LOCAL:-0}"
declare -A ORIGINAL_ENV_VARS=()
@@ -0,0 +1,18 @@
import type { InboundMessage, InboundFormat } from '../types';
export declare function scanDirectoryInbox(inboxDir: string, sourcePrefix: string, defaultFormat?: InboundFormat): InboundMessage[];
export declare function archiveInboxFile(filePath: string, archiveDir: string): void;
export declare function scanFileInbox(inboxDir: string): InboundMessage[];
export declare function pollP2pRailTransactions(options: {
baseUrl: string;
bearerToken?: string;
apiKey?: string;
path?: string;
sinceId?: string;
}): Promise<InboundMessage[]>;
export declare function pollFinGatewayMessages(options: {
baseUrl: string;
bearerToken?: string;
path?: string;
sinceId?: string;
}): Promise<InboundMessage[]>;
//# sourceMappingURL=inboundAdapters.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"inboundAdapters.d.ts","sourceRoot":"","sources":["../../src/adapters/inboundAdapters.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAK9D,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,aAAa,GAC5B,cAAc,EAAE,CA2BlB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,IAAI,CAIN;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,CAEhE;AAED,wBAAsB,uBAAuB,CAAC,OAAO,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAmC5B;AAED,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAwC5B"}
+127
View File
@@ -0,0 +1,127 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.scanDirectoryInbox = scanDirectoryInbox;
exports.archiveInboxFile = archiveInboxFile;
exports.scanFileInbox = scanFileInbox;
exports.pollP2pRailTransactions = pollP2pRailTransactions;
exports.pollFinGatewayMessages = pollFinGatewayMessages;
const fs_1 = require("fs");
const path_1 = require("path");
const inboundParser_1 = require("../parsers/inboundParser");
const INBOX_EXTENSIONS = /\.(fin|swift|xml|txt|json|as4|mx)$/i;
function scanDirectoryInbox(inboxDir, sourcePrefix, defaultFormat) {
const messages = [];
let entries = [];
try {
entries = (0, fs_1.readdirSync)(inboxDir);
}
catch {
return messages;
}
for (const name of entries.sort()) {
const full = (0, path_1.join)(inboxDir, name);
if (!(0, fs_1.statSync)(full).isFile())
continue;
if (name.startsWith('.'))
continue;
if (!INBOX_EXTENSIONS.test(name))
continue;
const payload = (0, fs_1.readFileSync)(full, 'utf8');
const lower = name.toLowerCase();
let format = defaultFormat ?? (0, inboundParser_1.detectInboundFormat)(payload);
if (lower.endsWith('.xml') || lower.endsWith('.mx') || lower.endsWith('.as4'))
format = 'iso20022';
if (lower.endsWith('.json'))
format = 'json_broadcast';
messages.push({
source: `${sourcePrefix}:${full}`,
format,
payload,
metadata: { fileName: name, filePath: full },
});
}
return messages;
}
function archiveInboxFile(filePath, archiveDir) {
(0, fs_1.mkdirSync)(archiveDir, { recursive: true });
const base = filePath.split('/').pop() ?? 'message';
(0, fs_1.renameSync)(filePath, (0, path_1.join)(archiveDir, `${Date.now()}-${base}`));
}
function scanFileInbox(inboxDir) {
return scanDirectoryInbox(inboxDir, 'file');
}
async function pollP2pRailTransactions(options) {
const url = `${options.baseUrl.replace(/\/$/, '')}${options.path ?? '/api/transactions'}`;
const headers = { Accept: 'application/json' };
if (options.bearerToken)
headers.Authorization = `Bearer ${options.bearerToken}`;
if (options.apiKey)
headers['X-API-Key'] = options.apiKey;
const res = await fetch(url, { headers });
if (!res.ok) {
throw new Error(`P2P rail poll failed: ${res.status} ${res.statusText}`);
}
const body = (await res.json());
const rows = Array.isArray(body) ? body : body?.data ?? [];
const out = [];
for (const row of rows) {
const obj = row;
const id = String(obj.id ?? obj.transactionId ?? obj.reference ?? '');
if (options.sinceId && id && id <= options.sinceId)
continue;
const payload = typeof obj.payload === 'string'
? obj.payload
: typeof obj.swiftMessage === 'string'
? obj.swiftMessage
: typeof obj.message === 'string'
? obj.message
: JSON.stringify(obj);
out.push({
source: `p2p:${id || 'unknown'}`,
format: (0, inboundParser_1.detectInboundFormat)(payload),
payload,
metadata: obj,
});
}
return out;
}
async function pollFinGatewayMessages(options) {
const url = `${options.baseUrl.replace(/\/$/, '')}${options.path ?? '/api/messages'}`;
const headers = { Accept: 'application/json' };
if (options.bearerToken)
headers.Authorization = `Bearer ${options.bearerToken}`;
const res = await fetch(url, { headers });
if (!res.ok) {
throw new Error(`FIN gateway poll failed: ${res.status} ${res.statusText}`);
}
const body = (await res.json());
const rows = Array.isArray(body)
? body
: body?.messages ??
body?.data ??
[];
const out = [];
for (const row of rows) {
const obj = row;
const id = String(obj.id ?? obj.messageId ?? obj.reference ?? '');
if (options.sinceId && id && id <= options.sinceId)
continue;
const payload = typeof obj.finMessage === 'string'
? obj.finMessage
: typeof obj.swiftMessage === 'string'
? obj.swiftMessage
: typeof obj.payload === 'string'
? obj.payload
: typeof obj.body === 'string'
? obj.body
: JSON.stringify(obj);
out.push({
source: `fin:${id || 'unknown'}`,
format: (0, inboundParser_1.detectInboundFormat)(payload),
payload,
metadata: obj,
});
}
return out;
}
//# sourceMappingURL=inboundAdapters.js.map
@@ -0,0 +1 @@
{"version":3,"file":"inboundAdapters.js","sourceRoot":"","sources":["../../src/adapters/inboundAdapters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
import { type Server } from 'net';
import type { InboundMessage } from '../types';
/** Extract FIN messages from a TCP buffer (brace blocks or $...$ framed). */
export declare function extractFinMessages(buffer: string): {
messages: string[];
remainder: string;
};
export type SwiftFinTcpServerOptions = {
host: string;
port: number;
onMessage: (msg: InboundMessage) => void;
};
export declare function startSwiftFinTcpServer(options: SwiftFinTcpServerOptions): Server;
//# sourceMappingURL=swiftFinTcpAdapter.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"swiftFinTcpAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/swiftFinTcpAdapter.ts"],"names":[],"mappings":"AAAA,OAAY,EAAE,KAAK,MAAM,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG/C,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CA4B5F;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;CAC1C,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CA8BhF"}
@@ -0,0 +1,64 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractFinMessages = extractFinMessages;
exports.startSwiftFinTcpServer = startSwiftFinTcpServer;
const net_1 = __importDefault(require("net"));
const inboundParser_1 = require("../parsers/inboundParser");
/** Extract FIN messages from a TCP buffer (brace blocks or $...$ framed). */
function extractFinMessages(buffer) {
const messages = [];
let rest = buffer;
while (rest.length > 0) {
const dollarStart = rest.indexOf('$');
const braceStart = rest.indexOf('{1:');
if (braceStart >= 0 && (dollarStart < 0 || braceStart < dollarStart)) {
const end = rest.indexOf('-}', braceStart);
if (end < 0)
break;
messages.push(rest.slice(braceStart, end + 2));
rest = rest.slice(end + 2);
continue;
}
if (dollarStart >= 0) {
const end = rest.indexOf('$', dollarStart + 1);
if (end < 0)
break;
messages.push(rest.slice(dollarStart + 1, end));
rest = rest.slice(end + 1);
continue;
}
break;
}
return { messages, remainder: rest };
}
function startSwiftFinTcpServer(options) {
const buffers = new Map();
const server = net_1.default.createServer((socket) => {
const key = `${socket.remoteAddress ?? 'unknown'}:${socket.remotePort ?? 0}`;
buffers.set(key, '');
socket.on('data', (chunk) => {
const prev = buffers.get(key) ?? '';
const combined = prev + chunk.toString('utf8');
const { messages, remainder } = extractFinMessages(combined);
buffers.set(key, remainder);
for (const payload of messages) {
if (!payload.trim())
continue;
options.onMessage({
source: `tcp:${key}:${Date.now()}`,
format: (0, inboundParser_1.detectInboundFormat)(payload),
payload,
metadata: { remote: key },
});
}
});
socket.on('close', () => buffers.delete(key));
socket.on('error', () => buffers.delete(key));
});
server.listen(options.port, options.host);
return server;
}
//# sourceMappingURL=swiftFinTcpAdapter.js.map
@@ -0,0 +1 @@
{"version":3,"file":"swiftFinTcpAdapter.js","sourceRoot":"","sources":["../../src/adapters/swiftFinTcpAdapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
import { type Server } from 'http';
import type { InboundMessage } from '../types';
export type WebhookServerOptions = {
port: number;
path: string;
healthPath?: string;
authBearer?: string;
webhookSecret?: string;
webhookSignatureHeader?: string;
onMessage: (msg: InboundMessage) => Promise<void>;
};
export declare function createWebhookServer(options: WebhookServerOptions): Server;
//# sourceMappingURL=webhookServer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"webhookServer.d.ts","sourceRoot":"","sources":["../../src/adapters/webhookServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,MAAM,CAAC;AAU5F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG/C,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC;AAyBF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CA4FzE"}
+114
View File
@@ -0,0 +1,114 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWebhookServer = createWebhookServer;
const http_1 = require("http");
const url_1 = require("url");
const integration_foundation_1 = require("@dbis/integration-foundation");
const inboundParser_1 = require("../parsers/inboundParser");
const idempotencyStore = new integration_foundation_1.IdempotencyStore();
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
req.on('error', reject);
});
}
function unauthorized(res, reason = 'unauthorized') {
res.statusCode = 401;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: reason }));
}
function conflict(res) {
res.statusCode = 409;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'duplicate idempotency key' }));
}
function createWebhookServer(options) {
const healthPath = options.healthPath ?? '/health';
const sigHeader = options.webhookSignatureHeader ?? 'x-hybx-signature';
return (0, http_1.createServer)((req, res) => {
void (async () => {
try {
const url = new url_1.URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
if (req.method === 'GET' && url.pathname === healthPath) {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 'ok', service: 'swift-listener' }));
return;
}
if (url.pathname !== options.path) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'not found' }));
return;
}
if (req.method !== 'POST') {
res.statusCode = 405;
res.end(JSON.stringify({ error: 'method not allowed' }));
return;
}
const raw = await readBody(req);
if (options.webhookSecret) {
const sig = String(req.headers[sigHeader] ?? req.headers['x-webhook-signature'] ?? '');
if (!(0, integration_foundation_1.verifyWebhookSignature)({ secret: options.webhookSecret, payload: raw, signature: sig })) {
unauthorized(res, 'invalid webhook signature');
return;
}
}
else if (options.authBearer) {
const auth = String(req.headers.authorization ?? '');
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
if (token !== options.authBearer) {
unauthorized(res);
return;
}
}
const idempotencyHeader = String(req.headers['x-idempotency-key'] ?? '');
if (idempotencyHeader) {
const key = (0, integration_foundation_1.deriveIdempotencyKey)({
tenantId: 'swift-listener',
entityId: 'webhook',
operation: 'inbound',
resourceId: idempotencyHeader,
});
if (!idempotencyStore.record(key)) {
conflict(res);
return;
}
}
const correlation = (0, integration_foundation_1.createCorrelationContext)({
correlationId: String(req.headers[integration_foundation_1.CORRELATION_ID_HEADER] ?? ''),
requestId: String(req.headers[integration_foundation_1.REQUEST_ID_HEADER] ?? req.headers['x-request-id'] ?? ''),
});
const contentType = String(req.headers['content-type'] ?? '');
let format = (0, inboundParser_1.detectInboundFormat)(raw);
if (contentType.includes('xml'))
format = 'iso20022';
if (contentType.includes('json') && format !== 'swift_fin')
format = 'json_broadcast';
await options.onMessage({
source: `webhook:${correlation.requestId}`,
format,
payload: raw,
metadata: {
contentType,
path: url.pathname,
correlationId: correlation.correlationId,
requestId: correlation.requestId,
},
});
res.statusCode = 202;
res.setHeader('Content-Type', 'application/json');
res.setHeader(integration_foundation_1.CORRELATION_ID_HEADER, correlation.correlationId);
res.setHeader(integration_foundation_1.REQUEST_ID_HEADER, correlation.requestId);
res.end(JSON.stringify({ accepted: true, correlationId: correlation.correlationId }));
}
catch (e) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
}
})();
}).listen(options.port);
}
//# sourceMappingURL=webhookServer.js.map
@@ -0,0 +1 @@
{"version":3,"file":"webhookServer.js","sourceRoot":"","sources":["../../src/adapters/webhookServer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
import { type CanonicalPaymentMessage } from '@dbis/checkpoint-core';
import type { ParsedInbound } from '../types';
export declare function mapToCanonical(parsed: ParsedInbound, dedupeKey: string): CanonicalPaymentMessage | undefined;
//# sourceMappingURL=mapToCanonical.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"mapToCanonical.d.ts","sourceRoot":"","sources":["../../src/canonical/mapToCanonical.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAC9F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AA8B9C,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAoD5G"}
@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mapToCanonical = mapToCanonical;
const ethers_1 = require("ethers");
const checkpoint_core_1 = require("@dbis/checkpoint-core");
function msgTypeCode(parsed) {
if (parsed.format === 'swift_fin') {
if (parsed.messageType === 'MT103')
return checkpoint_core_1.MSG_TYPE.MT103;
return checkpoint_core_1.MSG_TYPE.UNKNOWN;
}
if (parsed.format === 'iso20022') {
if (parsed.messageType === 'pacs.008')
return checkpoint_core_1.MSG_TYPE.PACS008;
if (parsed.messageType === 'pain.001')
return checkpoint_core_1.MSG_TYPE.PAIN001;
}
return checkpoint_core_1.MSG_TYPE.UNKNOWN;
}
function msgTypeLabel(parsed) {
if (parsed.format === 'swift_fin' && parsed.messageType === 'MT103')
return 'MT103';
if (parsed.format === 'iso20022' && parsed.messageType === 'pacs.008')
return 'pacs.008';
if (parsed.format === 'iso20022' && parsed.messageType === 'pain.001')
return 'pain.001';
return 'chain138.synthetic';
}
function pickString(parsed, ...keys) {
const obj = parsed.parsed;
for (const k of keys) {
const v = obj[k];
if (typeof v === 'string' && v.trim())
return v.trim();
}
return '';
}
function mapToCanonical(parsed, dedupeKey) {
const instructionId = pickString(parsed, 'instructionId', 'senderReference', 'transactionReference', 'messageId') ||
`SWIFT-${dedupeKey.slice(0, 16)}`;
const endToEndId = pickString(parsed, 'endToEndId', 'transactionReference', 'senderReference') || instructionId;
const msgId = pickString(parsed, 'messageId', 'senderReference') || instructionId;
const uetr = pickString(parsed, 'uetr') || '';
const debtorId = pickString(parsed, 'orderingCustomer', 'debtor', 'sendingInstitution') || 'UNKNOWN';
const creditorId = pickString(parsed, 'beneficiaryCustomer', 'creditor', 'beneficiaryInstitution') || 'UNKNOWN';
const currencyCode = pickString(parsed, 'currency') || 'USD';
const amountRaw = pickString(parsed, 'amount') || '0';
const purpose = pickString(parsed, 'remittanceInfo', 'remittance', 'purpose', 'senderToReceiverInfo', 'narrative') ||
`${parsed.format}/${parsed.messageType}`;
const instructionIdBytes32 = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(`INSTR:${instructionId}`));
const uetrBytes32 = uetr
? ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(`UETR:${uetr}`))
: ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(`UETR:${dedupeKey}`));
const base = {
msgType: msgTypeLabel(parsed),
msgTypeCode: msgTypeCode(parsed),
instructionId,
instructionIdBytes32,
endToEndId,
endToEndIdHash: (0, checkpoint_core_1.hashShortUtf8)(endToEndId),
msgId,
uetr: uetr || dedupeKey.slice(0, 36),
uetrBytes32,
accountRefId: debtorId,
counterpartyRefId: creditorId,
debtorId,
creditorId,
purpose,
settlementMethod: parsed.format === 'swift_fin' ? 'SWIFT' : 'ISO20022',
categoryPurpose: 'CBFF',
currencyCode,
amountRaw,
amountSmallestUnit: amountRaw.replace(/[,.]/g, '') || '0',
debtorRefHash: (0, checkpoint_core_1.hashShortUtf8)(debtorId),
creditorRefHash: (0, checkpoint_core_1.hashShortUtf8)(creditorId),
purposeHash: (0, checkpoint_core_1.hashShortUtf8)(purpose),
chain138TxHash: pickString(parsed, 'txId', 'chain138TxHash'),
valueDateIso: pickString(parsed, 'valueDate') || undefined,
};
const payloadHash = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(JSON.stringify({ ...base, sourceFormat: parsed.format, messageType: parsed.messageType })));
return { ...base, payloadHash };
}
//# sourceMappingURL=mapToCanonical.js.map
@@ -0,0 +1 @@
{"version":3,"file":"mapToCanonical.js","sourceRoot":"","sources":["../../src/canonical/mapToCanonical.ts"],"names":[],"mappings":"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"}
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=cli.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path");
const fs_1 = require("fs");
const listener_1 = require("./listener");
const resourceRegistry_1 = require("./resourceRegistry");
function parseArgs(argv) {
let once = false;
let projectRoot = process.env.PROXMOX_ROOT ?? process.cwd();
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--once')
once = true;
else if (a === '--project-root')
projectRoot = argv[++i] ?? projectRoot;
else if (a.startsWith('--project-root='))
projectRoot = a.slice('--project-root='.length);
else if (a === '--help' || a === '-h') {
console.log(`Usage: swift-listener [--once] [--project-root PATH]
Inbound adapters (env flags):
SWIFT_LISTENER_FILE_INBOX=1|0 file drop inbox (default on)
SWIFT_LISTENER_WEBHOOK=1 HTTP POST /swift/inbound + GET /health
SWIFT_LISTENER_P2P_RAIL=1 banktransfer Financial Broadcast poll
SWIFT_LISTENER_FIN_GATEWAY=1 FIN / Alliance Access HTTP poll
SWIFT_LISTENER_FIN_TCP=1 SWIFT FIN copy TCP listener
SWIFT_LISTENER_AS4_INBOX=1 AS4 / ISO drop directory poll
SWIFT_LISTENER_MAIL_INBOX=1 mail attachment drop directory poll
Outbound (on match):
SWIFT_LISTENER_OMNL_FORWARD=1 local ISO store + token-aggregation API
SWIFT_LISTENER_INTAKE_GATEWAY=1 ISO20022IntakeGateway (dry-run unless EXECUTE=1)
SWIFT_LISTENER_GATEWAY_EXECUTE=1 broadcast submitInbound tx
Credentials:
P2P_BASE_URL / P2P_BEARER_TOKEN / P2P_API_KEY
FIN_GATEWAY_URL / ALLIANCE_ACCESS_URL / FIN_GATEWAY_TOKEN
SWIFT_FIN_TCP_PORT (default 5001)
SWIFT_LISTENER_WEBHOOK_TOKEN
RPC_URL_138 / PRIVATE_KEY / ISO20022_INTAKE_GATEWAY_MAINNET
TOKEN_AGGREGATION_URL / OMNL_API_KEY
`);
process.exit(0);
}
}
return { once, projectRoot: (0, path_1.resolve)(projectRoot) };
}
async function main() {
const { once, projectRoot } = parseArgs(process.argv);
const config = (0, resourceRegistry_1.loadListenerConfig)(projectRoot);
const dirs = [
config.storage.inboxDir,
config.storage.eventsDir,
config.storage.as4InboxDir,
config.storage.mailInboxDir,
].filter(Boolean);
for (const d of dirs) {
(0, fs_1.mkdirSync)((0, path_1.resolve)(projectRoot, d), { recursive: true });
}
const listener = (0, listener_1.createSwiftListener)(projectRoot);
await listener.run({ once });
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
//# sourceMappingURL=cli.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;AACA,+BAA+B;AAC/B,2BAA+B;AAC/B,yCAAiD;AACjD,yDAAwD;AAExD,SAAS,SAAS,CAAC,IAAc;IAC/B,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAC5D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,CAAC,KAAK,QAAQ;YAAE,IAAI,GAAG,IAAI,CAAC;aAC3B,IAAI,CAAC,KAAK,gBAAgB;YAAE,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,WAAW,CAAC;aACnE,IAAI,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC;YAAE,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;aACrF,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;CAuBjB,CAAC,CAAC;YACG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAA,cAAO,EAAC,WAAW,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,IAAA,qCAAkB,EAAC,WAAW,CAAC,CAAC;IAE/C,MAAM,IAAI,GAAG;QACX,MAAM,CAAC,OAAO,CAAC,QAAQ;QACvB,MAAM,CAAC,OAAO,CAAC,SAAS;QACxB,MAAM,CAAC,OAAO,CAAC,WAAW;QAC1B,MAAM,CAAC,OAAO,CAAC,YAAY;KAC5B,CAAC,MAAM,CAAC,OAAO,CAAa,CAAC;IAE9B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAA,cAAS,EAAC,IAAA,cAAO,EAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,QAAQ,GAAG,IAAA,8BAAmB,EAAC,WAAW,CAAC,CAAC;IAClD,MAAM,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
+12
View File
@@ -0,0 +1,12 @@
export * from './types';
export { loadListenerConfig, buildResourceRegistry, applyEnvOverrides } from './resourceRegistry';
export { parseInbound, parseSwiftFin, parseIso20022 } from './parsers/inboundParser';
export { matchResources } from './matchers/resourceMatcher';
export { mapToCanonical } from './canonical/mapToCanonical';
export { createSwiftListener, SwiftListener } from './listener';
export { extractFinMessages, startSwiftFinTcpServer } from './adapters/swiftFinTcpAdapter';
export { createWebhookServer } from './adapters/webhookServer';
export { forwardToOmnl, OMNL_ISO20022_API_PATH } from './outbound/omnlForwardAdapter';
export { buildGatewayMessage, submitToIntakeGateway } from './outbound/intakeGatewayAdapter';
export { dispatchMatchedOutbounds } from './outbound/dispatchOutcomes';
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACtF,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAC7F,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC"}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.dispatchMatchedOutbounds = exports.submitToIntakeGateway = exports.buildGatewayMessage = exports.OMNL_ISO20022_API_PATH = exports.forwardToOmnl = exports.createWebhookServer = exports.startSwiftFinTcpServer = exports.extractFinMessages = exports.SwiftListener = exports.createSwiftListener = exports.mapToCanonical = exports.matchResources = exports.parseIso20022 = exports.parseSwiftFin = exports.parseInbound = exports.applyEnvOverrides = exports.buildResourceRegistry = exports.loadListenerConfig = void 0;
__exportStar(require("./types"), exports);
var resourceRegistry_1 = require("./resourceRegistry");
Object.defineProperty(exports, "loadListenerConfig", { enumerable: true, get: function () { return resourceRegistry_1.loadListenerConfig; } });
Object.defineProperty(exports, "buildResourceRegistry", { enumerable: true, get: function () { return resourceRegistry_1.buildResourceRegistry; } });
Object.defineProperty(exports, "applyEnvOverrides", { enumerable: true, get: function () { return resourceRegistry_1.applyEnvOverrides; } });
var inboundParser_1 = require("./parsers/inboundParser");
Object.defineProperty(exports, "parseInbound", { enumerable: true, get: function () { return inboundParser_1.parseInbound; } });
Object.defineProperty(exports, "parseSwiftFin", { enumerable: true, get: function () { return inboundParser_1.parseSwiftFin; } });
Object.defineProperty(exports, "parseIso20022", { enumerable: true, get: function () { return inboundParser_1.parseIso20022; } });
var resourceMatcher_1 = require("./matchers/resourceMatcher");
Object.defineProperty(exports, "matchResources", { enumerable: true, get: function () { return resourceMatcher_1.matchResources; } });
var mapToCanonical_1 = require("./canonical/mapToCanonical");
Object.defineProperty(exports, "mapToCanonical", { enumerable: true, get: function () { return mapToCanonical_1.mapToCanonical; } });
var listener_1 = require("./listener");
Object.defineProperty(exports, "createSwiftListener", { enumerable: true, get: function () { return listener_1.createSwiftListener; } });
Object.defineProperty(exports, "SwiftListener", { enumerable: true, get: function () { return listener_1.SwiftListener; } });
var swiftFinTcpAdapter_1 = require("./adapters/swiftFinTcpAdapter");
Object.defineProperty(exports, "extractFinMessages", { enumerable: true, get: function () { return swiftFinTcpAdapter_1.extractFinMessages; } });
Object.defineProperty(exports, "startSwiftFinTcpServer", { enumerable: true, get: function () { return swiftFinTcpAdapter_1.startSwiftFinTcpServer; } });
var webhookServer_1 = require("./adapters/webhookServer");
Object.defineProperty(exports, "createWebhookServer", { enumerable: true, get: function () { return webhookServer_1.createWebhookServer; } });
var omnlForwardAdapter_1 = require("./outbound/omnlForwardAdapter");
Object.defineProperty(exports, "forwardToOmnl", { enumerable: true, get: function () { return omnlForwardAdapter_1.forwardToOmnl; } });
Object.defineProperty(exports, "OMNL_ISO20022_API_PATH", { enumerable: true, get: function () { return omnlForwardAdapter_1.OMNL_ISO20022_API_PATH; } });
var intakeGatewayAdapter_1 = require("./outbound/intakeGatewayAdapter");
Object.defineProperty(exports, "buildGatewayMessage", { enumerable: true, get: function () { return intakeGatewayAdapter_1.buildGatewayMessage; } });
Object.defineProperty(exports, "submitToIntakeGateway", { enumerable: true, get: function () { return intakeGatewayAdapter_1.submitToIntakeGateway; } });
var dispatchOutcomes_1 = require("./outbound/dispatchOutcomes");
Object.defineProperty(exports, "dispatchMatchedOutbounds", { enumerable: true, get: function () { return dispatchOutcomes_1.dispatchMatchedOutbounds; } });
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,0CAAwB;AACxB,uDAAkG;AAAzF,sHAAA,kBAAkB,OAAA;AAAE,yHAAA,qBAAqB,OAAA;AAAE,qHAAA,iBAAiB,OAAA;AACrE,yDAAqF;AAA5E,6GAAA,YAAY,OAAA;AAAE,8GAAA,aAAa,OAAA;AAAE,8GAAA,aAAa,OAAA;AACnD,8DAA4D;AAAnD,iHAAA,cAAc,OAAA;AACvB,6DAA4D;AAAnD,gHAAA,cAAc,OAAA;AACvB,uCAAgE;AAAvD,+GAAA,mBAAmB,OAAA;AAAE,yGAAA,aAAa,OAAA;AAC3C,oEAA2F;AAAlF,wHAAA,kBAAkB,OAAA;AAAE,4HAAA,sBAAsB,OAAA;AACnD,0DAA+D;AAAtD,oHAAA,mBAAmB,OAAA;AAC5B,oEAAsF;AAA7E,mHAAA,aAAa,OAAA;AAAE,4HAAA,sBAAsB,OAAA;AAC9C,wEAA6F;AAApF,2HAAA,mBAAmB,OAAA;AAAE,6HAAA,qBAAqB,OAAA;AACnD,gEAAuE;AAA9D,4HAAA,wBAAwB,OAAA"}
+30
View File
@@ -0,0 +1,30 @@
import type { InboundMessage, SwiftListenerEvent, SwiftListenerOptions } from './types';
export type { SwiftListenerOptions };
export declare class SwiftListener {
private readonly projectRoot;
private readonly config;
private readonly registry;
private readonly store;
private lastP2pId?;
private lastFinId?;
private running;
private webhookServer?;
private tcpServer?;
constructor(projectRoot: string);
processMessage(msg: InboundMessage): Promise<SwiftListenerEvent | null>;
private maybeArchiveSource;
processBatch(sources: InboundMessage[]): Promise<SwiftListenerEvent[]>;
pollFileInbox(): Promise<SwiftListenerEvent[]>;
pollAs4Inbox(): Promise<SwiftListenerEvent[]>;
pollMailInbox(): Promise<SwiftListenerEvent[]>;
pollP2pRail(): Promise<SwiftListenerEvent[]>;
pollFinGateway(): Promise<SwiftListenerEvent[]>;
private trackLastId;
private startLongRunningAdapters;
private pollIntervals;
runPollCycle(): Promise<SwiftListenerEvent[]>;
run(options?: SwiftListenerOptions): Promise<void>;
shutdown(): void;
}
export declare function createSwiftListener(projectRoot: string): SwiftListener;
//# sourceMappingURL=listener.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAExF,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC,qBAAa,aAAa;IAUZ,OAAO,CAAC,QAAQ,CAAC,WAAW;IATxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAC,CAAa;gBAEF,WAAW,EAAE,MAAM;IAM1C,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IA6D7E,OAAO,CAAC,kBAAkB;IA4BpB,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAStE,aAAa,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAI9C,YAAY,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAQ7C,aAAa,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAM9C,WAAW,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAe5C,cAAc,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAmBrD,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,wBAAwB;IA4ChC,OAAO,CAAC,aAAa;IAWf,YAAY,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IA2C7C,GAAG,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB5D,QAAQ,IAAI,IAAI;CAKjB;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAEtE"}

Some files were not shown because too many files have changed in this diff Show More