fix(omnl): production blockers for M0-M4 settlement stack
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m13s
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Deploy ChainID 138 / Deploy ChainID 138 (push) Has been cancelled
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Verify Deployment / Verify Deployment (push) Has been cancelled

Exempt dashboard read APIs from anonymous rate limits, preserve nginx-injected auth, wire chain-mint and DB env through deploy/LXC scripts, resolve M3 token addresses from registry, and harden super-admin seeding.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 06:47:20 -07:00
parent 5007e74532
commit 5229c2d888
25 changed files with 424 additions and 137 deletions

View File

@@ -24,7 +24,7 @@
"rails": {
"swift": { "enabled": true, "listenerUrl": "http://192.168.11.114:8788" },
"hybx": { "enabled": true, "sidecarUrl": "http://192.168.11.89:8080" },
"chain138": { "enabled": true, "rpcEnv": "RPC_URL_138", "defaultLineId": "USD-M1" }
"chain138": { "enabled": true, "rpcEnv": "RPC_URL_138", "defaultLineId": "USD-M2" }
},
"omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.",
"omnlLei": "98450070C57395F6B906"

View File

@@ -9,7 +9,7 @@
"rateLimits": {
"customerPerMinute": 30,
"superAdminPerMinute": 120,
"anonymousPerMinute": 0
"anonymousPerMinute": 30
},
"corsOrigins": [
"https://secure.d-bis.org",
@@ -24,7 +24,11 @@
"/health",
"/health/ledger",
"/money-supply/public",
"/public/money-supply"
"/public/money-supply",
"/office",
"/tokens/m2",
"/chains",
"/chains/active"
],
"notes": "Customer keys: OMNL_CUSTOMER_API_KEYS=key:read|trade or OMNL_CUSTOMER_API_KEY_1..3. Super-admin keys: OMNL_SUPER_ADMIN_*_KEY in config/omnl-super-admins.v1.json. Portal browsers use nginx-injected auth — never VITE_OMNL_API_KEY."
}

View File

@@ -19,7 +19,6 @@ location /settlement/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_read_timeout 120s;
}

View File

@@ -8,6 +8,7 @@ export type OmnlPrincipal = {
export type SuperAdminEntry = {
id: string;
username: string;
email?: string;
displayName: string;
portalCtid: number;
portalName: string;
@@ -28,6 +29,7 @@ export type CustomerSecurityPolicy = {
superAdminPerMinute: number;
anonymousPerMinute: number;
};
publicEndpoints?: string[];
corsOrigins: string[];
};
export declare function loadSuperAdminRegistry(): SuperAdminEntry[];
@@ -42,3 +44,5 @@ export declare function resolveOmnlPrincipal(req: {
}, key?: string): OmnlPrincipal | null;
export declare function principalHasScope(principal: OmnlPrincipal | null, scope: string): boolean;
export declare function isProductionSecurityMode(): boolean;
/** Paths on settlement/exchange routers that skip anonymous rate limits (dashboard read APIs). */
export declare function isOmnlPublicEndpoint(pathname: string): boolean;

View File

@@ -9,6 +9,7 @@ exports.loadCustomerSecurityPolicy = loadCustomerSecurityPolicy;
exports.resolveOmnlPrincipal = resolveOmnlPrincipal;
exports.principalHasScope = principalHasScope;
exports.isProductionSecurityMode = isProductionSecurityMode;
exports.isOmnlPublicEndpoint = isOmnlPublicEndpoint;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
let superAdminCache = null;
@@ -71,7 +72,8 @@ function loadCustomerSecurityPolicy() {
requireApiKeyAllExchangeRoutes: true,
customerAllowedScopes: ['read', 'trade'],
superAdminOnlyScopes: ['settle', 'token_load', 'transfer', 'admin'],
rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 0 },
rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 30 },
publicEndpoints: ['/health', '/health/ledger', '/money-supply/public', '/public/money-supply'],
corsOrigins: [],
};
}
@@ -126,3 +128,20 @@ function isProductionSecurityMode() {
process.env.OMNL_CUSTOMER_SECURITY_PRODUCTION === '1' ||
(process.env.NODE_ENV || '').toLowerCase() === 'production');
}
const DEFAULT_PUBLIC_ENDPOINTS = [
'/health',
'/health/ledger',
'/money-supply/public',
'/public/money-supply',
'/office',
'/tokens/m2',
'/chains',
'/chains/active',
];
/** Paths on settlement/exchange routers that skip anonymous rate limits (dashboard read APIs). */
function isOmnlPublicEndpoint(pathname) {
const policy = loadCustomerSecurityPolicy();
const allowed = policy.publicEndpoints?.length ? policy.publicEndpoints : DEFAULT_PUBLIC_ENDPOINTS;
const path = pathname.split('?')[0] || '/';
return allowed.some((ep) => path === ep || path.startsWith(`${ep}/`));
}

View File

