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
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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 []
|
||||
|
||||
@@ -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 ?? {}),
|
||||
};
|
||||
}
|
||||
+33
@@ -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;
|
||||
@@ -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[];
|
||||
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+49
@@ -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*\\{`));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
/** Placeholder HYBX API types — require official HYBX documentation for production. */
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+14
@@ -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
@@ -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;
|
||||
}
|
||||
+39
@@ -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/**/*"]
|
||||
}
|
||||
@@ -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"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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.")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=cli.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
||||
Vendored
+68
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user