@@ -35,6 +35,7 @@ export type CustomerSecurityPolicy = {
superAdminPerMinute: number;
anonymousPerMinute: number;
};
publicEndpoints?: string[];
corsOrigins: string[];
};
@@ -106,7 +107,8 @@ export function loadCustomerSecurityPolicy(): CustomerSecurityPolicy {
requireApiKeyAllExchangeRoutes: true,
customerAllowedScopes: ['read', 'trade'],
superAdminOnlyScopes: ['settle', 'token_load', 'transfer', 'admin'],
rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 0 },
rateLimits: { customerPerMinute: 30, superAdminPerMinute: 120, anonymousPerMinute: 30 },
publicEndpoints: ['/health', '/health/ledger', '/money-supply/public', '/public/money-supply'],
corsOrigins: [],
};
}
@@ -172,3 +174,22 @@ export function isProductionSecurityMode(): boolean {
(process.env.NODE_ENV || '').toLowerCase() === 'production'
);
}
const DEFAULT_PUBLIC_ENDPOINTS = [
'/health',
'/health/ledger',
'/money-supply/public',
'/public/money-supply',
'/office',
'/tokens/m2',
'/chains',
'/chains/active',
];
/** Paths on settlement/exchange routers that skip anonymous rate limits (dashboard read APIs). */
export function isOmnlPublicEndpoint(pathname: string): boolean {
const policy = loadCustomerSecurityPolicy();
const allowed = policy.publicEndpoints?.length ? policy.publicEndpoints : DEFAULT_PUBLIC_ENDPOINTS;
const path = pathname.split('?')[0] || '/';
return allowed.some((ep) => path === ep || path.startsWith(`${ep}/`));
}

View File

@@ -42,3 +42,4 @@ export declare function mergeExchangeTokensWithM2Registry(baseTokens: {
}[], registry: M2TokenRegistryFile): M2TokenCapabilities[];
export declare function findM2TokenByAddress(registry: M2TokenRegistryFile, address: string): M2TokenCapabilities | undefined;
export declare function findM2TokenBySymbol(registry: M2TokenRegistryFile, symbol: string): M2TokenCapabilities | undefined;
export declare function findM2TokenByLineId(registry: M2TokenRegistryFile, lineId: string): M2TokenCapabilities | undefined;

View File

@@ -6,6 +6,7 @@ exports.withM2Capabilities = withM2Capabilities;
exports.mergeExchangeTokensWithM2Registry = mergeExchangeTokensWithM2Registry;
exports.findM2TokenByAddress = findM2TokenByAddress;
exports.findM2TokenBySymbol = findM2TokenBySymbol;
exports.findM2TokenByLineId = findM2TokenByLineId;
const money_supply_1 = require("./money-supply");
const CURRENCY_FROM_SYMBOL = {
USD: 'USD',
@@ -84,3 +85,7 @@ function findM2TokenBySymbol(registry, symbol) {
const s = symbol.trim().toLowerCase();
return registry.tokens.find((t) => t.symbol.toLowerCase() === s);
}
function findM2TokenByLineId(registry, lineId) {
const id = lineId.trim().toUpperCase();
return registry.tokens.find((t) => t.omnlLine.toUpperCase() === id);
}

View File

@@ -133,3 +133,11 @@ export function findM2TokenBySymbol(
const s = symbol.trim().toLowerCase();
return registry.tokens.find((t) => t.symbol.toLowerCase() === s);
}
export function findM2TokenByLineId(
registry: M2TokenRegistryFile,
lineId: string,
): M2TokenCapabilities | undefined {
const id = lineId.trim().toUpperCase();
return registry.tokens.find((t) => t.omnlLine.toUpperCase() === id);
}

View File

@@ -26,20 +26,37 @@ bash scripts/deployment/sync-ali-omnl-banking-portal-lxcs.sh --apply
log "=== 4/5 Push Fineract/API env to each LXC ==="
if [[ -f "$ENV_FILE" ]]; then
for CTID in 5825 5826 5827 5828; do
bash scripts/deployment/push-portal-env-to-lxc.sh "$CTID" || log "WARN: env push failed for CT $CTID"
omnl_pve_ssh "pct exec ${CTID} -- bash ${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}/scripts/deployment/start-omnl-banking-portal.sh" 2>/dev/null || true
bash scripts/deployment/push-portal-env-to-lxc.sh "$CTID"
omnl_pve_ssh "pct exec ${CTID} -- bash ${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}/scripts/deployment/start-omnl-banking-portal.sh" 2>/dev/null || log "WARN: portal restart failed for CT $CTID"
done
else
log "WARN: no $ENV_FILE — skip LXC Fineract env"
fi
log "=== 4b Super-admin DB seed (control panel) ==="
if [[ -f "$ENV_FILE" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]]; then
set -a
set +u
# shellcheck disable=SC1090
source "$ENV_FILE"
set -u
set +a
if [[ -n "${DATABASE_URL:-}" ]]; then
node scripts/deployment/seed-omnl-super-admin-users.mjs --stop-services | tee "$HOME/omnl-super-admin-credentials.txt" || log "WARN: super-admin user seed failed"
else
log "WARN: DATABASE_URL unset — skip super-admin user seed"
fi
fi
log "=== 5/5 Office 24 opening journal (activate live ledgers) ==="
MATERIAL_OPENING_USD="${OMNL_MATERIAL_OPENING_THRESHOLD:-10000000}"
STATUS_FILE="$REPO_DIR/config/omnl-hospitallers-office24-status.v1.json"
if [[ "$SEED_JOURNAL" == "1" ]] && [[ -f "$ENV_FILE" ]]; then
set -a
set +u
# shellcheck disable=SC1090
source "$ENV_FILE"
set -u
set +a
if [[ -f "$STATUS_FILE" ]] && grep -q '"fundingPosted"[[:space:]]*:[[:space:]]*true' "$STATUS_FILE" 2>/dev/null; then
log "Office 24 fundingPosted=true in status SSOT — skip single-leg seed (journal already live)"

View File

@@ -56,6 +56,9 @@ if [[ -f "$ENV_FILE" ]] && ! grep -q '^OMNL_SUPER_ADMIN_OFFICE24_KEY=' "$ENV_FIL
fi
if [[ -f "$ENV_FILE" ]]; then
for CTID in 5825 5826 5827 5828; do
bash "$REPO_DIR/scripts/deployment/generate-portal-nginx-auth-snippet.sh" "$CTID" "$REPO_DIR/deploy/nginx/omnl-portal-auth-${CTID}.conf.inc" || true
done
bash "$REPO_DIR/scripts/deployment/generate-portal-nginx-auth-snippet.sh" 5827 "$REPO_DIR/deploy/nginx/omnl-portal-auth-current.conf.inc" || true
if [[ -f "$REPO_DIR/deploy/nginx/omnl-portal-auth-current.conf.inc" ]]; then
sudo cp "$REPO_DIR/deploy/nginx/omnl-portal-auth-current.conf.inc" /etc/nginx/omnl-portal-auth-current.conf.inc 2>/dev/null || \
@@ -122,9 +125,10 @@ start_svc() {
OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \
OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \
OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}" \
SETTLEMENT_ALLOW_HYBX_PRODUCTION="${SETTLEMENT_ALLOW_HYBX_PRODUCTION:-}" \
SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE="${SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE:-}" \
OMNL_ALLOW_CHAIN_MINT_EXECUTE="${OMNL_ALLOW_CHAIN_MINT_EXECUTE:-}" \
DATABASE_URL="${DATABASE_URL:-}" \
SETTLEMENT_ALLOW_HYBX_PRODUCTION="${SETTLEMENT_ALLOW_HYBX_PRODUCTION:-1}" \
SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE="${SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE:-1}" \
OMNL_ALLOW_CHAIN_MINT_EXECUTE="${OMNL_ALLOW_CHAIN_MINT_EXECUTE:-1}" \
TOKEN_AGGREGATION_URL="${TOKEN_AGGREGATION_URL:-http://127.0.0.1:3000}" \
HYBX_MIFOS_FINERACT_SIDECAR_URL="${HYBX_MIFOS_FINERACT_SIDECAR_URL:-}" \
SWIFT_LISTENER_URL="${SWIFT_LISTENER_URL:-}" \
@@ -152,12 +156,23 @@ sleep 5
log "Health checks..."
curl -sf "http://127.0.0.1:3011/api/v1/settlement/health" | head -c 200 || true
echo
curl -sf "http://127.0.0.1:3011/api/v1/settlement/money-supply/public" | head -c 300 || true
curl -sf "http://127.0.0.1:3011/api/v1/settlement/money-supply/public" | head -c 400 || true
echo
curl -sf "http://127.0.0.1:3012/api/v1/exchange/health" | head -c 200 || true
echo
curl -sf -o /dev/null -w "frontend=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/central-bank"
if [[ -n "${DATABASE_URL:-}" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]]; then
log "Seeding super-admin control-panel users..."
node "$REPO_DIR/scripts/deployment/seed-omnl-super-admin-users.mjs" --stop-services >"$LOG_DIR/super-admin-seed.log" 2>&1 || log "WARN: super-admin seed failed — see $LOG_DIR/super-admin-seed.log"
start_svc token-aggregation services/token-aggregation 3000 "npm run start"
sleep 2
start_svc settlement-middleware services/settlement-middleware 3011 "npm run start"
sleep 2
start_svc dbis-exchange services/dbis-exchange 3012 "npm run start"
sleep 2
fi
log "OMNL Bank deploy complete"
log " Central Bank: http://127.0.0.1:$FRONTEND_PORT/central-bank"
log " Office 24: http://127.0.0.1:$FRONTEND_PORT/office-24"

View File

@@ -19,12 +19,14 @@ if [[ ! -f "$ENV_FILE" ]]; then
fi
set -a
set +u
# shellcheck disable=SC1090
source "$ENV_FILE"
set -u
set +a
FRAG="$(mktemp)"
grep -E '^(OMNL_FINERACT_|OMNL_PUBLIC_MONEY_SUPPLY=|OMNL_REQUIRE_API_KEY=|OFFICE24_OPENING_M1_USD=|OMNL_PORTAL_INTERNAL_SECRET=|OMNL_CUSTOMER_SECURITY_PRODUCTION=|JWT_SECRET=)' "$ENV_FILE" >"$FRAG" || true
grep -E '^(OMNL_FINERACT_|OMNL_PUBLIC_MONEY_SUPPLY=|OMNL_REQUIRE_API_KEY=|OMNL_CUSTOMER_SECURITY_PRODUCTION=|OMNL_CUSTOMER_API_KEYS=|OFFICE24_OPENING_M1_USD=|OMNL_PORTAL_INTERNAL_SECRET=|JWT_SECRET=|DATABASE_URL=|RPC_URL_138=|CHAIN_138_RPC_URL=|SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=|SETTLEMENT_ALLOW_HYBX_PRODUCTION=|OMNL_ALLOW_CHAIN_MINT_EXECUTE=|OMNL_MINT_OPERATOR_PRIVATE_KEY=|TOKEN_AGGREGATION_URL=|SWIFT_LISTENER_URL=|HYBX_MIFOS_FINERACT_SIDECAR_URL=|INFURA_PROJECT_ID=|INFURA_PROJECT_SECRET=)' "$ENV_FILE" >"$FRAG" || true
ADMIN_ENV_KEY="$(node -e "
const fs = require('fs');
@@ -46,9 +48,9 @@ elif [[ -n "${OMNL_API_KEY:-}" ]]; then
fi
if [[ ! -s "$FRAG" ]]; then
log "No Fineract/API keys found in $ENV_FILE — skip CT $CTID"
log "ERROR: No portal env keys matched in $ENV_FILE for CT $CTID" >&2
rm -f "$FRAG"
exit 0
exit 1
fi
log "Pushing portal env keys to CT $CTID..."

View File

@@ -6,9 +6,9 @@
* Usage:
* node scripts/deployment/seed-omnl-super-admin-users.mjs
* node scripts/deployment/seed-omnl-super-admin-users.mjs --dry-run
* node scripts/deployment/seed-omnl-super-admin-users.mjs --credentials-only
*
* Passwords: generated per user unless OMNL_SUPER_ADMIN_<ID>_PASSWORD env is set.
* Output: one-time credentials printed to stdout (never logged to git).
*/
import fs from 'fs';
import path from 'path';
@@ -25,6 +25,24 @@ const pg = require('pg');
const configPath = path.join(repoRoot, 'config/omnl-super-admins.v1.json');
const dryRun = process.argv.includes('--dry-run');
const credentialsOnly = process.argv.includes('--credentials-only');
const stopServices = process.argv.includes('--stop-services');
const envFile = process.env.OMNL_BANK_ENV || path.join(repoRoot, '.env');
if (fs.existsSync(envFile)) {
for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
if (process.env[key] !== undefined) continue;
let val = trimmed.slice(eq + 1).trim();
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
process.env[key] = val;
}
}
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const databaseUrl = process.env.DATABASE_URL;
@@ -80,45 +98,59 @@ if (!databaseUrl) {
process.exit(1);
}
if (stopServices) {
const { execSync } = await import('child_process');
for (const pattern of ['token-aggregation', 'settlement-middleware', 'dbis-exchange']) {
try {
execSync(`pkill -f "smom-dbis-138/services/${pattern}" || true`, { stdio: 'ignore' });
} catch {
/* ignore */
}
}
await new Promise((r) => setTimeout(r, 2000));
}
const client = new pg.Client({ connectionString: databaseUrl });
await client.connect();
try {
await client.connect();
await client.query(`
CREATE TABLE IF NOT EXISTS admin_users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255),
password_hash TEXT NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'admin',
is_active BOOLEAN DEFAULT true,
last_login TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
`);
await client.query(`
CREATE TABLE IF NOT EXISTS admin_users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255),
password_hash TEXT NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'admin',
is_active BOOLEAN DEFAULT true,
last_login TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
`);
for (const { admin, password, hash } of rows) {
await client.query(
`INSERT INTO admin_users (username, email, password_hash, role, is_active)
VALUES ($1, $2, $3, 'super_admin', true)
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash,
role = 'super_admin',
is_active = true,
updated_at = NOW()`,
[admin.username, admin.email, hash],
);
}
await client.end();
console.log('=== OMNL super_admin accounts (one-time — distribute securely) ===');
for (const { admin, password } of rows) {
console.log(`${admin.displayName}`);
console.log(` Portal CT ${admin.portalCtid} · ${admin.products.join(', ')}`);
console.log(` Username: ${admin.username}`);
console.log(` Email: ${admin.email}`);
console.log(` Password: ${password}`);
console.log('');
for (const { admin, password, hash } of rows) {
await client.query(
`INSERT INTO admin_users (username, email, password_hash, role, is_active)
VALUES ($1, $2, $3, 'super_admin', true)
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash,
role = 'super_admin',
is_active = true,
updated_at = NOW()`,
[admin.username, admin.email, hash],
);
}
console.log('=== OMNL super_admin accounts (one-time — distribute securely) ===');
for (const { admin, password } of rows) {
console.log(`${admin.displayName}`);
console.log(` Portal CT ${admin.portalCtid} · ${admin.products.join(', ')}`);
console.log(` Username: ${admin.username}`);
console.log(` Email: ${admin.email}`);
console.log(` Password: ${password}`);
console.log('');
}
} finally {
await client.end().catch(() => {});
}

View File

@@ -1,61 +1,92 @@
#!/usr/bin/env bash
# Start OMNL services from /srv/zardasht-portal inside PCT
set -euo pipefail
ROOT="${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}"
LOG_DIR="${OMNL_BANK_LOG_DIR:-/var/log/omnl-bank}"
NGINX_TEMPLATE="${ROOT}/deploy/nginx/omnl-lxc-portal.conf"
mkdir -p "$LOG_DIR"
export SETTLEMENT_MIDDLEWARE_CONFIG="${ROOT}/config/settlement-middleware.production.v1.json"
export DBIS_EXCHANGE_CONFIG="${ROOT}/config/dbis-exchange.production.v1.json"
export OMNL_SUPPORTED_CHAINS_CONFIG="${ROOT}/config/omnl-supported-chains.v1.json"
export OMNL_M2_TOKEN_REGISTRY="${ROOT}/config/omnl-m2-token-registry.v1.json"
start() {
local name="$1" dir="$2" cmd="$3"
cd "${ROOT}/${dir}"
pkill -f "${ROOT}/${dir}" 2>/dev/null || true
nohup env \
SETTLEMENT_MIDDLEWARE_CONFIG="$SETTLEMENT_MIDDLEWARE_CONFIG" \
DBIS_EXCHANGE_CONFIG="$DBIS_EXCHANGE_CONFIG" \
OMNL_SUPPORTED_CHAINS_CONFIG="$OMNL_SUPPORTED_CHAINS_CONFIG" \
OMNL_M2_TOKEN_REGISTRY="$OMNL_M2_TOKEN_REGISTRY" \
OMNL_API_KEY="${OMNL_API_KEY:-}" \
OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-}" \
OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-omnl}" \
OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \
OMNL_FINERACT_USER="${OMNL_FINERACT_USER:-}" \
OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \
OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}" \
bash -c "$cmd" >"${LOG_DIR}/${name}.log" 2>&1 &
echo $! >"${LOG_DIR}/${name}.pid"
}
start token-aggregation services/token-aggregation "npm run start"
sleep 2
start settlement-middleware services/settlement-middleware "npm run start"
sleep 2
start dbis-exchange services/dbis-exchange "npm run start"
sleep 2
FRONTEND_PORT="${OMNL_BANK_FRONTEND_PORT:-3002}"
pkill -f "serve -s.*${FRONTEND_PORT}" 2>/dev/null || true
if [ -f "${LOG_DIR}/nginx.pid" ]; then
nginx -s quit -g "pid ${LOG_DIR}/nginx.pid;" 2>/dev/null || true
fi
fuser -k "${FRONTEND_PORT}/tcp" 2>/dev/null || true
sleep 1
if command -v nginx >/dev/null 2>&1 && [ -f "$NGINX_TEMPLATE" ]; then
sed "s|/srv/zardasht-portal|${ROOT}|g; s|/srv/ali-portal|${ROOT}|g" "$NGINX_TEMPLATE" > "${LOG_DIR}/nginx.conf"
nginx -t -c "${LOG_DIR}/nginx.conf"
nohup nginx -c "${LOG_DIR}/nginx.conf" >"${LOG_DIR}/frontend.log" 2>&1 &
echo $! >"${LOG_DIR}/frontend.pid"
echo "OMNL banking portal started under ${ROOT} (nginx :${FRONTEND_PORT})"
else
cd "${ROOT}/frontend-dapp"
nohup npx --yes serve -s dist -l "$FRONTEND_PORT" >"${LOG_DIR}/frontend.log" 2>&1 &
echo $! >"${LOG_DIR}/frontend.pid"
echo "OMNL banking portal started under ${ROOT} (serve :${FRONTEND_PORT})"
fi
#!/usr/bin/env bash
# Start OMNL services from /srv/zardasht-portal inside PCT
set -euo pipefail
ROOT="${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}"
LOG_DIR="${OMNL_BANK_LOG_DIR:-/var/log/omnl-bank}"
NGINX_TEMPLATE="${ROOT}/deploy/nginx/omnl-lxc-portal.conf"
ENV_FILE="${OMNL_BANK_ENV:-${ROOT}/.env}"
mkdir -p "$LOG_DIR"
if [[ -f "$ENV_FILE" ]]; then
set -a
set +u
# shellcheck disable=SC1090
source "$ENV_FILE"
set -u
set +a
fi
export SETTLEMENT_MIDDLEWARE_CONFIG="${ROOT}/config/settlement-middleware.production.v1.json"
export DBIS_EXCHANGE_CONFIG="${ROOT}/config/dbis-exchange.production.v1.json"
export OMNL_SUPPORTED_CHAINS_CONFIG="${ROOT}/config/omnl-supported-chains.v1.json"
export OMNL_M2_TOKEN_REGISTRY="${ROOT}/config/omnl-m2-token-registry.v1.json"
export OMNL_BANK_ROOT="$ROOT"
start() {
local name="$1" dir="$2" cmd="$3"
cd "${ROOT}/${dir}"
pkill -f "${ROOT}/${dir}" 2>/dev/null || true
nohup env \
SETTLEMENT_MIDDLEWARE_CONFIG="$SETTLEMENT_MIDDLEWARE_CONFIG" \
DBIS_EXCHANGE_CONFIG="$DBIS_EXCHANGE_CONFIG" \
OMNL_SUPPORTED_CHAINS_CONFIG="$OMNL_SUPPORTED_CHAINS_CONFIG" \
OMNL_M2_TOKEN_REGISTRY="$OMNL_M2_TOKEN_REGISTRY" \
OMNL_BANK_ROOT="$ROOT" \
OMNL_API_KEY="${OMNL_API_KEY:-}" \
OMNL_SUPER_ADMIN_OFFICE24_KEY="${OMNL_SUPER_ADMIN_OFFICE24_KEY:-}" \
OMNL_SUPER_ADMIN_EXCHANGE_KEY="${OMNL_SUPER_ADMIN_EXCHANGE_KEY:-}" \
OMNL_SUPER_ADMIN_SECURE_KEY="${OMNL_SUPER_ADMIN_SECURE_KEY:-}" \
OMNL_SUPER_ADMIN_FOREX_KEY="${OMNL_SUPER_ADMIN_FOREX_KEY:-}" \
OMNL_PORTAL_INTERNAL_SECRET="${OMNL_PORTAL_INTERNAL_SECRET:-}" \
OMNL_CUSTOMER_API_KEYS="${OMNL_CUSTOMER_API_KEYS:-}" \
OMNL_CUSTOMER_SECURITY_PRODUCTION="${OMNL_CUSTOMER_SECURITY_PRODUCTION:-1}" \
OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-}" \
OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-omnl}" \
OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \

View File

@@ -11,6 +11,7 @@ function asHandler(h) {
}
exports.exchangeRateLimiter = asHandler((0, express_rate_limit_1.default)({
windowMs: 60_000,
skip: (req) => (0, integration_foundation_1.isOmnlPublicEndpoint)(req.path),
max: (req) => {
if (!(0, integration_foundation_1.isProductionSecurityMode)())
return 200;

View File

@@ -1,6 +1,11 @@
import rateLimit from 'express-rate-limit';
import type { RequestHandler } from 'express';
import { isProductionSecurityMode, loadCustomerSecurityPolicy, resolveOmnlPrincipal } from '@dbis/integration-foundation';
import {
isOmnlPublicEndpoint,
isProductionSecurityMode,
loadCustomerSecurityPolicy,
resolveOmnlPrincipal,
} from '@dbis/integration-foundation';
function asHandler(h: ReturnType<typeof rateLimit>): RequestHandler {
return h as unknown as RequestHandler;
@@ -9,6 +14,7 @@ function asHandler(h: ReturnType<typeof rateLimit>): RequestHandler {
export const exchangeRateLimiter = asHandler(
rateLimit({
windowMs: 60_000,
skip: (req) => isOmnlPublicEndpoint(req.path),
max: (req) => {
if (!isProductionSecurityMode()) return 200;
const policy = loadCustomerSecurityPolicy();

View File

@@ -8,6 +8,7 @@ exports.postJournalEntry = postJournalEntry;
exports.resolveGlAccountId = resolveGlAccountId;
exports.fetchGlBalances = fetchGlBalances;
const axios_1 = __importDefault(require("axios"));
const settlement_store_1 = require("../store/settlement-store");
const GL_HINTS = ['1000', '1050', '1410', '2000', '2100', '2200', '2300'];
function fineractBase() {
return (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, '');
@@ -173,28 +174,38 @@ async function fetchGlBalances(officeId) {
const base = fineractBase();
if (!base || !process.env.OMNL_FINERACT_PASSWORD)
return {};
let out = {};
try {
const { data } = await axios_1.default.get(`${base}/runreports/General Ledger Report`, {
headers: authHeaders(),
params: { R_officeId: officeId, genericResultSet: false },
timeout: 60000,
});
const out = {};
const rows = Array.isArray(data) ? data : data?.data ?? [];
for (const row of rows) {
if (row.glCode)
out[row.glCode] = String(row.balance ?? 0);
}
if (Object.keys(out).length > 0)
return out;
if (Object.keys(out).length === 0) {
out = await fetchGlBalancesFromJournals(officeId);
}
}
catch {
/* report unavailable on this Fineract — fall through */
}
try {
return await fetchGlBalancesFromJournals(officeId);
}
catch {
return {};
try {
out = await fetchGlBalancesFromJournals(officeId);
}
catch {
out = {};
}
}
out.inFlightSwift = sumInFlightSwift().toFixed(2);
return out;
}
function sumInFlightSwift() {
const inFlightPhases = new Set(['HYBX_RAIL_DISPATCHED', 'ISO20022_ARCHIVED', 'CHAIN_MINT_REQUESTED']);
return settlement_store_1.settlementStore
.list(500)
.filter((r) => inFlightPhases.has(r.phase) &&
(r.request.rail === 'SWIFT' || r.request.rail === 'RTGS' || r.request.rail === 'SEPA'))
.reduce((sum, r) => sum + (parseFloat(r.request.amount ?? '0') || 0), 0);
}

View File

@@ -11,6 +11,7 @@ function asHandler(h) {
}
exports.settlementRateLimiter = asHandler((0, express_rate_limit_1.default)({
windowMs: 60_000,
skip: (req) => (0, integration_foundation_1.isOmnlPublicEndpoint)(req.path),
max: (req) => {
if (!(0, integration_foundation_1.isProductionSecurityMode)())
return 200;

View File

@@ -120,6 +120,8 @@ async function processExternalTransfer(input) {
const m3Check = (0, settlement_core_1.m3TokenLoadAmount)(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
if (!fullyBacked || !m3Check.fullyBacked) {
record.errors.push(`M2/M3 backing insufficient for token load (${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`);
advance('FAILED');
return record;
}
const mint = await (0, omnl_1.requestTokenMint)({
lineId: req.convertToCrypto.tokenLineId,

View File

@@ -1,12 +1,38 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.processTokenLoad = processTokenLoad;
const uuid_1 = require("uuid");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const settlement_core_1 = require("@dbis/settlement-core");
const config_1 = require("../config");
const fineract_1 = require("../adapters/fineract");
const omnl_1 = require("../adapters/omnl");
const settlement_store_1 = require("../store/settlement-store");
function loadTokenRegistry() {
const registryPath = process.env.OMNL_M2_TOKEN_REGISTRY ||
path_1.default.resolve(__dirname, '../../../config/omnl-m2-token-registry.v1.json');
return JSON.parse(fs_1.default.readFileSync(registryPath, 'utf8'));
}
function resolveTokenAddress(req) {
if (req.tokenAddress?.startsWith('0x'))
return req.tokenAddress;
const registry = loadTokenRegistry();
const byLine = (0, settlement_core_1.findM2TokenByLineId)(registry, req.lineId);
if (byLine?.address.startsWith('0x') && byLine.address !== '0x0000000000000000000000000000000000000000') {
return byLine.address;
}
if (req.symbol) {
const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === req.symbol.toLowerCase());
if (bySym?.address.startsWith('0x') && bySym.address !== '0x0000000000000000000000000000000000000000') {
return bySym.address;
}
}
return undefined;
}
async function processTokenLoad(input) {
const cfg = (0, config_1.loadConfig)();
const profile = (0, config_1.loadOfficeProfile)();
@@ -93,7 +119,7 @@ async function processTokenLoad(input) {
settlementRef: record.settlementId,
dryRun: !cfg.production.allowChainMintExecute,
symbol: req.symbol,
tokenAddress: req.tokenAddress,
tokenAddress: resolveTokenAddress(req),
});
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
advance('SETTLED');

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import { settlementStore } from '../store/settlement-store';
const GL_HINTS = ['1000', '1050', '1410', '2000', '2100', '2200', '2300'];
@@ -179,24 +180,39 @@ export async function resolveGlAccountId(glCode: string): Promise<number> {
export async function fetchGlBalances(officeId: number): Promise<Record<string, string>> {
const base = fineractBase();
if (!base || !process.env.OMNL_FINERACT_PASSWORD) return {};
let out: Record<string, string> = {};
try {
const { data } = await axios.get(`${base}/runreports/General Ledger Report`, {
headers: authHeaders(),
params: { R_officeId: officeId, genericResultSet: false },
timeout: 60000,
});
const out: Record<string, string> = {};
const rows = Array.isArray(data) ? data : (data as { data?: unknown[] })?.data ?? [];
for (const row of rows as { glCode?: string; balance?: number }[]) {
if (row.glCode) out[row.glCode] = String(row.balance ?? 0);
}
if (Object.keys(out).length > 0) return out;
if (Object.keys(out).length === 0) {
out = await fetchGlBalancesFromJournals(officeId);
}
} catch {
/* report unavailable on this Fineract — fall through */
}
try {
return await fetchGlBalancesFromJournals(officeId);
} catch {
return {};
try {
out = await fetchGlBalancesFromJournals(officeId);
} catch {
out = {};
}
}
out.inFlightSwift = sumInFlightSwift().toFixed(2);
return out;
}
function sumInFlightSwift(): number {
const inFlightPhases = new Set(['HYBX_RAIL_DISPATCHED', 'ISO20022_ARCHIVED', 'CHAIN_MINT_REQUESTED']);
return settlementStore
.list(500)
.filter(
(r) =>
inFlightPhases.has(r.phase) &&
(r.request.rail === 'SWIFT' || r.request.rail === 'RTGS' || r.request.rail === 'SEPA'),
)
.reduce((sum, r) => sum + (parseFloat(r.request.amount ?? '0') || 0), 0);
}

View File

@@ -1,6 +1,11 @@
import rateLimit from 'express-rate-limit';
import type { RequestHandler } from 'express';
import { isProductionSecurityMode, loadCustomerSecurityPolicy, resolveOmnlPrincipal } from '@dbis/integration-foundation';
import {
isOmnlPublicEndpoint,
isProductionSecurityMode,
loadCustomerSecurityPolicy,
resolveOmnlPrincipal,
} from '@dbis/integration-foundation';
function asHandler(h: ReturnType<typeof rateLimit>): RequestHandler {
return h as unknown as RequestHandler;
@@ -9,6 +14,7 @@ function asHandler(h: ReturnType<typeof rateLimit>): RequestHandler {
export const settlementRateLimiter = asHandler(
rateLimit({
windowMs: 60_000,
skip: (req) => isOmnlPublicEndpoint(req.path),
max: (req) => {
if (!isProductionSecurityMode()) return 200;
const policy = loadCustomerSecurityPolicy();

View File

@@ -142,6 +142,8 @@ export async function processExternalTransfer(
record.errors.push(
`M2/M3 backing insufficient for token load (${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`,
);
advance('FAILED');
return record;
}
const mint = await requestTokenMint({
lineId: req.convertToCrypto.tokenLineId,

View File

@@ -1,9 +1,13 @@
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs';
import path from 'path';
import {
buildMoneySupplySnapshot,
findM2TokenByLineId,
GL_CODES,
m2FiatToTokenLoadAmount,
m3TokenLoadAmount,
type M2TokenRegistryFile,
type TokenLoadRequest,
type SettlementRecord,
} from '@dbis/settlement-core';
@@ -12,6 +16,29 @@ import { fetchGlBalances, postJournalEntry, resolveGlAccountId } from '../adapte
import { requestTokenMint } from '../adapters/omnl';
import { settlementStore } from '../store/settlement-store';
function loadTokenRegistry(): M2TokenRegistryFile {
const registryPath =
process.env.OMNL_M2_TOKEN_REGISTRY ||
path.resolve(__dirname, '../../../config/omnl-m2-token-registry.v1.json');
return JSON.parse(fs.readFileSync(registryPath, 'utf8')) as M2TokenRegistryFile;
}
function resolveTokenAddress(req: TokenLoadRequest): string | undefined {
if (req.tokenAddress?.startsWith('0x')) return req.tokenAddress;
const registry = loadTokenRegistry();
const byLine = findM2TokenByLineId(registry, req.lineId);
if (byLine?.address.startsWith('0x') && byLine.address !== '0x0000000000000000000000000000000000000000') {
return byLine.address;
}
if (req.symbol) {
const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === req.symbol!.toLowerCase());
if (bySym?.address.startsWith('0x') && bySym.address !== '0x0000000000000000000000000000000000000000') {
return bySym.address;
}
}
return undefined;
}
export async function processTokenLoad(input: TokenLoadRequest): Promise<SettlementRecord> {
const cfg = loadConfig();
const profile = loadOfficeProfile();
@@ -106,7 +133,7 @@ export async function processTokenLoad(input: TokenLoadRequest): Promise<Settlem
settlementRef: record.settlementId,
dryRun: !cfg.production.allowChainMintExecute,
symbol: req.symbol,
tokenAddress: req.tokenAddress,
tokenAddress: resolveTokenAddress(req),
});
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
advance('SETTLED');

View File

@@ -21,6 +21,34 @@ import {
executeM2TokenMint,
chainRailConfigured,
} from '../../services/omnl-settlement-chain';
import fs from 'fs';
import path from 'path';
function resolveMintTokenAddress(lineId: string, symbol?: string, tokenAddress?: string): string | undefined {
if (tokenAddress?.startsWith('0x')) return tokenAddress;
const registryPath =
process.env.OMNL_M2_TOKEN_REGISTRY ||
path.resolve(__dirname, '../../../../config/omnl-m2-token-registry.v1.json');
try {
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as {
tokens: Array<{ omnlLine: string; symbol: string; address: string }>;
};
const id = lineId.trim().toUpperCase();
const byLine = registry.tokens.find((t) => t.omnlLine.toUpperCase() === id);
if (byLine?.address.startsWith('0x') && byLine.address !== '0x0000000000000000000000000000000000000000') {
return byLine.address;
}
if (symbol) {
const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === symbol.toLowerCase());
if (bySym?.address.startsWith('0x') && bySym.address !== '0x0000000000000000000000000000000000000000') {
return bySym.address;
}
}
} catch {
/* registry optional */
}
return undefined;
}
const router = Router();
router.use(omnlRateLimiter);
@@ -381,6 +409,9 @@ router.post('/omnl/settlement/token-load', async (req: Request, res: Response) =
return;
}
if (!tokenAddress?.startsWith('0x')) {
tokenAddress = resolveMintTokenAddress(lineId, symbol, tokenAddress);
}
if (!tokenAddress?.startsWith('0x')) {
res.status(400).json({
error: 'tokenAddress required for on-chain mint',