feat: SolaceNet gateway rails, IRU marketplace hardening, and docs

- Gateway adapter registry, rails routes, optional SOLACENET_GATEWAY_RAILS_ENFORCE; HTTP integration tests.
- IRU marketplace: rate limits, public routes, notifications/SMTP env docs; marketplace UI constants and flows.
- Quantum proxy legacy protocol types; debank/tezos/GSDS touch-ups; .env.example operator notes.
- SolaceNet doc set (gaps, runbooks, telecom schema example).

Tests: npm run test:iru-marketplace, npm run test:gateway (pass).
Note: full-repo tsc still reports unrelated legacy errors outside this change set.
Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-07 23:21:55 -07:00
parent 1190476b0a
commit 6ebf71dda8
75 changed files with 4104 additions and 338 deletions

View File

@@ -22,11 +22,55 @@ EMAIL_ALERT_RECIPIENTS=
CRYPTO_COM_API_KEY=
CRYPTO_COM_API_SECRET=
# ----------------------------------------------------------------------------
# API behind reverse proxy (rate limits, logging)
# TRUST_PROXY=1
# ----------------------------------------------------------------------------
# IRU marketplace — rate limits (optional; sensible defaults in code)
# With TRUST_PROXY=1, limits key on real client IP from X-Forwarded-For.
# IRU_MARKETPLACE_INQUIRY_WINDOW_MS=900000
# IRU_MARKETPLACE_INQUIRY_MAX=10
# IRU_MARKETPLACE_PUBLIC_WINDOW_MS=60000
# IRU_MARKETPLACE_PUBLIC_MAX=200
# Jest disables limits when IRU_MARKETPLACE_RATE_LIMIT_IN_TEST=1 (set in tests only)
# ----------------------------------------------------------------------------
# IRU / marketplace email (notification.service)
# Default provider is SMTP (EMAIL_PROVIDER=smtp). On Proxmox LAN use the Mail Proxy VM:
# VMID 100 — 192.168.11.32 (hostname proxmox-mail-gateway) — ports 25, 587, 465
# See: proxmox repo docs/04-configuration/ALL_VMIDS_ENDPOINTS.md (Mail Proxy note)
# EMAIL_PROVIDER=smtp
# SMTP_HOST=192.168.11.32
# SMTP_PORT=587
# SMTP_SECURE=false
# Lab relay with private CA: SMTP_TLS_REJECT_UNAUTHORIZED=false
# SMTP_USER=
# SMTP_PASSWORD=
# EMAIL_FROM=noreply@yourdomain.tld
# EMAIL_FROM_NAME=SolaceNet
# DBIS_SALES_EMAIL=sales@yourdomain.tld
# Or: EMAIL_PROVIDER=sendgrid | ses (set EMAIL_API_KEY / AWS creds per integration)
# ----------------------------------------------------------------------------
# SolaceNet — gateway rails (optional enforcement)
# ----------------------------------------------------------------------------
# When 1|true, POST/GET rail adapter endpoints require capability gateway-microservices.
# SOLACENET_GATEWAY_RAILS_ENFORCE=0
# Tenant for policy checks if not in x-tenant-id / body.tenantId:
# SOLACENET_DEFAULT_TENANT_ID=system
# Optional NDJSON audit file for allow/deny (create directory; rotate externally):
# SOLACENET_GATEWAY_AUDIT_LOG_PATH=/var/log/dbis/gateway-rails-audit.ndjson
# POST .../rails/:id/validate|receive — extra per-IP limit (default 120/min):
# GATEWAY_RAIL_MUTATE_WINDOW_MS=60000
# GATEWAY_RAIL_MUTATE_MAX=120
# Jest / integration: disable rail mutate limiter
# GATEWAY_RAIL_RATE_LIMIT_IN_TEST=1
# ----------------------------------------------------------------------------
# Other (add as needed from dbis_core code)
# ----------------------------------------------------------------------------
CHAIN138_RPC_URL=https://rpc-core.d-bis.org
# ADMIN_CENTRAL_API_KEY=
# VAULT_ROOT_TOKEN=
# DBIS_SALES_EMAIL=
# etc.

View File

@@ -66,6 +66,8 @@ This document tracks the implementation status of the SolaceNet Capability Platf
- Request routing
- Authentication/authorization
- **Note**: Requires Go 1.21+ and Redis
- **Rail / protocol governance**: External messaging rails and adapter contracts (SWIFT, DTC/DTCC, TT, KTT legacy evidence, and northbound contracts from telecom boundaries) are **maintained under SolaceNet**. See `docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md` and TypeScript adapters under `src/core/gateway/adapters/`.
- **Gateway REST (rails registry)**: `GET /api/v1/gateway/rails`, `GET/POST .../rails/:adapterId/health|validate|receive`, plus optional `SOLACENET_GATEWAY_RAILS_ENFORCE` on gateway routes — `src/core/gateway/routes/gateway.routes.ts`, `src/core/gateway/rails/gateway-rails-enforcement.ts`.
### ✅ Service SDK
- **Status**: Complete
@@ -273,6 +275,10 @@ To test the implementation:
3. Start the server: `npm run dev`
4. Test API endpoints using the Swagger UI: `http://localhost:3000/api-docs`
## Protocol gaps (rails and external messaging)
Tracked checklist with gap IDs, RTGS matrix cross-reference, and suggested close order: `docs/solacenet/PROTOCOL_GAPS_CHECKLIST.md`. Governance: `docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md`.
## Notes
- The Go gateway requires Go 1.21+ and Redis

View File

@@ -18,6 +18,12 @@ Quick reference guide for the SolaceNet Capability Platform.
- Channel (API/UI/mobile)
- Customer segment (optional)
### Rail and external protocol governance
Financial rails (SWIFT, DTC/DTCC, TT, KTT legacy evidence, etc.) and **integration contracts** for telecom-adjacent stacks (for example SS7 terminated at a carrier boundary) are **maintained under SolaceNet**: capabilities, policy, audit, Go gateway, and TypeScript adapters in `src/core/gateway/adapters/`. Full policy, code map, and change process: [docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md](docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md). **Tracked protocol gaps (complete list):** [docs/solacenet/PROTOCOL_GAPS_CHECKLIST.md](docs/solacenet/PROTOCOL_GAPS_CHECKLIST.md).
### Gateway REST (dbis_core API)
Authenticated routes under **`/api/v1/gateway`**: **`GET /rails`** (list adapter IDs), **`GET /rails/:adapterId/health`**, **`POST .../validate`**, **`POST .../receive`**, plus existing instructions and event replay. Optional SolaceNet enforcement: **`SOLACENET_GATEWAY_RAILS_ENFORCE=1`** and **`SOLACENET_DEFAULT_TENANT_ID`** — see `src/core/gateway/rails/README.md` and `.env.example`. OpenAPI: **`/api-docs`** (tag **SolaceNet Gateway Rails**).
## API Quick Reference
### Capability Registry
@@ -206,5 +212,7 @@ JWT_SECRET=your-secret
- **Services**: `src/core/solacenet/`
- **Shared SDK**: `src/shared/solacenet/`
- **Gateway**: `gateway/go/`
- **Rail adapters**: `src/core/gateway/adapters/` (governed per [docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md](docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md))
- **Rail enforcement env**: `src/core/gateway/rails/README.md`
- **Console**: `frontend/solacenet-console/`
- **Schema**: `prisma/schema.prisma`

View File

@@ -15,6 +15,7 @@ This document tracks the complete implementation of the IRU Production Readiness
### Phase 1: Marketplace & Portal Foundation ✅ COMPLETE
#### 1.1 Sankofa Phoenix Marketplace ✅
- **Catalog model:** Phoenix IRU marketplace rows are **partner-style** offerings (e.g. SolaceNet). **Native** platform services (VMs, IPs, app hosting, etc.) are documented and operated separately; see `docs/03-deployment/SANKOFA_MARKETPLACE_SURFACES.md` (native vs partner methodology).
- ✅ Database schema (IruOffering, IruInquiry, IruSubscription, IruAgreement)
- ✅ Backend services:
- `marketplace.service.ts` - Marketplace business logic

View File

@@ -0,0 +1,68 @@
# Audit — gaps, inconsistencies, and missing elements (SolaceNet + gateway + edges)
**Purpose:** Delta review beyond [PROTOCOL_GAPS_CHECKLIST.md](PROTOCOL_GAPS_CHECKLIST.md): naming drift, missing wiring, doc collisions, and security/ops gaps.
**Last updated:** 2026-04-07
---
## 1. Gaps (not yet covered elsewhere or under-specified)
| Topic | Detail |
|--------|--------|
| **HTTP integration tests** | ~~Added~~ `gateway-rails-http.integration.test.ts` + `createGatewayHttpTestApp`; run via `npm run test:gateway`. |
| **CI for gateway unit tests** | ~~Added~~ workflow `dbis-core-gateway-ci.yml` (Proxmox repo) on gateway path changes. |
| **Go edge vs Node rails** | Documented as **intentional layering** in [PLUGIN_AND_GATEWAY_BRIDGE.md](PLUGIN_AND_GATEWAY_BRIDGE.md) §6; optional future shared config if one edge must enumerate `dbis.adapter.*`. |
| **Outbox / send pipeline** | No shared **idempotency store**, **DLQ**, or **transactional outbox** for rail `send` / instruction fan-out. |
| **Audit export** | **Partial:** Winston + optional `SOLACENET_GATEWAY_AUDIT_LOG_PATH` NDJSON (`gateway-rails-audit.ts`). SIEM signing/long-term store still operator-owned. |
| **Provider connector data** | **Partial:** `npm run seed:gateway-provider` upserts `dbis-gateway-rail-plane`. |
| **Telecom northbound** | **Example schema** in `docs/solacenet/schemas/telecom-northbound-event.example.json`; AsyncAPI when boundary exists. |
| **Rate limits** | **Partial:** extra per-IP limiter on `POST .../validate` and `.../receive` (`GATEWAY_RAIL_MUTATE_*`). Per-tenant budgets still open. |
| **Adapter state & secrets** | `initialize(config, secretsRef)` not exercised by REST routes (always `{}`); **no** pattern for loading HSM/KMS refs per rail. |
---
## 2. Inconsistencies
| Topic | Detail |
|--------|--------|
| **Naming: marketplace vs quantum vs adapter ID** | Marketplace says **“KTT Legacy”**; quantum uses **`KTT_EVIDENCE`**; adapter id is **`dbis.adapter.ktt-evidence`**. Same concept, three namespaces — integrators need a mapping table. |
| **`EntitlementCheckContext.capabilityId`** | ~~Fixed~~ for payment-gateway, mobile-money, wallet-accounts, card-issuing, and tokenization routes; re-audit other `requireCapability` call sites if new services are added. |
| **`GET /api/v1/gateway/rails` vs enforcement** | **Listing** adapters does **not** call `maybeRequireGatewayMicroservices` (by design: metadata). Still requires **same `/api` auth** as other routes. Document so it is not mistaken for “public” data. |
| **Synthetic vs failed send** | SWIFT FIN / TT scaffolds return **`SENT`** without wire; KTT returns **`FAILED`** on send — **test harness semantics** differ; UIs/dashboards could mislead if not labeled scaffold. |
| **“Remaining tasks” doc collision** | Proxmox **`docs/00-meta/REMAINING_TASKS.md`** is **unrelated** to **`dbis_core/docs/solacenet/REMAINING_TASKS_FULL_LIST.md`** — similar names, different scope (confusing in search). |
| **MASTER_INDEX row** | SolaceNet row links into **`dbis_core/`** submodule path; if submodule not checked out, links 404 from browser — **operator** must open from full monorepo. |
| **Section C checklist** (fixed) | Was stale vs `LegacyProtocol`; checklist now references **`legacy-protocol-types.ts`**. |
---
## 3. Missing elements
| Element | Where it should live / action |
|---------|-------------------------------|
| **Rail name mapping table** | ~~Done~~ — [PLUGIN_AND_GATEWAY_BRIDGE.md](PLUGIN_AND_GATEWAY_BRIDGE.md) §5. |
| **Runbook: enable enforcement in prod** | ~~Done~~ — [SOLACENET_GATEWAY_RAILS_ENFORCE_RUNBOOK.md](SOLACENET_GATEWAY_RAILS_ENFORCE_RUNBOOK.md). |
| **OpenAPI request body** for `POST /instructions` | ~~Expanded~~ in `gateway.routes.ts` Swagger block. |
| **Go gateway** middleware hook (optional) | Still optional; see PLUGIN bridge §6. |
| **E2E list** | **Internal verifier:** `scripts/verify/check-dbis-core-gateway-rails.sh` (needs `DBIS_CORE_API_BASE` + bearer token). |
| **SEO / share** | **`/solacenet` document title** set in `SolaceNetPage` via `useEffect` (og tags still apex defaults unless SSR). |
| **`AGENTS.md` pointer** | ~~Done~~ — SolaceNet row links hub, full list, audit, API path. |
| **Cloudflare purge** | `purge-info-defi-oracle-cache.sh` purges static files only; **HTML** for new routes may still be served from stale **`index.html`** at edge in some setups — document **purge everything** or origin cache bust when needed. |
| **Thirdweb adapter** | **`ethers`** added as dependency; `Provider` / `Signer` via `import type` from `ethers` — full-repo `tsc` may still have unrelated errors. |
---
## 4. Recommended follow-ups (short)
1. ~~Add **rail mapping table** to [PLUGIN_AND_GATEWAY_BRIDGE.md](PLUGIN_AND_GATEWAY_BRIDGE.md)~~ — done (section 5 + Go vs Node §6).
2. ~~Add **integration test** for `/api/v1/gateway/rails`~~ — done: `src/__tests__/integration/api/gateway-rails-http.integration.test.ts` + `npm run test:gateway` (HTTP suite uses `jest.gateway-http.config.js` with `ts-jest` `diagnostics: false` so it does not require full-repo Prisma strict compile); CI: `.github/workflows/dbis-core-gateway-ci.yml`.
3. ~~Align **`EntitlementCheckContext`**~~ — done for payment-gateway, mobile-money, wallet-accounts, card-issuing, tokenization routes.
4. ~~Link **AGENTS.md**~~ — done (SolaceNet row includes full list + audit).
5. **Remaining:** production **outbox/DLQ**, per-tenant rate budgets, full **`tsc`** green for entire `dbis_core`, wire **`requireGatewayMicroservicesForWorker`** into real MQ/file workers.
---
## Related
- [PROTOCOL_GAPS_CHECKLIST.md](PROTOCOL_GAPS_CHECKLIST.md)
- [public/GAPS_AND_INCONSISTENCIES.md](public/GAPS_AND_INCONSISTENCIES.md)
- [REMAINING_TASKS_FULL_LIST.md](REMAINING_TASKS_FULL_LIST.md)

View File

@@ -0,0 +1,52 @@
# SolaceNet — Plugin registry vs gateway rail adapters
**Purpose:** Remove ambiguity between two integration surfaces in `dbis_core`.
## 1. Plugin registry (`src/integration/plugins/plugin-registry.ts`)
- **Intent:** Core-banking and ERP connectors (Temenos, Flexcube, SAP, Oracle Banking Platform).
- **Pattern:** `IPluginAdapter` instances registered by short name (`temenos`, `swift`, `iso20022`, …).
- **Use when:** Batch or API integration to a **core system** product, not necessarily a **marketplace rail** contract.
## 2. Gateway rail adapters (`src/core/gateway/adapters/`)
- **Intent:** Regulated **messaging rails** and evidence paths (SWIFT families, DTC/DTCC, TT, KTT evidence, Thirdweb contract invocation).
- **Pattern:** `GatewayAdapter` + **`gateway-adapter-registry.ts`** canonical IDs (`dbis.adapter.*`).
- **HTTP surface:** `/api/v1/gateway/rails`, `/api/v1/gateway/rails/:adapterId/health|validate|receive` (authenticated like other `/api/*` routes).
- **SolaceNet:** Optional enforcement via `SOLACENET_GATEWAY_RAILS_ENFORCE=1` and tenant resolution (`x-tenant-id` or body `tenantId`).
## 3. Bridge strategy (recommended)
1. **Do not** duplicate the same bank connection as both an unnamed `swift` plugin and a `dbis.adapter.swift-fin` rail without documentation.
2. Prefer **gateway adapters** for anything listed in `marketplace/gateway-microservices-offering.json` rails.
3. Implement a **facade service** if one physical SWIFT connection must feed both: the facade calls the single connector and exposes plugin vs gateway shapes.
4. Document the chosen path in the institutional onboarding pack.
## 4. Quantum / QPS alignment
`LegacyProtocol` in `src/infrastructure/quantum/proxy/legacy-protocol-types.ts` includes SolaceNet-extended rails so translation/compatibility APIs can reference the same protocol names where useful.
## 5. Rail name mapping (marketplace ↔ quantum ↔ adapter ID)
| Marketplace offering rail (`gateway-microservices-offering.json`) | `LegacyProtocol` (quantum) | Gateway adapter ID |
|----------------------------------|-----------------------------|-------------------|
| SWIFT FIN/MT | `SWIFT` | `dbis.adapter.swift-fin` |
| SWIFT ISO 20022 | `ISO20022` | `dbis.adapter.swift-iso` |
| SWIFT gpi | `SWIFT_GPI` | `dbis.adapter.swift-gpi` |
| DTC Settlement | `DTC_SETTLEMENT` | `dbis.adapter.dtc-settlement` |
| DTCC Family (NSCC) | `DTCC_NSCC` | `dbis.adapter.dtcc-nscc` |
| DTCC Family (FICC) | `DTCC_FICC` | `dbis.adapter.dtcc-ficc` |
| TT Route | `TT_ROUTE` | `dbis.adapter.tt-route` |
| KTT Legacy | `KTT_EVIDENCE` | `dbis.adapter.ktt-evidence` |
| Extensible Adapter SDK | N/A (integration pattern) | Add factory + ID in `gateway-adapter-registry.ts` |
**HYBX sidecars:** map per deployment in `docs/03-deployment/DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md` — not duplicated here.
## 6. Go edge gateway vs Node rails
The **Go** service under `gateway/go/` performs **generic** capability pre-checks from path/headers (`X-Capability-ID`, `X-Tenant-ID`, …). It does **not** embed the TypeScript **`dbis.adapter.*`** catalog. **Node** (`/api/v1/gateway/rails`) owns rail adapter IDs and enforcement via `gateway-rails-enforcement.ts`. Treat this as **intentional layering**: Go = edge policy cache + proxy; Node = rail semantics — or add a shared config artifact if a single edge must enumerate rails.
---
**Version:** 1.1
**Last updated:** 2026-04-07

View File

@@ -0,0 +1,164 @@
# SolaceNet — protocol gaps checklist (tracked)
**Purpose:** Single **complete** inventory of **protocol, messaging, and integration** gaps across `dbis_core`, SolaceNet, and cross-repo RTGS / DBIS Rail dependencies. Use for backlog triage, ownership, and closing the loop with [RAIL_AND_PROTOCOL_GOVERNANCE.md](RAIL_AND_PROTOCOL_GOVERNANCE.md).
**How to use status**
| Status | Meaning |
|--------|---------|
| `open` | Not implemented or only stub/placeholder |
| `partial` | Some path exists; production contract or wiring incomplete |
| `n/a` | Explicitly out of scope for this repo (document boundary only) |
| `done` | Production-capable per defined gate (update row when true) |
**Last updated:** 2026-04-07
---
## A. Carrier and telecom signaling
| ID | Protocol / stack | Location / note | Status | Close when |
|----|------------------|-----------------|--------|------------|
| PG-CAR-001 | **SS7** (MTP2/3, SCCP, ISUP, TCAP…) | Not in repo | `open` | N/A in app repo unless a **telecom boundary service** is chartered; SolaceNet owns northbound API only per governance doc |
| PG-CAR-002 | **SIGTRAN** (M2PA, M3UA, SUA over SCTP) | Not in repo | `open` | Same as PG-CAR-001 |
| PG-CAR-003 | **MAP / CAP / INAP** (circuit intelligence) | Not in repo | `open` | Same |
| PG-CAR-004 | **Diameter** (LTE/5G adjacent) | Not in repo | `open` | Same |
---
## B. `dbis_core` gateway rail adapters (TypeScript)
Path: `src/core/gateway/adapters/`. All are **scaffolds** unless status changed.
| ID | Rail | Adapter path | Status | Gap summary |
|----|------|--------------|--------|-------------|
| PG-GW-001 | SWIFT FIN/MT | `swift-fin/swift-fin-adapter.ts` | `open` | No SWIFTNet, no real MT build/parse/sign, synthetic SENT/ACK |
| PG-GW-002 | SWIFT ISO 20022 | `swift-iso/swift-iso-adapter.ts` | `open` | No transport, no XSD/JSON schema pipeline, no ACK semantics |
| PG-GW-003 | SWIFT gpi | `swift-gpi/swift-gpi-adapter.ts` | `open` | No UETR tracker integration |
| PG-GW-004 | DTC settlement | `dtc-settlement/dtc-settlement-adapter.ts` | `open` | No DTC message/API integration |
| PG-GW-005 | DTCC NSCC | `dtcc/dtcc-nscc-adapter.ts` | `open` | No NSCC API/protocol integration |
| PG-GW-006 | DTCC FICC | `dtcc/dtcc-ficc-adapter.ts` | `open` | No FICC API/protocol integration |
| PG-GW-007 | TT route | `tt-route/tt-route-adapter.ts` | `open` | Synthetic routing only |
| PG-GW-008 | KTT legacy evidence | `ktt-evidence/ktt-evidence-adapter.ts` | `open` | Trivial validate; **send unsupported**; no SoR match, no persistence contract |
| PG-GW-009 | Thirdweb (chain RPC) | `thirdweb/thirdweb-adapter.ts` | `partial` | Encode/path comments still **placeholder** in places; not a banking rail but shares adapter plane |
**B1. Wiring and registry**
| ID | Gap | Status | Close when |
|----|-----|--------|------------|
| PG-GW-W01 | No **single** gateway adapter registry importing all rail adapters | `partial` | **Registry + REST + OpenAPI + unit tests:** `gateway-adapter-registry.ts`, `/api/v1/gateway/rails*`, Swagger tag **SolaceNet Gateway Rails**, `src/__tests__/unit/core/gateway/*.test.ts`. **Remaining:** DI singleton policy, production connectors, optional `send` orchestration |
| PG-GW-W02 | **PluginRegistry** (`integration/plugins/`) vs **gateway adapters** — duplicate Swift/ISO concepts, no bridge doc in code | `partial` | **Doc:** [PLUGIN_AND_GATEWAY_BRIDGE.md](PLUGIN_AND_GATEWAY_BRIDGE.md). **Remaining:** optional facade service in code |
| PG-GW-W03 | SolaceNet **requireCapability** not consistently enforced on adapter ingress paths | `partial` | **On:** `SOLACENET_GATEWAY_RAILS_ENFORCE=1` for `/api/v1/gateway/rails/*`, `POST /instructions`, `GET /events/replay`. **Runbook:** [SOLACENET_GATEWAY_RAILS_ENFORCE_RUNBOOK.md](SOLACENET_GATEWAY_RAILS_ENFORCE_RUNBOOK.md). **Audit:** structured logs + optional `SOLACENET_GATEWAY_AUDIT_LOG_PATH` NDJSON; **`requireGatewayMicroservicesForWorker`** for MQ/file. **Remaining:** wire worker call sites + SIEM pipelines |
---
## C. Quantum / QPS legacy protocol coverage
Services: `src/infrastructure/quantum/proxy/quantum-translation.service.ts`, `quantum-compatibility.service.ts`.
**Type source:** `legacy-protocol-types.ts``LegacyProtocol` = core five plus **SolaceNet-extended** rails (`KTT_EVIDENCE`, `TT_ROUTE`, `DTC_SETTLEMENT`, `DTCC_NSCC`, `DTCC_FICC`, `SWIFT_GPI`, `MOJALOOP`, `RTGS`, `CARD_NETWORK`). **`GET /api/quantum-proxy/compatibility/protocols`** returns the full list.
| ID | Missing vs marketplace / gateway rails | Status |
|----|----------------------------------------|--------|
| PG-QP-001 | **KTT**, **TT route**, **DTC**, **DTCC** (NSCC/FICC), **SWIFT gpi** as distinct handling | `partial` | **Types + defaults** in `legacy-protocol-types.ts` / quantum translation & compatibility |
| PG-QP-002 | **RTGS** (generic), **Mojaloop**, **card networks**, **instant payment** schemes | `partial` | **RTGS**, **MOJALOOP**, **CARD_NETWORK** in `LegacyProtocol`; still scaffold scoring |
| PG-QP-003 | **Telecom northbound** (carrier boundary event schema) | `partial` | **Example JSON Schema:** [schemas/telecom-northbound-event.example.json](schemas/telecom-northbound-event.example.json). **Remaining:** boundary service + AsyncAPI |
---
## D. Admin dashboards and metrics (protocol surfaces)
| ID | Component | Path | Status | Gap |
|----|-----------|------|--------|-----|
| PG-AD-001 | GAS/QPS legacy rails | `gas-qps.service.ts` | `partial` | **SolaceNet adapter IDs** merged into `legacyRails`; volumes still 0 / heuristic until QPS integration |
| PG-AD-002 | GAS/QPS mapping profiles | same | `open` | Placeholder profiles only |
| PG-AD-003 | Global overview QPS / Ω / GPN / etc. | `global-overview.service.ts` | `open` | Marked placeholder |
| PG-AD-004 | SCB payment rails | `scb-overview.service.ts` | `open` | Payment rails placeholder |
---
## E. DBIS Rail — on-chain and authorization protocol (parent repo docs)
Source of truth for deployment truth: `docs/dbis-rail/DBIS_RAIL_AND_PROJECT_COMPLETION_MASTER_V1.md` (Proxmox repo).
| ID | Protocol / contract layer | Status | Gap summary |
|----|---------------------------|--------|-------------|
| PG-RAIL-001 | **DBIS_RootRegistry** | `open` | Contract set not implemented in repo |
| PG-RAIL-002 | **DBIS_ParticipantRegistry** | `open` | Same |
| PG-RAIL-003 | **DBIS_SignerRegistry** | `open` | Same |
| PG-RAIL-004 | **DBIS_SettlementRouter** | `open` | Same |
| PG-RAIL-005 | **DBIS_GRU_MintController** (router-only mint) | `open` | Mint path still owner-mint on c* per master doc |
| PG-RAIL-006 | **Production ISO Gateway** matching EIP-712 MintAuth pipeline | `partial` | Spec/rulebook complete; production service + on-chain registry not aligned |
| PG-RAIL-007 | **MintAuth relayer** operational hardening | `partial` | Documented; full ops gate open |
---
## F. RTGS and external messaging protocols (Proxmox matrix)
Canonical table: `docs/03-deployment/DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md`.
Below: rows that are **protocol, endpoint, or message-contract** heavy and **not Complete**. (Infrastructure rows like Besu/Explorer are omitted here; see matrix for full operational checklist.)
| ID | Matrix component | Typical protocol / contract gap | Matrix state (as of doc) |
|----|------------------|--------------------------------|--------------------------|
| PG-RTGS-001 | FireFly primary `6200` | Event/orchestration API contract across workflows | Partial |
| PG-RTGS-002 | FireFly secondary `6201` | HA / secondary node payload | Retired / standby |
| PG-RTGS-003 | Fabric `6000-6002` | Fabric channel/chaincode deployment contract if in scope | Partial |
| PG-RTGS-004 | Indy `6400-6402` | DID/ledger protocol production role | Partial |
| PG-RTGS-005 | Aries / AnonCreds / Ursa | DIDComm / credential protocol lifecycle | Partial |
| PG-RTGS-006 | Cacti | Cross-ledger connector protocol | Partial |
| PG-RTGS-007 | OMNL / Fineract API rail | REST/auth/tenant settlement contract | Partial |
| PG-RTGS-008 | Mifos X / Fineract UI | Operator/API procedures | Partial |
| PG-RTGS-009 | HYBX participant / treasury | Business protocol for nostro/vostro + IDs | Planned |
| PG-RTGS-010 | Depository / CSD | Securities settlement messaging model | Planned |
| PG-RTGS-011 | Global custodian | Custody reporting / instruction protocols | Planned |
| PG-RTGS-012 | FX pricing / dealing | Quote/booking protocol to OMNL | Planned |
| PG-RTGS-013 | Liquidity pooling + **source adapters** | Per-source bank/pool protocol contracts | Planned |
| PG-RTGS-014 | Custody / safekeeping lifecycle | End-to-end instruction + statement protocol | Planned |
| PG-RTGS-015 | **Mojaloop** | Quote/transfer/callback/settlement API contract | Planned |
| PG-RTGS-016 | HYBX sidecar layer + listed sidecars | Ingress/auth/retry/event protocols | Partial / Planned |
| PG-RTGS-017 | **mt103-hardcopy-sidecar** | MT103 ingest ↔ settlement correlation | Partial |
| PG-RTGS-018 | **card-networks-sidecar** | Card scheme settlement file/API protocol if in scope | Partial |
| PG-RTGS-019 | **securities-sidecar** | Securities instruction protocol vs CSD | Partial |
| PG-RTGS-020 | Chain 138 settlement path | Contract call graph as frozen protocol | Partial |
| PG-RTGS-021 | MerchantSettlementRegistry / WithdrawalEscrow | Invocation protocol in RTGS flow | Partial |
| PG-RTGS-022 | DBIS settlement tokens + reserve/oracle | Mint/burn and attestation protocols | Partial |
| PG-RTGS-023 | FireFly / sidecar / chain **event model** | Correlation ID, retry, DLQ protocol | Planned |
| PG-RTGS-024 | **ISO 20022 evidence and vault** | Archive/manifest/hash protocol | Partial |
| PG-RTGS-025 | **Institutional 4.995 package** | Submission package protocol | Partial |
| PG-RTGS-026 | **Indonesia / BNI** domestic | Live endpoint/auth/message contract | Planned |
| PG-RTGS-027 | **Global correspondent / liquidity bank** | SWIFT/ISO/correspondent API contract | Planned |
| PG-RTGS-028 | **ISO20022Router** on-chain + off-chain | G4 acceptance steps in matrix subsection | Partial until manual gate passes |
| PG-RTGS-029 | RTGS production gate | All mandatory protocol lanes green | Planned |
---
## G. SolaceNet capability vs rail implementation
| ID | Gap | Status | Close when |
|----|-----|--------|------------|
| PG-SN-001 | `gateway-microservices` capability describes rails; **adapters remain scaffolds** | `open` | Critical rails reach `done` in section B or scope reduced in offering JSON |
| PG-SN-002 | `solacenet_provider_connector` rows for each live rail provider | `partial` | **Seed:** `npm run seed:gateway-provider` (`dbis-gateway-rail-plane`). **Remaining:** per-institution connectors + bindings |
| PG-SN-003 | smom-dbis-138-publish / tokenization docs: **SolaceNet policy** integration called out as future in places | `partial` | Code paths call `requireCapability` or documented exception |
---
## H. Suggested close order (dependencies)
1. **PG-GW-W01 / W02 / W03** — Single registry + SolaceNet enforcement (enables safe iteration).
2. **PG-RAIL-001005 / 006** — On-chain authorization protocol (or explicit deferral).
3. **PG-RTGS-007, PG-RTGS-023** — OMNL + canonical event/correlation protocol.
4. **PG-GW-001008** — Replace scaffolds rail-by-rail with sandbox → prod endpoints.
5. **PG-RTGS-015, 026, 027** — External switch/domestic/correspondent protocols.
6. **PG-CAR-*** — Only if product scope includes carrier signaling; boundary service first.
---
## Related documents
- [REMAINING_TASKS_FULL_LIST.md](REMAINING_TASKS_FULL_LIST.md) — **prioritized full task list** (all remaining work in one place)
- [AUDIT_GAPS_INCONSISTENCIES_MISSING.md](AUDIT_GAPS_INCONSISTENCIES_MISSING.md) — **delta audit** (naming drift, missing CI/integration tests, edge gaps)
- [RAIL_AND_PROTOCOL_GOVERNANCE.md](RAIL_AND_PROTOCOL_GOVERNANCE.md)
- Proxmox: [DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md](../../../docs/03-deployment/DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md)
- Proxmox: [DBIS_RAIL_AND_PROJECT_COMPLETION_MASTER_V1.md](../../../docs/dbis-rail/DBIS_RAIL_AND_PROJECT_COMPLETION_MASTER_V1.md)
- `dbis_core/src/core/gateway/adapters/README.md`
- `dbis_core/marketplace/gateway-microservices-offering.json`

View File

@@ -0,0 +1,69 @@
# SolaceNet — rail and external protocol governance
**Purpose:** Define where **external messaging and carrier-style protocols** (payment rails, legacy evidence feeds, telecom-adjacent integrations) are **owned, versioned, and operated** inside the DBIS Core / SolaceNet stack.
**Authority:** SolaceNet is the **control plane** for capabilities, entitlements, policy, audit, and the **Go edge gateway** (`gateway/go/`). Rails and protocol adapters are **maintained under SolaceNet** as defined below—not as ad-hoc modules without registry or policy hooks.
---
## 1. What this covers
| Class | Examples | SolaceNet role |
|--------|-----------|----------------|
| **Financial messaging rails** | SWIFT FIN/MT, SWIFT ISO 20022, SWIFT gpi, DTC, DTCC family, TT route, KTT legacy evidence | Capability `gateway-microservices` / `gateway-adapters`; adapter implementations; schema and version registry; policy before execute |
| **Institutional evidence** | KTT-style legacy files, batch ingest, untrusted evidence until SoR match | Same; explicit “evidence-only” adapters; no mint/settlement without downstream checks |
| **Telecom signaling (SS7 family)** | MTP/SCCP/SIGTRAN, MAP/CAP at carrier edge | **Not implemented in application code here.** SolaceNet owns only the **northbound contract** (HTTPS/MQ/events) from a dedicated telecom boundary service; carrier stack stays outside this repo |
---
## 2. Maintenance responsibilities
1. **Capability registry** — Each rail or protocol surface is registered (or sub-capability under `gateway-adapters`) with `defaultState`, dependencies, and version.
2. **Policy** — Ingress and execution require `requireCapability` / gateway middleware alignment per tenant, region, and channel.
3. **Audit** — Toggle changes, kill-switch use, and (where implemented) adapter decisions feed SolaceNet audit patterns.
4. **Adapter code** — Canonical TypeScript adapters live under `src/core/gateway/adapters/`; the **edge** is `gateway/go/` (proxy, auth, capability pre-check).
5. **Offerings and marketplace** — JSON offerings (e.g. `marketplace/gateway-microservices-offering.json`) declare rails; **governance** field identifies SolaceNet as maintainer.
---
## 3. Code and doc map
| Artifact | Path |
|----------|------|
| Capability platform | `src/core/solacenet/` |
| SolaceNet SDK | `src/shared/solacenet/sdk.ts` |
| Go API gateway | `gateway/go/` |
| Rail adapters (TS) | `src/core/gateway/adapters/` |
| Docker stack | `docker-compose.solacenet.yml` |
| Operator quick reference | `SOLACENET_QUICK_REFERENCE.md` (repo root of `dbis_core`) |
| This governance doc | `docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md` |
---
## 4. Adding or changing a protocol
1. Register or update **capability** (and entitlements) in the SolaceNet registry.
2. Add adapter under `src/core/gateway/adapters/<rail>/` implementing `GatewayAdapter` (`sdk/adapter-interface.ts`).
3. Wire adapter into the **single** gateway adapter registry when introduced (avoid parallel unregistered copies).
4. Document trust model (trusted rail vs evidence-only vs telecom boundary).
5. Update marketplace offering JSON and, if needed, seed scripts (`scripts/seed-gateway-capability.ts`).
---
## 5. Related platform notes
- **Plugin registry** (`src/integration/plugins/plugin-registry.ts`) covers core-banking connectors; **rail protocol contracts** for regulated messaging should align with this governance doc or be explicitly bridged—do not treat plugins and gateway adapters as competing sources of truth without documentation. **Bridge guide:** [PLUGIN_AND_GATEWAY_BRIDGE.md](PLUGIN_AND_GATEWAY_BRIDGE.md).
- **DBIS Rail / Chain 138** settlement docs in the parent Proxmox repo describe on-chain authorization; SolaceNet rails feed **off-chain** evidence and controls that must satisfy those specs before submission.
## 6. Tracked protocol gaps
Maintained checklist (IDs, status, close criteria, RTGS cross-reference): [PROTOCOL_GAPS_CHECKLIST.md](PROTOCOL_GAPS_CHECKLIST.md).
## 7. Public documentation
Markdown for **public** audiences (mirrored in spirit on **info.defi-oracle.io/solacenet**): [public/OVERVIEW.md](public/OVERVIEW.md), [public/GAPS_AND_INCONSISTENCIES.md](public/GAPS_AND_INCONSISTENCIES.md). Hub index: `docs/04-configuration/SOLACENET_PUBLIC_HUB.md` (parent Proxmox repo).
---
**Version:** 1.2
**Last updated:** 2026-04-07

View File

@@ -0,0 +1,171 @@
# SolaceNet, gateway rails, and RTGS — full remaining task list
**Purpose:** One actionable backlog derived from [PROTOCOL_GAPS_CHECKLIST.md](PROTOCOL_GAPS_CHECKLIST.md), [public/GAPS_AND_INCONSISTENCIES.md](public/GAPS_AND_INCONSISTENCIES.md), and the Proxmox RTGS / DBIS Rail docs. Update row statuses in the checklist as tasks close.
**Last reviewed:** 2026-04-07
**Priority legend**
| Tier | Meaning |
|------|---------|
| **P0** | Blocks honest production claims or regulatory narrative alignment |
| **P1** | Core path to first production slice (settlement + evidence + one live rail) |
| **P2** | Breadth, observability, UX, secondary rails |
| **P3** | Optional, carrier, or explicit deferrals |
---
## P0 — Truth in advertising and critical alignment
| # | Task | Gap ID(s) | Notes |
|---|------|-----------|--------|
| 1 | **Reconcile marketplace/offering copy** with live connector maturity — or scope down `gateway-microservices-offering.json` until rails are real | PG-SN-001, offering JSON | **Updated 2026-04-08:** description names scaffolds + KTT evidence semantics; keep row until production connectors ship. |
| 2 | **Document KTT explicitly** as evidence-only in marketplace/UI copy; separate from symmetric send/receive rails | PG-GW-008, inconsistencies doc | Code already fails `send` by design |
| 3 | **Decide DBIS Rail scope:** implement contracts + router-only mint **or** formally defer and update all docs that imply live Rail | PG-RAIL-001005, master doc | See `docs/dbis-rail/DBIS_RAIL_AND_PROJECT_COMPLETION_MASTER_V1.md` |
| 4 | **Fix or quarantine Thirdweb adapter** type issues (`ethers` namespace) for clean `tsc` / CI | PG-GW-009 | **Partial:** `ethers` dependency + `import type { Provider, Signer }`; full `dbis_core` `tsc` may still fail elsewhere. |
---
## P1 — Gateway core (SolaceNet + REST + enforcement)
| # | Task | Gap ID(s) | Notes |
|---|------|-----------|--------|
| 5 | **DI / lifecycle policy** for rail adapters (singleton vs per-request) and document | PG-GW-W01 | Registry exists; avoid accidental state bleed |
| 6 | **Orchestrated `send` path** (outbox, idempotency, DLQ) for rails that support outbound messages | PG-GW-W01, PG-GW-001007 | KTT may stay receive-only |
| 7 | **Extend `SOLACENET_GATEWAY_RAILS_ENFORCE`** to every rail ingress (file drop, MQ, future workers) + **audit log** export for denied/allowed | PG-GW-W03 | HTTP path partially done |
| 8 | **Optional code facade** for PluginRegistry ↔ gateway adapters when one physical connector serves both | PG-GW-W02 | Doc exists; implement if needed |
| 9 | **Register `solacenet_provider_connector`** rows per environment for each live provider | PG-SN-002 | Tie to capability bindings |
|10 | **smom-dbis-138-publish / tokenization:** wire `requireCapability` or document explicit exception | PG-SN-003 | |
---
## P1 — Replace rail scaffolds (production connectors)
| # | Task | Gap ID | Notes |
|---|------|--------|--------|
|11| SWIFT FIN/MT — SWIFTNet, crypto, parse, ACK/NAK, sandbox → prod | PG-GW-001 | |
|12| SWIFT ISO 20022 — transport + schema validation + status | PG-GW-002 | |
|13| SWIFT gpi — UETR tracker integration | PG-GW-003 | |
|14| DTC settlement — real message/API | PG-GW-004 | |
|15| DTCC NSCC — API/protocol | PG-GW-005 | |
|16| DTCC FICC — API/protocol | PG-GW-006 | |
|17| TT route — real routing contract | PG-GW-007 | |
|18| KTT evidence — real validate, persistence, **SoR match**, correlation IDs; keep or drop `send` | PG-GW-008 | |
|19| Thirdweb — remove encode placeholders or document as non-prod | PG-GW-009 | |
---
## P1 — DBIS Rail off-chain + relayer
| # | Task | Gap ID | Notes |
|---|------|--------|--------|
|20| **Production ISO Gateway** aligned with EIP-712 MintAuth + `accountingRef` / `isoHash` | PG-RAIL-006 | Spec/rulebook exist |
|21| **MintAuth relayer** — ops runbook, monitoring, key rotation, incident drill | PG-RAIL-007 | |
---
## P1 — RTGS spine (canonical docs matrix)
*Full row-by-row status lives in* `docs/03-deployment/DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md`. *Tasks below are the protocol-heavy items still not Complete.*
| # | Task | Gap ID | Matrix state |
|---|------|--------|--------------|
|22| Freeze **OMNL / Fineract** tenant, auth, settlement API contract | PG-RTGS-007 | Partial |
|23| **Canonical event model** — correlation ID, retry, DLQ, FireFly/sidecar/chain | PG-RTGS-023 | Planned |
|24| **Mojaloop** — live endpoint, auth, quote/transfer/callback | PG-RTGS-015 | Planned |
|25| **Indonesia / BNI** — live domestic contract | PG-RTGS-026 | Planned |
|26| **Global correspondent** — SWIFT/ISO, nostro/vostro, confirmations | PG-RTGS-027 | Planned |
|27| **ISO 20022 evidence + vault** — manifests, hashes, legal path | PG-RTGS-024 | Partial |
|28| **Institutional 4.995 package**`--strict` readiness, real materials | PG-RTGS-025 | Partial |
|29| **ISO20022Router** — complete G4 manual acceptance (deploy, payload, tx, correlation, evidence) | PG-RTGS-028 | Partial |
|30| Freeze **Chain 138 settlement** contract graph for RTGS | PG-RTGS-020 | Partial |
|31| Place **MerchantSettlementRegistry** / **WithdrawalEscrow** in canonical flows | PG-RTGS-021 | Partial |
|32| **MT103 hardcopy sidecar** — ingest ↔ settlement correlation | PG-RTGS-017 | Partial |
|33| **HYBX sidecars** — boundaries, auth, retries (all listed in matrix) | PG-RTGS-016 | Partial / Planned |
|34| **RTGS production gate** — all mandatory rows green for chosen architecture | PG-RTGS-029 | Planned |
---
## P2 — Hyperledger / identity / interoperability (RTGS matrix)
| # | Task | Gap ID | Notes |
|---|------|--------|--------|
|35| FireFly primary — orchestration role + real workflow | PG-RTGS-001 | Partial |
|36| FireFly secondary — rebuild or formally exclude | PG-RTGS-002 | Retired / standby |
|37| Fabric — production topology decision | PG-RTGS-003 | Partial |
|38| Indy — production role decision | PG-RTGS-004 | Partial |
|39| Aries / AnonCreds / Ursa — lifecycle validation | PG-RTGS-005 | Partial |
|40| Cacti — cross-ledger contract | PG-RTGS-006 | Partial |
|41| Mifos X / operator procedures | PG-RTGS-008 | Partial |
|42| HYBX participant / treasury / nostro-vostro model | PG-RTGS-009 | Planned |
|43| Depository / CSD layer | PG-RTGS-010 | Planned |
|44| Global custodian | PG-RTGS-011 | Planned |
|45| FX pricing / dealing engine | PG-RTGS-012 | Planned |
|46| Liquidity pooling + source adapters | PG-RTGS-013 | Planned |
|47| Custody / safekeeping lifecycle | PG-RTGS-014 | Planned |
|48| Card-networks sidecar — scope + protocol | PG-RTGS-018 | Partial |
|49| Securities sidecar — CSD alignment | PG-RTGS-019 | Partial |
|50| Settlement tokens + reserve/oracle mapping | PG-RTGS-022 | Partial |
---
## P2 — Quantum / QPS and admin dashboards
| # | Task | Gap ID | Notes |
|---|------|--------|--------|
|51| Replace **scaffold** FX/risk/scoring for `LegacyProtocol` extended rails with real rules | PG-QP-001, PG-QP-002 | Types exist |
|52| **Telecom northbound event schema** (when boundary service exists) | PG-QP-003 | |
|53| **GAS/QPS mapping profiles** — real profiles, not placeholders | PG-AD-002 | |
|54| **Global overview** — QPS / Ω / GPN placeholders → real metrics or remove | PG-AD-003 | |
|55| **SCB payment rails** — real data source | PG-AD-004 | |
|56| **GAS/QPS** — wire true volumes per rail (not only adapter ID list + heuristics) | PG-AD-001 | |
---
## P3 — Carrier / telecom (out of app repo unless product requires)
| # | Task | Gap ID | Notes |
|---|------|--------|--------|
|57| Charter **telecom boundary service** + northbound API into SolaceNet | PG-CAR-001004 | SS7/SIGTRAN/MAP/CAP/Diameter |
---
## P3 — Consistency and hygiene (ongoing)
| # | Task | Source | Notes |
|---|------|--------|--------|
|58| Align **outbox semantics** — synthetic `SENT` vs KTT `FAILED` on send; document test vs prod behavior | GAPS_AND_INCONSISTENCIES | |
|59| **QPS “SWIFT” volume** — replace `iso_messages` string heuristic with real classification | GAPS_AND_INCONSISTENCIES | |
|60| Keep **agent-hints / sitemap / verify script** in sync when adding public routes | Public gaps §3 | |
|61| Run **`pnpm run verify:info-defi-oracle-public`** after each hub deploy | Ops | |
|62| Optional **`pnpm run audit:info-defi-oracle-site`** (Playwright) in release cadence | Ops | |
|63| Maintain **`PROTOCOL_GAPS_CHECKLIST.md`** statuses as backlog moves | Process | |
---
## Infrastructure rows (not duplicated here)
Besu, Explorer, Caliper benchmarks, and other **non-protocol** RTGS matrix rows remain in `DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md` — track there for VM/LXC/ops completeness.
---
## Suggested execution waves (summary)
1. **Wave A:** P0 items + PG-RAIL decision + first real rail sandbox (pick one: SWIFT ISO or domestic).
2. **Wave B:** OMNL freeze + event model + ISO evidence path + ISO20022Router G4.
3. **Wave C:** Remaining gateway adapters + provider connectors + QPS real metrics.
4. **Wave D:** Mojaloop / BNI / correspondent as jurisdiction requires.
5. **Wave E:** P2 institutional layers (CSD, custody, FX engine) per product scope.
6. **Wave F:** Carrier only if explicitly in scope.
---
## Related documents
- [PROTOCOL_GAPS_CHECKLIST.md](PROTOCOL_GAPS_CHECKLIST.md) — ID-level tracking
- [public/GAPS_AND_INCONSISTENCIES.md](public/GAPS_AND_INCONSISTENCIES.md) — narrative gaps
- [RAIL_AND_PROTOCOL_GOVERNANCE.md](RAIL_AND_PROTOCOL_GOVERNANCE.md)
- [PLUGIN_AND_GATEWAY_BRIDGE.md](PLUGIN_AND_GATEWAY_BRIDGE.md)
- Proxmox: `docs/03-deployment/DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md`
- Proxmox: `docs/dbis-rail/DBIS_RAIL_AND_PROJECT_COMPLETION_MASTER_V1.md`
- Proxmox: `docs/04-configuration/SOLACENET_PUBLIC_HUB.md`

View File

@@ -0,0 +1,36 @@
# Runbook — enable `SOLACENET_GATEWAY_RAILS_ENFORCE` (gateway rails)
**Purpose:** Turn on SolaceNet capability checks for mutating gateway rail endpoints and related ingress (`/api/v1/gateway/rails/*` except `GET /rails` list, `POST /instructions`, `GET /events/replay`).
**Prerequisites**
- Policy engine / entitlements can resolve `gateway-microservices` for target tenants.
- Tenants that must use rails have **active entitlements** (or policy allow) for capability id **`gateway-microservices`**.
- Clients send **`x-tenant-id`** (or `tenantId` in JSON body where applicable). Optional default: set **`SOLACENET_DEFAULT_TENANT_ID`** in environment for lab only.
**Audit**
- Every allow/deny is logged as structured **`Gateway rails enforcement audit`** (Winston).
- Optional **NDJSON file:** set `SOLACENET_GATEWAY_AUDIT_LOG_PATH` to an operator-writable path; append-only, rotate externally.
**Steps**
1. **Seed or verify entitlements** for each production tenant that should call rail adapters.
2. Set environment on the API process:
- `SOLACENET_GATEWAY_RAILS_ENFORCE=1` (or `true`)
- Optionally `SOLACENET_DEFAULT_TENANT_ID=…` for single-tenant lab (not recommended for multi-tenant prod).
3. **Smoke test without entitlement:** call `GET /api/v1/gateway/rails/dbis.adapter.ktt-evidence/health` with a tenant that lacks the capability — expect **403** with a forbidden message.
4. **Smoke test with entitlement:** same call with an entitled tenant — expect **200** and health JSON.
5. **Confirm list endpoint:** `GET /api/v1/gateway/rails` remains **200** (metadata only; still requires normal API auth).
**Rollback**
- Unset `SOLACENET_GATEWAY_RAILS_ENFORCE` or set to `0` / `false` and restart the API.
**Related**
- `src/core/gateway/rails/gateway-rails-enforcement.ts`
- `dbis_core/.env.example` — SolaceNet gateway variables
- `AUDIT_GAPS_INCONSISTENCIES_MISSING.md` — SIEM/export pipelines beyond file + logs remain optional
**Version:** 1.0 · **Last updated:** 2026-04-07

View File

@@ -0,0 +1,59 @@
# SolaceNet — gaps and inconsistencies (consolidated)
**Purpose:** Single list of **protocol gaps**, **product/documentation mismatches**, and **structural inconsistencies** relevant to SolaceNet, gateway rails, and public messaging. For **actionable gap IDs** and close criteria, use [PROTOCOL_GAPS_CHECKLIST.md](../PROTOCOL_GAPS_CHECKLIST.md). For a **delta audit** (missing CI, naming drift, dual edge), see [AUDIT_GAPS_INCONSISTENCIES_MISSING.md](../AUDIT_GAPS_INCONSISTENCIES_MISSING.md).
**Last updated:** 2026-04-07
---
## 1. Protocol and implementation gaps (summary)
- **Carrier signaling (SS7/SIGTRAN/MAP/CAP/Diameter):** Not implemented in application repos; only a **northbound integration contract** is in scope for SolaceNet if a telecom boundary service is added.
- **All TypeScript gateway rail adapters** under `src/core/gateway/adapters/` remain **scaffolds** (synthetic success or evidence-only stubs) until replaced with live connectors.
- **KTT evidence adapter:** `send` is explicitly unsupported; **validate** is trivial; no system-of-record match.
- **DBIS Rail on-chain contracts** (RootRegistry, ParticipantRegistry, SignerRegistry, SettlementRouter, GRU_MintController): **not deployed** per Proxmox DBIS Rail master status; **owner mint** vs **router-only mint** remains a gap.
- **Quantum / QPS** — **Partial:** `LegacyProtocol` now includes SolaceNet-aligned rails (KTT evidence, TT, DTC/DTCC, gpi, Mojaloop, RTGS, card); scoring remains **scaffold** until field contracts are frozen.
- **Admin dashboards** (`gas-qps`, `global-overview`, `scb-overview`): **placeholder** rail metrics and profiles.
- **RTGS / external bank protocols:** Many matrix rows **Planned** or **Partial** (Mojaloop, BNI, correspondent/SWIFT live contracts, ISO evidence packages, etc.)—see `docs/03-deployment/DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md` in the Proxmox repo.
---
## 2. Inconsistencies (architecture and naming)
| Topic | Issue |
|--------|--------|
| **Dual adapter systems** | **Mitigated (doc):** [PLUGIN_AND_GATEWAY_BRIDGE.md](../PLUGIN_AND_GATEWAY_BRIDGE.md). **Remaining:** optional facade in code if one physical connector serves both shapes. |
| **KTT vs other rails** | Marketplace lists **KTT Legacy** next to full **send/receive** rails; code is **evidence-only** and **send** fails by design. |
| **Synthetic success** | SWIFT FIN / TT scaffolds return **SENT** without network I/O; **KTT** returns **FAILED** on send—mixed semantics for “outbox” testing. |
| **Thirdweb in gateway folder** | Shares adapter plane with **bank rails**; not a messaging rail—boundary should stay documented for integrators. |
| **Offering copy vs reality** | `gateway-microservices-offering.json` describes a regulated fabric; **implementations are not production-grade** connectors yet. |
| **QPS dashboard “SWIFT” volume** | Derived from `iso_messages` heuristics (`messageType.includes('SWIFT')`)—**not** true SWIFT FIN volume. |
---
## 3. Public web and documentation gaps
| Topic | Issue |
|--------|--------|
| **info.defi-oracle.io** | **Mitigated:** `/solacenet` SPA page and nav link; `agent-hints.json`, `sitemap.xml`, `llms.txt`, and `verify:info-defi-oracle-public` updated. Deep technical docs remain in repo markdown only. |
| **Cross-repo discovery** | **Mitigated:** `docs/04-configuration/SOLACENET_PUBLIC_HUB.md` indexes public web + `dbis_core/docs/solacenet/`. |
| **Agent hints / sitemap** | Ongoing hygiene: any **new** public routes must update the same artifacts. |
---
## 4. Recommended next steps (ordered)
1. Keep **PROTOCOL_GAPS_CHECKLIST.md** statuses current as code changes.
2. Enforce **SolaceNet capability checks** on any new rail ingress path.
3. Replace scaffolds **rail-by-rail** with sandbox then production connectors; update **public** pages to reflect “generally available” only when true.
4. Unify or document **PluginRegistry ↔ gateway adapters** with one facade.
5. Align **quantum** protocol enums with **marketplace** rails or explicitly exclude them.
---
## Related documents
- [PROTOCOL_GAPS_CHECKLIST.md](../PROTOCOL_GAPS_CHECKLIST.md)
- [RAIL_AND_PROTOCOL_GOVERNANCE.md](../RAIL_AND_PROTOCOL_GOVERNANCE.md)
- Proxmox: [DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md](../../../../docs/03-deployment/DBIS_RTGS_E2E_REQUIREMENTS_MATRIX.md)
- Proxmox: [DBIS_RAIL_AND_PROJECT_COMPLETION_MASTER_V1.md](../../../../docs/dbis-rail/DBIS_RAIL_AND_PROJECT_COMPLETION_MASTER_V1.md)

View File

@@ -0,0 +1,39 @@
# SolaceNet — public overview
**Audience:** Institutions, integrators, and supervisors evaluating how **capabilities, policy, and rails** are governed in the DBIS Core stack. This page is **non-operational**: it does not expose credentials, internal endpoints, or live tenant data.
---
## What SolaceNet is
**SolaceNet** is the **capability and control plane** in DBIS Core: it registers **what** product functions may run (payment gateway, tokenization, limits, fees, **gateway microservices / rail adapters**), for **whom** (tenant, program, region, channel), and under **which policy** (allow/deny, kill switch, audit). The **Go API gateway** (`dbis_core/gateway/go/`) can enforce capability checks at the edge before traffic reaches Node services.
**Rail and messaging protocols** (for example SWIFT families, DTC/DTCC, telegraphic transfer routes, legacy evidence ingest such as KTT) are **maintained under SolaceNet**—see internal governance: `docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md`.
---
## What the public should expect
| Topic | Public | Authenticated / contract |
|--------|--------|---------------------------|
| **Existence and scope** of SolaceNet | Yes — this document and the info hub page | — |
| **Capability names and high-level behavior** | Summarized | Full API schemas and runbooks |
| **Rail adapter wire protocols** | Described at category level only | Message formats, endpoints, keys, IPs |
| **Production status** | “Integration in progress” where adapters are scaffolds | Environment-specific readiness |
| **Chain 138 settlement** | Via [info.defi-oracle.io](https://info.defi-oracle.io) hub and explorer | DBIS Rail / RTGS runbooks (restricted) |
---
## Related reading (repository)
- **Governance:** `dbis_core/docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md`
- **Protocol gap tracking:** `dbis_core/docs/solacenet/PROTOCOL_GAPS_CHECKLIST.md`
- **Gaps and inconsistencies (product/doc):** `dbis_core/docs/solacenet/public/GAPS_AND_INCONSISTENCIES.md`
- **Operator quick reference:** `dbis_core/SOLACENET_QUICK_REFERENCE.md`
- **Proxmox public hub pointer:** `docs/04-configuration/SOLACENET_PUBLIC_HUB.md`
- **Authenticated API (integrators):** `GET /api/v1/gateway/rails` and related routes on DBIS Core (see `gateway.routes.ts`); optional `SOLACENET_GATEWAY_RAILS_ENFORCE``dbis_core/.env.example`
---
**Version:** 1.0
**Last updated:** 2026-04-07

View File

@@ -0,0 +1,12 @@
# SolaceNet — public documentation (markdown)
Markdown in this folder is the **canonical source** for public-facing SolaceNet narrative. The live **web** summary is published on **https://info.defi-oracle.io/solacenet** (see `info-defi-oracle-138` in the Proxmox repo).
| File | Purpose |
|------|---------|
| [OVERVIEW.md](OVERVIEW.md) | What SolaceNet is; public vs authenticated expectations; repo pointers |
| [GAPS_AND_INCONSISTENCIES.md](GAPS_AND_INCONSISTENCIES.md) | Consolidated gaps and doc/product inconsistencies |
**Internal** (not necessarily reproduced on the public site): [RAIL_AND_PROTOCOL_GOVERNANCE.md](../RAIL_AND_PROTOCOL_GOVERNANCE.md), [PROTOCOL_GAPS_CHECKLIST.md](../PROTOCOL_GAPS_CHECKLIST.md), [REMAINING_TASKS_FULL_LIST.md](../REMAINING_TASKS_FULL_LIST.md) (prioritized backlog), [AUDIT_GAPS_INCONSISTENCIES_MISSING.md](../AUDIT_GAPS_INCONSISTENCIES_MISSING.md) (delta audit), [PLUGIN_AND_GATEWAY_BRIDGE.md](../PLUGIN_AND_GATEWAY_BRIDGE.md).
**Operator index:** `docs/04-configuration/SOLACENET_PUBLIC_HUB.md` (Proxmox repo root).

View File

@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://dbis.example/schemas/solacenet/telecom-northbound-event.example.json",
"title": "Telecom boundary → Core northbound event (example)",
"description": "Placeholder contract for PG-QP-003 when a carrier boundary service exists. Not wire format for SS7; JSON over HTTPS/Kafka from the boundary only.",
"type": "object",
"required": ["eventId", "eventType", "occurredAt", "correlationId", "tenantId"],
"properties": {
"eventId": { "type": "string", "format": "uuid" },
"eventType": {
"type": "string",
"examples": ["carrier.signaling.observed", "carrier.route.health", "carrier.fraud.signal"]
},
"occurredAt": { "type": "string", "format": "date-time" },
"correlationId": { "type": "string" },
"tenantId": { "type": "string" },
"programId": { "type": "string" },
"region": { "type": "string" },
"channel": { "type": "string", "const": "TELECOM_NORTHBOUND" },
"payload": {
"type": "object",
"additionalProperties": true,
"description": "Opaque carrier-normalized facts; no raw MAP/CAP on this bus."
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,30 @@
/**
* Sankofa Marketplace — product and vendor labels.
* SolaceNet is the commercial name for the IRU offering line; other products may be added over time.
*/
import { OfferMetadata } from '@/constants/offerTaxonomy';
export {
BILLING_MODE_LABELS,
COMMERCIAL_MODEL_LABELS,
FULFILLMENT_MODE_LABELS,
OFFER_STATUS_LABELS,
OFFER_TYPE_LABELS,
SUPPORT_OWNER_LABELS,
} from '@/constants/offerTaxonomy';
export const MARKETPLACE_NAME = 'Sankofa Phoenix Marketplace';
export const SOLACENET_PRODUCT_NAME = 'SolaceNet';
export const SOLACENET_VENDOR_NAME = 'Solace Bank Group PLC';
/** Legal / technical term shown alongside the product name */
export const SOLACENET_IRU_LABEL = 'Irrevocable Right of Use (IRU)';
export const SOLACENET_OFFER_METADATA: OfferMetadata = {
offerType: 'partner',
commercialModel: 'IRU',
supportOwner: 'partner',
fulfillmentMode: 'request_only',
billingMode: 'contract',
status: 'request_only',
};

View File

@@ -0,0 +1,57 @@
export type OfferType = 'native' | 'partner';
export type CommercialModel =
| 'IRU'
| 'SaaS'
| 'managed_service'
| 'reserved_capacity'
| 'custom';
export type SupportOwner = 'sankofa' | 'partner' | 'shared';
export type FulfillmentMode = 'self_service' | 'request_only' | 'operator_provisioned';
export type BillingMode = 'subscription' | 'contract' | 'quote';
export type OfferStatus = 'active' | 'preview' | 'request_only';
export interface OfferMetadata {
offerType: OfferType;
commercialModel: CommercialModel;
supportOwner: SupportOwner;
fulfillmentMode: FulfillmentMode;
billingMode: BillingMode;
status: OfferStatus;
}
export const OFFER_TYPE_LABELS: Record<OfferType, string> = {
native: 'Native offer',
partner: 'Partner offer',
};
export const COMMERCIAL_MODEL_LABELS: Record<CommercialModel, string> = {
IRU: 'IRU',
SaaS: 'SaaS',
managed_service: 'Managed service',
reserved_capacity: 'Reserved capacity',
custom: 'Custom commercial model',
};
export const SUPPORT_OWNER_LABELS: Record<SupportOwner, string> = {
sankofa: 'Sankofa support',
partner: 'Partner support',
shared: 'Shared support',
};
export const FULFILLMENT_MODE_LABELS: Record<FulfillmentMode, string> = {
self_service: 'Self-service',
request_only: 'Request only',
operator_provisioned: 'Operator provisioned',
};
export const BILLING_MODE_LABELS: Record<BillingMode, string> = {
subscription: 'Subscription billing',
contract: 'Contract billing',
quote: 'Quote-based billing',
};
export const OFFER_STATUS_LABELS: Record<OfferStatus, string> = {
active: 'Active',
preview: 'Preview',
request_only: 'Request only',
};

View File

@@ -1,8 +1,8 @@
// Agreement Viewer Component
// Preview and e-signature for IRU Agreement
// SolaceNet (IRU) agreement preview / e-signature
import React, { useState, useEffect } from 'react';
import { apiClient } from '@/services/api/client';
import { SOLACENET_PRODUCT_NAME, SOLACENET_VENDOR_NAME } from '@/constants/marketplace';
interface AgreementViewerProps {
agreementId?: string;
@@ -27,7 +27,7 @@ export const AgreementViewer: React.FC<AgreementViewerProps> = ({
// For now, use placeholder
setLoading(false);
setAgreement({
content: 'IRU Participation Agreement content will be loaded here...',
content: `${SOLACENET_PRODUCT_NAME} participation agreement content will be loaded here...`,
status: 'draft',
});
}, [agreementId, subscriptionId]);
@@ -74,8 +74,9 @@ export const AgreementViewer: React.FC<AgreementViewerProps> = ({
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-4xl">
<div className="bg-white rounded-lg shadow-lg p-8">
<p className="text-sm text-gray-500 mb-2">{SOLACENET_VENDOR_NAME}</p>
<h2 className="text-3xl font-bold mb-6 text-gray-800">
IRU Participation Agreement
{SOLACENET_PRODUCT_NAME} participation agreement
</h2>
{agreement?.status && (

View File

@@ -1,8 +1,8 @@
// Checkout Flow Component
// Subscription and payment flow for IRU
// Checkout — SolaceNet (IRU) subscription flow
import React, { useState } from 'react';
import { apiClient } from '@/services/api/client';
import { SOLACENET_PRODUCT_NAME, SOLACENET_VENDOR_NAME } from '@/constants/marketplace';
interface CheckoutFlowProps {
subscriptionId?: string;
@@ -86,7 +86,8 @@ export const CheckoutFlow: React.FC<CheckoutFlowProps> = ({
<div className="bg-gray-50 p-4 rounded">
<p className="text-gray-600">Offering ID: {offeringId}</p>
<p className="text-gray-600 mt-2">
Please review the IRU Participation Agreement before proceeding.
Please review the {SOLACENET_PRODUCT_NAME} participation agreement (
{SOLACENET_VENDOR_NAME}) before proceeding.
</p>
</div>
<button

View File

@@ -1,9 +1,19 @@
// IRU Offerings Page
// Catalog view with filtering
// SolaceNet (IRU) catalog — Sankofa Marketplace; other products may be listed elsewhere
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { apiClient } from '@/services/api/client';
import {
BILLING_MODE_LABELS,
COMMERCIAL_MODEL_LABELS,
FULFILLMENT_MODE_LABELS,
OFFER_TYPE_LABELS,
SOLACENET_OFFER_METADATA,
SOLACENET_PRODUCT_NAME,
SOLACENET_VENDOR_NAME,
SOLACENET_IRU_LABEL,
SUPPORT_OWNER_LABELS,
} from '@/constants/marketplace';
interface MarketplaceOffering {
id: string;
@@ -100,7 +110,37 @@ export const IRUOfferings: React.FC = () => {
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-4 text-gray-800">IRU Offerings</h1>
<p className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-1">
{SOLACENET_VENDOR_NAME}
</p>
<h1 className="text-3xl font-bold mb-2 text-gray-800">
{SOLACENET_PRODUCT_NAME}{' '}
<span className="text-xl font-normal text-gray-600">({SOLACENET_IRU_LABEL})</span>
</h1>
<p className="text-gray-600 mb-4">
One product family on Sankofa Marketplace additional offerings from other vendors may be
available separately.
</p>
<div className="mb-4 flex flex-wrap gap-2">
<span className="rounded-full bg-blue-50 px-3 py-1 text-sm text-blue-700">
{OFFER_TYPE_LABELS[SOLACENET_OFFER_METADATA.offerType]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{COMMERCIAL_MODEL_LABELS[SOLACENET_OFFER_METADATA.commercialModel]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{FULFILLMENT_MODE_LABELS[SOLACENET_OFFER_METADATA.fulfillmentMode]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{SUPPORT_OWNER_LABELS[SOLACENET_OFFER_METADATA.supportOwner]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{BILLING_MODE_LABELS[SOLACENET_OFFER_METADATA.billingMode]}
</span>
</div>
<p className="text-sm text-gray-600">
IRU remains the commercial model for this partner offering line. It does not define a separate marketplace category.
</p>
{/* Filters */}
<div className="bg-white rounded-lg shadow p-4 mb-6">

View File

@@ -1,8 +1,14 @@
// Inquiry Form Component
// Form for submitting initial IRU inquiry
// Inquiry form for SolaceNet (IRU) — Solace Bank Group PLC on Sankofa Marketplace
import React, { useState } from 'react';
import { apiClient } from '@/services/api/client';
import {
COMMERCIAL_MODEL_LABELS,
FULFILLMENT_MODE_LABELS,
SOLACENET_OFFER_METADATA,
SOLACENET_PRODUCT_NAME,
SOLACENET_VENDOR_NAME,
} from '@/constants/marketplace';
interface InquiryFormProps {
offeringId: string;
@@ -79,12 +85,13 @@ export const InquiryForm: React.FC<InquiryFormProps> = ({
return (
<div className="text-center py-8">
<div className="text-green-600 text-5xl mb-4"></div>
<h3 className="text-2xl font-semibold mb-2 text-gray-800">Inquiry Submitted Successfully</h3>
<h3 className="text-2xl font-semibold mb-2 text-gray-800">Inquiry submitted</h3>
<p className="text-gray-600 mb-4">
You will receive an acknowledgment within 24 hours.
Thank you for your interest in {SOLACENET_PRODUCT_NAME}. {SOLACENET_VENDOR_NAME} will follow up
within 24 hours.
</p>
<p className="text-sm text-gray-500">
We'll review your inquiry and contact you with next steps.
We will review your inquiry and contact you with next steps.
</p>
</div>
);
@@ -92,6 +99,14 @@ export const InquiryForm: React.FC<InquiryFormProps> = ({
return (
<form onSubmit={handleSubmit} className="space-y-4">
<p className="text-sm text-gray-500">
This inquiry is for <strong>{SOLACENET_PRODUCT_NAME}</strong>, offered by {SOLACENET_VENDOR_NAME}{' '}
on Sankofa Marketplace.
</p>
<p className="text-sm text-gray-500">
Commercial model: <strong>{COMMERCIAL_MODEL_LABELS[SOLACENET_OFFER_METADATA.commercialModel]}</strong>.
Fulfillment: <strong>{FULFILLMENT_MODE_LABELS[SOLACENET_OFFER_METADATA.fulfillmentMode]}</strong>.
</p>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}

View File

@@ -4,6 +4,18 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { apiClient } from '@/services/api/client';
import {
BILLING_MODE_LABELS,
COMMERCIAL_MODEL_LABELS,
FULFILLMENT_MODE_LABELS,
MARKETPLACE_NAME,
OFFER_TYPE_LABELS,
SOLACENET_PRODUCT_NAME,
SOLACENET_OFFER_METADATA,
SOLACENET_VENDOR_NAME,
SOLACENET_IRU_LABEL,
SUPPORT_OWNER_LABELS,
} from '@/constants/marketplace';
interface MarketplaceOffering {
id: string;
@@ -84,22 +96,65 @@ export const MarketplaceHome: React.FC = () => {
{/* Hero Section */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-700 text-white py-20">
<div className="container mx-auto px-4">
<h1 className="text-4xl md:text-5xl font-bold mb-4">
Sankofa Phoenix Marketplace
</h1>
<p className="text-xl md:text-2xl mb-8 text-blue-100">
Digital Bank of International Settlements - IRU Offerings
<h1 className="text-4xl md:text-5xl font-bold mb-4">{MARKETPLACE_NAME}</h1>
<p className="text-xl md:text-2xl mb-4 text-blue-100">
Multiple sovereign-grade offerings one trusted venue
</p>
<p className="text-lg text-blue-100 max-w-3xl">
Discover and subscribe to Irrevocable Right of Use (IRU) offerings for financial
infrastructure and SaaS services. Designed for Central Banks, Settlement Banks,
Commercial Banks, DFIs, and Special Entities.
<p className="text-lg text-blue-50 max-w-3xl mb-6">
Explore products from leading vendors. Below you can browse{' '}
<span className="font-semibold text-white">{SOLACENET_PRODUCT_NAME}</span> (
{SOLACENET_IRU_LABEL}) from <span className="font-semibold text-white">{SOLACENET_VENDOR_NAME}</span>
a partner program offered through Sankofa Marketplace for Central Banks, Settlement Banks, Commercial Banks, DFIs, and
Special Entities.
</p>
<div className="flex flex-wrap gap-2">
<span className="rounded-full bg-white/15 px-3 py-1 text-sm">
{OFFER_TYPE_LABELS[SOLACENET_OFFER_METADATA.offerType]}
</span>
<span className="rounded-full bg-white/15 px-3 py-1 text-sm">
{COMMERCIAL_MODEL_LABELS[SOLACENET_OFFER_METADATA.commercialModel]}
</span>
<span className="rounded-full bg-white/15 px-3 py-1 text-sm">
{FULFILLMENT_MODE_LABELS[SOLACENET_OFFER_METADATA.fulfillmentMode]}
</span>
<span className="rounded-full bg-white/15 px-3 py-1 text-sm">
{SUPPORT_OWNER_LABELS[SOLACENET_OFFER_METADATA.supportOwner]}
</span>
<span className="rounded-full bg-white/15 px-3 py-1 text-sm">
{BILLING_MODE_LABELS[SOLACENET_OFFER_METADATA.billingMode]}
</span>
</div>
</div>
</div>
{/* Offerings by Tier */}
{/* SolaceNet (IRU) offerings — additional marketplace products may appear here over time */}
<div className="container mx-auto px-4 py-12">
<div className="mb-10 pb-6 border-b border-gray-200">
<p className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-1">
{SOLACENET_VENDOR_NAME}
</p>
<h2 className="text-2xl md:text-3xl font-bold text-gray-900">
{SOLACENET_PRODUCT_NAME}{' '}
<span className="text-lg md:text-xl font-normal text-gray-600">({SOLACENET_IRU_LABEL})</span>
</h2>
<div className="mt-3 flex flex-wrap gap-2">
<span className="rounded-full bg-blue-50 px-3 py-1 text-sm text-blue-700">
{OFFER_TYPE_LABELS[SOLACENET_OFFER_METADATA.offerType]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{COMMERCIAL_MODEL_LABELS[SOLACENET_OFFER_METADATA.commercialModel]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{FULFILLMENT_MODE_LABELS[SOLACENET_OFFER_METADATA.fulfillmentMode]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{BILLING_MODE_LABELS[SOLACENET_OFFER_METADATA.billingMode]}
</span>
</div>
<p className="mt-3 text-sm text-gray-600">
IRU is the commercial model for this partner offering line. It is not a separate marketplace type.
</p>
</div>
{Object.entries(offeringsByTier).map(([tier, tierOfferings]) => (
<div key={tier} className="mb-12">
<h2 className="text-3xl font-bold mb-6 text-gray-800">
@@ -148,7 +203,12 @@ export const MarketplaceHome: React.FC = () => {
{/* Features Section */}
<div className="bg-white py-12">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 text-center text-gray-800">Why Choose DBIS IRU?</h2>
<h2 className="text-3xl font-bold mb-8 text-center text-gray-800">
Why choose {SOLACENET_PRODUCT_NAME}?
</h2>
<p className="text-center text-gray-600 max-w-2xl mx-auto -mt-4 mb-8">
Offered by {SOLACENET_VENDOR_NAME} on {MARKETPLACE_NAME}.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center">
<div className="text-4xl mb-4">🏛</div>

View File

@@ -1,10 +1,20 @@
// Offering Detail Page
// Detailed view of an IRU offering with specs and inquiry form
// Offering detail — SolaceNet (IRU) offering from Solace Bank Group PLC on Sankofa Marketplace
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { apiClient } from '@/services/api/client';
import { InquiryForm } from './InquiryForm';
import {
BILLING_MODE_LABELS,
COMMERCIAL_MODEL_LABELS,
FULFILLMENT_MODE_LABELS,
OFFER_TYPE_LABELS,
SOLACENET_OFFER_METADATA,
SOLACENET_PRODUCT_NAME,
SOLACENET_VENDOR_NAME,
SOLACENET_IRU_LABEL,
SUPPORT_OWNER_LABELS,
} from '@/constants/marketplace';
interface MarketplaceOffering {
id: string;
@@ -114,8 +124,25 @@ export const OfferingDetail: React.FC = () => {
>
Back to Marketplace
</button>
<p className="text-sm text-gray-500 mb-1">
{SOLACENET_VENDOR_NAME} · {SOLACENET_PRODUCT_NAME} ({SOLACENET_IRU_LABEL})
</p>
<h1 className="text-4xl font-bold mb-2 text-gray-800">{offering.name}</h1>
<div className="flex items-center gap-4">
<div className="mb-3 flex flex-wrap gap-2">
<span className="rounded-full bg-blue-50 px-3 py-1 text-sm text-blue-700">
{OFFER_TYPE_LABELS[SOLACENET_OFFER_METADATA.offerType]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{COMMERCIAL_MODEL_LABELS[SOLACENET_OFFER_METADATA.commercialModel]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{FULFILLMENT_MODE_LABELS[SOLACENET_OFFER_METADATA.fulfillmentMode]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{SUPPORT_OWNER_LABELS[SOLACENET_OFFER_METADATA.supportOwner]}
</span>
</div>
<div className="flex flex-wrap items-center gap-4">
<span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-semibold">
Tier {offering.capacityTier}: {TIER_NAMES[offering.capacityTier]}
</span>
@@ -214,11 +241,25 @@ export const OfferingDetail: React.FC = () => {
{/* Pricing Card */}
<div className="bg-white rounded-lg shadow p-6 sticky top-4">
<h2 className="text-2xl font-semibold mb-4 text-gray-800">Pricing</h2>
<p className="mb-4 text-sm text-gray-600">
This is a request-based partner program. Commercial terms are handled through qualification and agreement,
not as a separate marketplace type.
</p>
<div className="mb-4 flex flex-wrap gap-2">
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{BILLING_MODE_LABELS[SOLACENET_OFFER_METADATA.billingMode]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{FULFILLMENT_MODE_LABELS[SOLACENET_OFFER_METADATA.fulfillmentMode]}
</span>
</div>
{pricing ? (
<div className="space-y-4">
{pricing.basePrice && (
<div>
<div className="text-sm text-gray-500 mb-1">IRU Grant Fee</div>
<div className="text-sm text-gray-500 mb-1">
{SOLACENET_PRODUCT_NAME} grant fee ({SOLACENET_IRU_LABEL})
</div>
<div className="text-2xl font-bold text-blue-600">
{pricing.currency} {pricing.basePrice.toLocaleString()}
</div>
@@ -296,7 +337,9 @@ export const OfferingDetail: React.FC = () => {
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-semibold text-gray-800">Request Information</h2>
<h2 className="text-2xl font-semibold text-gray-800">
Request information {SOLACENET_PRODUCT_NAME}
</h2>
<button
onClick={() => setShowInquiryForm(false)}
className="text-gray-500 hover:text-gray-700"

View File

@@ -1,8 +1,8 @@
// IRU Management Page
// IRU lifecycle management
// SolaceNet (IRU) lifecycle — vendor: Solace Bank Group PLC
import React, { useState, useEffect } from 'react';
import { apiClient } from '@/services/api/client';
import { SOLACENET_PRODUCT_NAME, SOLACENET_VENDOR_NAME } from '@/constants/marketplace';
interface IRUManagementData {
subscriptionId: string;
@@ -30,7 +30,7 @@ export const IRUManagement: React.FC = () => {
setManagement(data.data);
}
} catch (err: any) {
setError(err.message || 'Failed to load IRU management data');
setError(err.message || `Failed to load ${SOLACENET_PRODUCT_NAME} management data`);
} finally {
setLoading(false);
}
@@ -44,7 +44,7 @@ export const IRUManagement: React.FC = () => {
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading IRU management...</p>
<p className="mt-4 text-gray-600">Loading {SOLACENET_PRODUCT_NAME}</p>
</div>
</div>
);
@@ -64,7 +64,10 @@ export const IRUManagement: React.FC = () => {
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold mb-8 text-gray-800">IRU Management</h1>
<p className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-1">
{SOLACENET_VENDOR_NAME}
</p>
<h1 className="text-3xl font-bold mb-8 text-gray-800">{SOLACENET_PRODUCT_NAME} management</h1>
{management.length > 0 ? (
<div className="space-y-6">
@@ -142,7 +145,7 @@ export const IRUManagement: React.FC = () => {
</div>
) : (
<div className="bg-white rounded-lg shadow p-8 text-center">
<p className="text-gray-600 text-lg">No IRU subscriptions found.</p>
<p className="text-gray-600 text-lg">No {SOLACENET_PRODUCT_NAME} subscriptions found.</p>
</div>
)}
</div>

View File

@@ -1,9 +1,16 @@
// Participant Dashboard
// Main dashboard for IRU participants
// Participant dashboard — SolaceNet (IRU) subscription via Sankofa Marketplace
import React, { useState, useEffect } from 'react';
import { apiClient } from '@/services/api/client';
import { Link } from 'react-router-dom';
import {
COMMERCIAL_MODEL_LABELS,
FULFILLMENT_MODE_LABELS,
OFFER_TYPE_LABELS,
SOLACENET_OFFER_METADATA,
SOLACENET_PRODUCT_NAME,
SOLACENET_VENDOR_NAME,
} from '@/constants/marketplace';
interface DashboardData {
subscription: any;
@@ -68,7 +75,21 @@ export const ParticipantDashboard: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Subscription Card */}
<div className="lg:col-span-2 bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-semibold mb-4 text-gray-800">IRU Subscription</h2>
<h2 className="text-2xl font-semibold mb-4 text-gray-800">
{SOLACENET_PRODUCT_NAME} subscription
</h2>
<p className="text-sm text-gray-500 mb-4">{SOLACENET_VENDOR_NAME}</p>
<div className="mb-4 flex flex-wrap gap-2">
<span className="rounded-full bg-blue-50 px-3 py-1 text-sm text-blue-700">
{OFFER_TYPE_LABELS[SOLACENET_OFFER_METADATA.offerType]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{COMMERCIAL_MODEL_LABELS[SOLACENET_OFFER_METADATA.commercialModel]}
</span>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700">
{FULFILLMENT_MODE_LABELS[SOLACENET_OFFER_METADATA.fulfillmentMode]}
</span>
</div>
<div className="space-y-3">
<div>
<div className="text-sm text-gray-500">Offering</div>
@@ -99,7 +120,7 @@ export const ParticipantDashboard: React.FC = () => {
to="/portal/iru-management"
className="mt-4 inline-block text-blue-600 hover:text-blue-700"
>
Manage IRU
Manage {SOLACENET_PRODUCT_NAME}
</Link>
</div>
@@ -153,7 +174,8 @@ export const ParticipantDashboard: React.FC = () => {
<div className="bg-white rounded-lg shadow p-8 text-center">
<h2 className="text-2xl font-semibold mb-4 text-gray-800">No Active Subscription</h2>
<p className="text-gray-600 mb-6">
You don't have an active IRU subscription. Browse the marketplace to get started.
You don't have an active {SOLACENET_PRODUCT_NAME} subscription. Browse the marketplace to
explore offerings from {SOLACENET_VENDOR_NAME} and others.
</p>
<Link
to="/marketplace"

View File

@@ -0,0 +1,20 @@
/**
* HTTP integration tests: transpile-only (no ts-jest semantic diagnostics) so the
* suite does not pull full strict checking of the SolaceNet → Prisma dependency graph.
* Unit tests under src/__tests__/unit/core/gateway use the main jest.config.js.
*/
const base = require('./jest.config.js');
module.exports = {
...base,
setupFiles: ['<rootDir>/src/__tests__/gateway-http-env-setup.ts'],
testMatch: ['**/gateway-rails-http.integration.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
diagnostics: false,
},
],
},
};

View File

@@ -3,7 +3,9 @@
"name": "DBIS Gateway Microservices",
"version": "1.0.0",
"category": "integration",
"description": "Regulated-grade integration fabric for financial rails",
"protocolMaintainer": "SolaceNet",
"governanceDoc": "docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md",
"description": "SolaceNet-maintained gateway rail adapters and HTTP surface for financial messaging rails. Adapter contracts and governance are under SolaceNet (see governanceDoc). Most listed rails are integration scaffolds until production connectors (SWIFTNet, DTCC APIs, etc.) are live — see dbis_core/docs/solacenet/PROTOCOL_GAPS_CHECKLIST.md. KTT Legacy is evidence-oriented (validate/receive); symmetric send is not a production bank rail in the current adapter.",
"capabilities": [
"gateway-microservices",
"gateway-edge",

1400
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,9 @@
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:iru-marketplace": "jest src/__tests__/iru/marketplace.service.test.ts src/__tests__/integration/api/iru-marketplace-http.integration.test.ts --forceExit --no-cache",
"test:gateway": "jest src/__tests__/unit/core/gateway --forceExit --no-cache && jest --config jest.gateway-http.config.js --forceExit --no-cache",
"seed:gateway-provider": "ts-node -r tsconfig-paths/register scripts/seed-solacenet-gateway-provider.ts",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
@@ -49,6 +52,7 @@
"date-fns": "^3.0.6",
"decimal.js": "^10.4.3",
"dotenv": "^16.3.1",
"ethers": "^6.13.5",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",

View File

@@ -9,7 +9,7 @@ async function main() {
name: 'DBIS Gateway Microservices',
version: '1.0.0',
description:
'Regulated-grade integration fabric for SWIFT, DTC/DTCC, and extensible financial rails',
'SolaceNet-maintained gateway rail adapters and HTTP surface; many connectors are scaffolds until production APIs (see marketplace copy + PROTOCOL_GAPS_CHECKLIST). Governance: docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md',
defaultState: 'enabled' as any,
dependencies: ['ledger', 'iso20022', 'reconciliation'].filter(Boolean),
});

View File

@@ -0,0 +1,63 @@
/**
* Upsert solacenet_provider_connector for the gateway rail plane (PG-SN-002).
* Run: npx ts-node -r tsconfig-paths/register scripts/seed-solacenet-gateway-provider.ts
* Requires DATABASE_URL and prisma client.
*/
import { v4 as uuidv4 } from 'uuid';
import type { Prisma } from '@prisma/client';
import prisma from '@/shared/database/prisma';
import { logger } from '@/infrastructure/monitoring/logger';
const CONNECTOR_ID = 'dbis-gateway-rail-plane';
async function main() {
const metadata: Prisma.InputJsonValue = {
capabilityId: 'gateway-microservices',
governanceDoc: 'docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md',
note: 'Logical provider for SolaceNet-maintained gateway rail adapters; bindings use solacenet_capability_binding.',
};
const existing = await prisma.solacenet_provider_connector.findUnique({
where: { connectorId: CONNECTOR_ID },
});
if (existing) {
await prisma.solacenet_provider_connector.update({
where: { connectorId: CONNECTOR_ID },
data: {
name: 'DBIS Gateway Rail Plane (SolaceNet)',
providerType: 'gateway-rail',
status: 'active',
metadata,
},
});
logger.info('Updated solacenet_provider_connector', { connectorId: CONNECTOR_ID });
return;
}
await prisma.solacenet_provider_connector.create({
data: {
id: uuidv4(),
connectorId: CONNECTOR_ID,
name: 'DBIS Gateway Rail Plane (SolaceNet)',
providerType: 'gateway-rail',
status: 'active',
metadata,
},
});
logger.info('Created solacenet_provider_connector', { connectorId: CONNECTOR_ID });
}
if (require.main === module) {
main()
.catch((e) => {
logger.error('seed-solacenet-gateway-provider failed', {
error: e instanceof Error ? e.message : e,
});
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});
}

View File

@@ -0,0 +1,19 @@
-- Repair IruInquiry.offeringId when it incorrectly stores IruOffering.offeringId (business code)
-- instead of IruOffering.id (UUID). FK: IruInquiry.offeringId -> IruOffering.id
--
-- Review affected rows first:
-- SELECT i."inquiryId", i."offeringId" AS broken_fk, o.id AS correct_uuid, o."offeringId" AS business_id
-- FROM "IruInquiry" i
-- INNER JOIN "IruOffering" o ON o."offeringId" = i."offeringId"
-- WHERE i."offeringId" <> o.id;
--
-- Apply (transaction recommended):
-- BEGIN;
-- \i scripts/sql/fix-iru-inquiry-offering-fk.sql
-- COMMIT;
UPDATE "IruInquiry" AS i
SET "offeringId" = o.id
FROM "IruOffering" AS o
WHERE o."offeringId" = i."offeringId"
AND i."offeringId" IS DISTINCT FROM o.id;

View File

@@ -37,6 +37,17 @@ export interface IRUSubscription {
activationDate?: Date;
}
/** Public GET /inquiries/:id (no organization, contact, qualification, risk, or notes) */
export interface IRUPublicInquiryStatus {
inquiryId: string;
status: string;
offering: { name: string; capacityTier: number };
submittedAt: string;
acknowledgedAt: string | null;
reviewedAt: string | null;
completedAt: string | null;
}
export class IRUClient {
private config: IRUClientConfig;
private baseUrl: string;
@@ -98,10 +109,10 @@ export class IRUClient {
}
/**
* Get inquiry status
* Get public inquiry status (workflow + offering summary only).
*/
async getInquiryStatus(inquiryId: string): Promise<any> {
const response = await this.request<{ success: boolean; data: any }>(
async getInquiryStatus(inquiryId: string): Promise<IRUPublicInquiryStatus> {
const response = await this.request<{ success: boolean; data: IRUPublicInquiryStatus }>(
`/api/v1/iru/marketplace/inquiries/${inquiryId}`
);

View File

@@ -0,0 +1,2 @@
/** Loaded before gateway HTTP integration tests (imports run before test body). */
process.env.GATEWAY_RAIL_RATE_LIMIT_IN_TEST = '1';

View File

@@ -0,0 +1,72 @@
/**
* Gateway rail HTTP surface — list, health, validate, enforcement hook (supertest).
* GATEWAY_RAIL_RATE_LIMIT_IN_TEST: see gateway-http-env-setup.ts (jest.gateway-http.config.js).
*/
import request from 'supertest';
import { DbisError, ErrorCode } from '@/shared/types';
import * as gatewayEnforcement from '@/core/gateway/rails/gateway-rails-enforcement';
import { createGatewayHttpTestApp } from '@/__tests__/utils/gateway-http-test-app';
const app = createGatewayHttpTestApp();
const BASE = '/api/v1/gateway';
describe('Gateway rails HTTP', () => {
const prevEnforce = process.env.SOLACENET_GATEWAY_RAILS_ENFORCE;
afterEach(() => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = prevEnforce;
jest.restoreAllMocks();
});
it('GET /rails returns maintainer, adapters, enforcement flag', async () => {
const res = await request(app).get(`${BASE}/rails`);
expect(res.status).toBe(200);
expect(res.body.maintainer).toBe('SolaceNet');
expect(Array.isArray(res.body.adapters)).toBe(true);
expect(res.body.adapters).toContain('dbis.adapter.ktt-evidence');
expect(typeof res.body.enforcementEnabled).toBe('boolean');
});
it('GET /rails/:adapterId/health returns 404 for unknown adapter', async () => {
const res = await request(app).get(`${BASE}/rails/not-a-real-adapter/health`);
expect(res.status).toBe(404);
expect(res.body.error).toBe('UNKNOWN_ADAPTER');
});
it('GET /rails/dbis.adapter.ktt-evidence/health returns 200', async () => {
const res = await request(app).get(`${BASE}/rails/dbis.adapter.ktt-evidence/health`);
expect(res.status).toBe(200);
expect(res.body.adapterId).toBe('dbis.adapter.ktt-evidence');
});
it('POST /rails/dbis.adapter.ktt-evidence/validate returns validation shape', async () => {
const res = await request(app)
.post(`${BASE}/rails/dbis.adapter.ktt-evidence/validate`)
.send({ tenantId: 't1', canonicalInstruction: {} });
expect(res.status).toBe(200);
expect(res.body.adapterId).toBe('dbis.adapter.ktt-evidence');
expect(res.body).toHaveProperty('ok');
});
it('returns 403 when enforcement denies capability', async () => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = '1';
jest.spyOn(gatewayEnforcement, 'maybeRequireGatewayMicroservices').mockRejectedValue(
new DbisError(ErrorCode.FORBIDDEN, 'Capability gateway-microservices is not available'),
);
const res = await request(app).get(`${BASE}/rails/dbis.adapter.ktt-evidence/health`);
expect(res.status).toBe(403);
expect(res.body.code).toBe(ErrorCode.FORBIDDEN);
});
it('POST /instructions returns 202 and correlation fields', async () => {
const res = await request(app)
.post(`${BASE}/instructions`)
.send({ tenantId: 't1', amount: '1', currency: 'USD' });
expect(res.status).toBe(202);
expect(res.body.status).toBe('RECEIVED');
expect(res.body.txnId).toBeDefined();
expect(res.body.correlationId).toBeDefined();
});
});

View File

@@ -0,0 +1,110 @@
/**
* IRU marketplace HTTP wiring: validation, pricing JSON, public inquiry shape (service spies; no DB).
*/
import request from 'supertest';
import { createIruMarketplaceHttpTestApp } from '@/__tests__/utils/iru-marketplace-http-test-app';
import { marketplaceService } from '@/core/iru/marketplace.service';
process.env.IRU_MARKETPLACE_RATE_LIMIT_IN_TEST = '1';
const app = createIruMarketplaceHttpTestApp();
const BASE = '/api/v1/iru/marketplace';
const validInquiryBody = {
offeringId: 'IRU-OFF-TEST',
organizationName: 'Test Reserve',
institutionalType: 'CentralBank',
jurisdiction: 'US',
contactEmail: 'ops@example.org',
contactName: 'Test User',
};
describe('IRU marketplace HTTP', () => {
let submitSpy: jest.SpyInstance;
let publicStatusSpy: jest.SpyInstance;
let pricingSpy: jest.SpyInstance;
beforeAll(() => {
submitSpy = jest.spyOn(marketplaceService, 'submitInquiry').mockResolvedValue({
inquiryId: 'INQ-MOCK',
status: 'submitted',
message: 'ok',
});
publicStatusSpy = jest.spyOn(marketplaceService, 'getInquiryStatusPublic').mockResolvedValue({
inquiryId: 'INQ-PUB',
status: 'submitted',
offering: { name: 'Tier 1', capacityTier: 1 },
submittedAt: new Date('2026-01-01T00:00:00.000Z'),
acknowledgedAt: null,
reviewedAt: null,
completedAt: null,
});
pricingSpy = jest.spyOn(marketplaceService, 'calculatePricing').mockResolvedValue({
offeringId: 'IRU-OFF-TEST',
capacityTier: 1,
basePrice: 100,
currency: 'USD',
pricingModel: 'Fixed',
});
});
afterAll(() => {
submitSpy.mockRestore();
publicStatusSpy.mockRestore();
pricingSpy.mockRestore();
});
beforeEach(() => {
submitSpy.mockClear();
publicStatusSpy.mockClear();
pricingSpy.mockClear();
});
it('POST /inquiries returns 201 and strips validation before service', async () => {
const res = await request(app).post(`${BASE}/inquiries`).send(validInquiryBody);
expect(res.status).toBe(201);
expect(submitSpy).toHaveBeenCalledTimes(1);
expect(submitSpy.mock.calls[0][0]).toMatchObject({
offeringId: 'IRU-OFF-TEST',
contactEmail: 'ops@example.org',
});
});
it('POST /inquiries returns 400 when jurisdiction is not 2 letters', async () => {
const res = await request(app)
.post(`${BASE}/inquiries`)
.send({ ...validInquiryBody, jurisdiction: 'USA' });
expect(res.status).toBe(400);
expect(submitSpy).not.toHaveBeenCalled();
});
it('GET /offerings/:id/pricing returns 400 for invalid usageProfile JSON', async () => {
const res = await request(app)
.get(`${BASE}/offerings/IRU-OFF-TEST/pricing`)
.query({ usageProfile: 'not-json{' });
expect(res.status).toBe(400);
expect(res.body.error?.code).toBe('INVALID_JSON');
expect(pricingSpy).not.toHaveBeenCalled();
});
it('GET /offerings/:id/pricing hits calculatePricing when JSON is valid', async () => {
const res = await request(app)
.get(`${BASE}/offerings/IRU-OFF-TEST/pricing`)
.query({ usageProfile: JSON.stringify({ tier: 1 }) });
expect(res.status).toBe(200);
expect(pricingSpy).toHaveBeenCalledWith('IRU-OFF-TEST', { tier: 1 });
});
it('GET /inquiries/:inquiryId returns public shape via getInquiryStatusPublic', async () => {
const res = await request(app).get(`${BASE}/inquiries/INQ-PUB`);
expect(res.status).toBe(200);
expect(publicStatusSpy).toHaveBeenCalledWith('INQ-PUB');
expect(res.body.data).toMatchObject({
inquiryId: 'INQ-PUB',
offering: { name: 'Tier 1', capacityTier: 1 },
});
expect(res.body.data).not.toHaveProperty('organizationName');
expect(res.body.data).not.toHaveProperty('notes');
});
});

View File

@@ -70,10 +70,10 @@ describe('MarketplaceService', () => {
(prisma.iruOffering.findUnique as jest.Mock).mockResolvedValue(mockOffering);
(prisma.iruInquiry.findFirst as jest.Mock).mockResolvedValue(null);
(prisma.iruInquiry.create as jest.Mock).mockResolvedValue({
inquiryId: 'INQ-12345678',
(prisma.iruInquiry.create as jest.Mock).mockImplementation(async ({ data }: { data: Record<string, unknown> }) => ({
...data,
status: 'submitted',
});
}));
const result = await marketplaceService.submitInquiry({
offeringId: 'IRU-OFF-001',
@@ -84,8 +84,16 @@ describe('MarketplaceService', () => {
contactName: 'Test User',
});
expect(result.inquiryId).toBe('INQ-12345678');
expect(result.inquiryId).toMatch(/^INQ-[0-9A-F]{32}$/);
expect(result.status).toBe('submitted');
expect(prisma.iruInquiry.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
offeringId: '1',
inquiryId: expect.stringMatching(/^INQ-[0-9A-F]{32}$/),
}),
})
);
});
it('should reject inquiry for inactive offering', async () => {
@@ -109,4 +117,35 @@ describe('MarketplaceService', () => {
).rejects.toThrow('is not active');
});
});
describe('getInquiryStatusPublic', () => {
it('omits PII and internal fields', async () => {
const submittedAt = new Date('2026-01-01T00:00:00.000Z');
(prisma.iruInquiry.findUnique as jest.Mock).mockResolvedValue({
inquiryId: 'INQ-PUB',
status: 'submitted',
organizationName: 'Secret',
contactEmail: 'x@y.com',
notes: 'internal',
qualificationResult: {},
riskScore: 1,
submittedAt,
acknowledgedAt: null,
reviewedAt: null,
completedAt: null,
offering: { name: 'T1', capacityTier: 1 },
});
const row = await marketplaceService.getInquiryStatusPublic('INQ-PUB');
expect(row).toEqual({
inquiryId: 'INQ-PUB',
status: 'submitted',
offering: { name: 'T1', capacityTier: 1 },
submittedAt,
acknowledgedAt: null,
reviewedAt: null,
completedAt: null,
});
});
});
});

View File

@@ -15,8 +15,6 @@ jest.setTimeout(10000);
// Cleanup after all tests
afterAll(async () => {
// Close any open connections
const prisma = new PrismaClient();
await prisma.$disconnect();
await prisma.$disconnect().catch(() => undefined);
});

View File

@@ -0,0 +1,63 @@
jest.mock('@/core/gateway/adapters/thirdweb/thirdweb-adapter', () => ({
ThirdwebAdapter: class ThirdwebAdapter {
async initialize() {}
async health() {
return { status: 'UP' as const };
}
async capabilities() {
return [];
}
async validate() {
return { ok: true };
}
async send() {
return { status: 'SENT' as const, railMessageId: 'mock' };
}
async receive() {
return { status: 'ACK' as const, railMessageId: 'mock' };
}
mapStatus(rail: { code: string; description?: string; source?: string }) {
return { status: 'IN_PROGRESS', railStatus: rail };
}
finalityHints() {
return {};
}
errorMap(railError: { code: string; message?: string }) {
return { errorCode: railError.code || 'UNKNOWN', retryClass: 'MANUAL_REVIEW' as const };
}
},
}));
import {
createGatewayRailAdapter,
GATEWAY_RAIL_ADAPTER_IDS,
isGatewayRailAdapterId,
listGatewayRailAdapterIds,
} from '@/core/gateway/adapters/gateway-adapter-registry';
describe('gateway-adapter-registry', () => {
it('exports a stable ordered list of rail adapter IDs', () => {
const ids = listGatewayRailAdapterIds();
expect(ids.length).toBe(GATEWAY_RAIL_ADAPTER_IDS.length);
expect(ids).toEqual([...GATEWAY_RAIL_ADAPTER_IDS]);
});
it('recognizes known adapter IDs', () => {
expect(isGatewayRailAdapterId('dbis.adapter.swift-fin')).toBe(true);
expect(isGatewayRailAdapterId('dbis.adapter.ktt-evidence')).toBe(true);
expect(isGatewayRailAdapterId('unknown')).toBe(false);
});
it('creates adapter instances for each registered id', () => {
for (const id of listGatewayRailAdapterIds()) {
const adapter = createGatewayRailAdapter(id);
expect(adapter).toBeDefined();
expect(typeof adapter!.validate).toBe('function');
expect(typeof adapter!.health).toBe('function');
}
});
it('returns undefined for unknown id', () => {
expect(createGatewayRailAdapter('not-a-rail')).toBeUndefined();
});
});

View File

@@ -0,0 +1,119 @@
jest.mock('@/shared/solacenet/sdk', () => ({
requireCapability: jest.fn().mockResolvedValue(undefined),
checkCapability: jest.fn(),
getCapabilityState: jest.fn(),
}));
import type { Request } from 'express';
import {
gatewayRailsEnforcementEnabled,
maybeRequireGatewayMicroservices,
requireGatewayMicroservicesForWorker,
resolveGatewayTenantId,
} from '@/core/gateway/rails/gateway-rails-enforcement';
import { requireCapability } from '@/shared/solacenet/sdk';
import { logger } from '@/infrastructure/monitoring/logger';
import { DbisError, ErrorCode } from '@/shared/types';
const mockRequireCapability = requireCapability as jest.MockedFunction<typeof requireCapability>;
describe('gateway-rails-enforcement', () => {
const origEnforce = process.env.SOLACENET_GATEWAY_RAILS_ENFORCE;
const origTenant = process.env.SOLACENET_DEFAULT_TENANT_ID;
let logSpy: jest.SpyInstance;
beforeAll(() => {
logSpy = jest.spyOn(logger, 'info').mockImplementation(() => logger as never);
});
afterAll(() => {
logSpy.mockRestore();
});
afterEach(() => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = origEnforce;
process.env.SOLACENET_DEFAULT_TENANT_ID = origTenant;
});
it('is disabled when env unset or not 1/true', () => {
delete process.env.SOLACENET_GATEWAY_RAILS_ENFORCE;
expect(gatewayRailsEnforcementEnabled()).toBe(false);
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = '0';
expect(gatewayRailsEnforcementEnabled()).toBe(false);
});
it('is enabled for 1 or true', () => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = '1';
expect(gatewayRailsEnforcementEnabled()).toBe(true);
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = 'true';
expect(gatewayRailsEnforcementEnabled()).toBe(true);
});
it('resolves tenant from body, header, env, then system', () => {
const req = { body: { tenantId: 't-body' }, headers: {} } as Request;
expect(resolveGatewayTenantId(req)).toBe('t-body');
const req2 = { body: {}, headers: { 'x-tenant-id': 't-header' } } as unknown as Request;
expect(resolveGatewayTenantId(req2)).toBe('t-header');
delete process.env.SOLACENET_DEFAULT_TENANT_ID;
const req3 = { body: {}, headers: {} } as Request;
expect(resolveGatewayTenantId(req3)).toBe('system');
process.env.SOLACENET_DEFAULT_TENANT_ID = 't-env';
expect(resolveGatewayTenantId(req3)).toBe('t-env');
});
it('maybeRequireGatewayMicroservices calls requireCapability when enforcement on', async () => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = '1';
mockRequireCapability.mockClear();
const req = { body: { tenantId: 'acme' }, headers: {} } as Request;
await maybeRequireGatewayMicroservices(req);
expect(mockRequireCapability).toHaveBeenCalledWith(
'gateway-microservices',
expect.objectContaining({ tenantId: 'acme', capabilityId: 'gateway-microservices' }),
);
});
it('maybeRequireGatewayMicroservices skips when enforcement off', async () => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = '0';
mockRequireCapability.mockClear();
await maybeRequireGatewayMicroservices({ body: {}, headers: {} } as Request);
expect(mockRequireCapability).not.toHaveBeenCalled();
});
it('maybeRequireGatewayMicroservices logs deny and rethrows on DbisError', async () => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = '1';
mockRequireCapability.mockRejectedValueOnce(new DbisError(ErrorCode.FORBIDDEN, 'denied'));
const req = {
body: { tenantId: 'acme' },
headers: {},
params: { adapterId: 'dbis.adapter.ktt-evidence' },
path: '/rails/dbis.adapter.ktt-evidence/health',
method: 'GET',
} as unknown as Request;
await expect(maybeRequireGatewayMicroservices(req)).rejects.toThrow(DbisError);
expect(logSpy).toHaveBeenCalledWith(
'Gateway rails enforcement audit',
expect.objectContaining({
decision: 'deny',
tenantId: 'acme',
adapterId: 'dbis.adapter.ktt-evidence',
}),
);
});
it('requireGatewayMicroservicesForWorker allows when capability ok', async () => {
process.env.SOLACENET_GATEWAY_RAILS_ENFORCE = '1';
mockRequireCapability.mockResolvedValueOnce(undefined);
await requireGatewayMicroservicesForWorker({
tenantId: 't1',
ingressKind: 'mq',
detail: 'queue:gateway.in',
});
expect(mockRequireCapability).toHaveBeenCalledWith(
'gateway-microservices',
expect.objectContaining({ tenantId: 't1', channel: 'mq' }),
);
});
});

View File

@@ -0,0 +1,15 @@
/**
* Minimal Express app with gateway routes only (for HTTP integration tests).
*/
import express, { Express } from 'express';
import gatewayRoutes from '@/core/gateway/routes/gateway.routes';
import { errorHandler } from '@/integration/api-gateway/middleware/error.middleware';
export function createGatewayHttpTestApp(): Express {
const app = express();
app.use(express.json());
app.use('/api/v1/gateway', gatewayRoutes);
app.use(errorHandler);
return app;
}

View File

@@ -0,0 +1,15 @@
/**
* Express app with IRU marketplace routes only (public + admin mount) for HTTP tests.
*/
import express, { Express } from 'express';
import iruMarketplacePublicRoutes from '@/integration/api-gateway/routes/iru-marketplace-public.routes';
import { errorHandler } from '@/integration/api-gateway/middleware/error.middleware';
export function createIruMarketplaceHttpTestApp(): Express {
const app = express();
app.use(express.json());
app.use('/api/v1/iru/marketplace', iruMarketplacePublicRoutes);
app.use(errorHandler);
return app;
}

View File

@@ -4,6 +4,7 @@
import prisma from '@/shared/database/prisma';
import { logger } from '@/infrastructure/monitoring/logger';
import Decimal from 'decimal.js';
import { listGatewayRailAdapterIds } from '@/core/gateway/adapters/gateway-adapter-registry';
export interface GASMetrics {
atomicSettlementSuccessRate: number;
@@ -133,7 +134,13 @@ export class GASQPSService {
messageTypes.set(msg.messageType, count + 1);
});
// Legacy rails (placeholder - would need actual QPS integration)
// Legacy rails: core buckets + SolaceNet gateway adapter IDs (volumes still heuristic until QPS integration)
const solaceNetRails = listGatewayRailAdapterIds().map((adapterId) => ({
railType: adapterId,
enabled: true,
volume24h: 0,
errorRate: 0,
}));
const legacyRails = [
{
railType: 'SWIFT',
@@ -153,6 +160,7 @@ export class GASQPSService {
volume24h: 0,
errorRate: 0,
},
...solaceNetRails,
];
// Mapping profiles (placeholder)

View File

@@ -1 +1 @@
export * from './debank-portfolio.service.js';
export * from './debank-portfolio.service';

View File

@@ -1,7 +1,7 @@
export * from './bridge-capability-matrix.js';
export * from './tezos-dex-quote.service.js';
export * from './chain138-quote.service.js';
export * from './ccip-fee.service.js';
export * from './allowlist.config.js';
export * from './tezos-signer.types.js';
export * from './route-planner.service.js';
export * from './bridge-capability-matrix';
export * from './tezos-dex-quote.service';
export * from './chain138-quote.service';
export * from './ccip-fee.service';
export * from './allowlist.config';
export * from './tezos-signer.types';
export * from './route-planner.service';

View File

@@ -2,12 +2,18 @@
* Route Planner - Chain138 to Tezos USDtz
*/
import { validateTezosAddress } from '../../../shared/utils/tezos-address.js';
import { getCandidateBridgesForPlanning, CHAIN_138, CHAIN_ETHEREUM, CHAIN_TEZOS, CHAIN_ALL_MAINNET } from './bridge-capability-matrix.js';
import { getChain138SwapQuote } from './chain138-quote.service.js';
import { getCCIPFeeEstimate } from './ccip-fee.service.js';
import { getTezosDexQuote } from './tezos-dex-quote.service.js';
import { validateRouteAllowlist } from './allowlist.config.js';
import { validateTezosAddress } from '../../../shared/utils/tezos-address';
import {
getCandidateBridgesForPlanning,
CHAIN_138,
CHAIN_ETHEREUM,
CHAIN_TEZOS,
CHAIN_ALL_MAINNET,
} from './bridge-capability-matrix';
import { getChain138SwapQuote } from './chain138-quote.service';
import { getCCIPFeeEstimate } from './ccip-fee.service';
import { getTezosDexQuote } from './tezos-dex-quote.service';
import { validateRouteAllowlist } from './allowlist.config';
export type ChainLabel = 'CHAIN138' | 'ALL_MAINNET' | 'HUB_EVM' | 'TEZOS';
export type HopAction = 'SWAP' | 'BRIDGE' | 'MINT' | 'REDEEM' | 'TRANSFER';

View File

@@ -179,18 +179,20 @@ export class GsdsContractService {
* Auto-close contract
*/
async autoClose(derivativeId: string, reason: Record<string, unknown>): Promise<void> {
const derivative = await prisma.synthetic_derivatives.findUnique({
where: { derivativeId },
});
const existing = derivative?.contractTerms;
const base =
typeof existing === 'object' && existing !== null
? (existing as Record<string, unknown>)
: {};
await prisma.synthetic_derivatives.update({
where: { derivativeId },
data: {
status: 'auto_closed',
contractTerms: (() => {
const derivative = await prisma.synthetic_derivatives.findUnique({
where: { derivativeId },
});
const existing = derivative?.contractTerms;
const base = typeof existing === 'object' && existing !== null ? existing as Record<string, unknown> : {};
return { ...base, autoCloseReason: reason } as Prisma.InputJsonValue;
})(),
contractTerms: { ...base, autoCloseReason: reason } as Prisma.InputJsonValue,
},
});
}
@@ -240,4 +242,3 @@ export class GsdsContractService {
}
export const gsdsContractService = new GsdsContractService();

View File

@@ -0,0 +1,13 @@
# Gateway rail adapters
TypeScript implementations of **external rail and protocol** surfaces (SWIFT, DTC/DTCC, TT, KTT evidence, etc.).
**Governance:** These protocols are **maintained under SolaceNet**—capability registry, policy, audit, and the Go edge gateway. See:
- `dbis_core/docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md`
- `dbis_core/docs/solacenet/PROTOCOL_GAPS_CHECKLIST.md` (tracked gap IDs and close criteria)
- `dbis_core/SOLACENET_QUICK_REFERENCE.md`
New adapters should be registered with SolaceNet capabilities and documented with their trust model (full rail vs evidence-only vs northbound-only from a telecom boundary).
**Registry:** `gateway-adapter-registry.ts` lists canonical adapter IDs and factory functions (`createGatewayRailAdapter`, `listGatewayRailAdapterIds`). Add every new rail adapter there.

View File

@@ -0,0 +1,55 @@
import type { GatewayAdapter } from './sdk/adapter-interface';
import { DtcSettlementAdapter } from './dtc-settlement/dtc-settlement-adapter';
import { DtccFiccAdapter } from './dtcc/dtcc-ficc-adapter';
import { DtccNsccAdapter } from './dtcc/dtcc-nscc-adapter';
import { KttEvidenceAdapter } from './ktt-evidence/ktt-evidence-adapter';
import { SwiftFinAdapter } from './swift-fin/swift-fin-adapter';
import { SwiftGpiAdapter } from './swift-gpi/swift-gpi-adapter';
import { SwiftIsoAdapter } from './swift-iso/swift-iso-adapter';
import { ThirdwebAdapter } from './thirdweb/thirdweb-adapter';
import { TtRouteAdapter } from './tt-route/tt-route-adapter';
/**
* Canonical IDs for SolaceNet-maintained gateway rail adapters (see docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md).
* Implementations are scaffolds until replaced with production connectors.
*/
export const GATEWAY_RAIL_ADAPTER_IDS = [
'dbis.adapter.swift-fin',
'dbis.adapter.swift-iso',
'dbis.adapter.swift-gpi',
'dbis.adapter.dtc-settlement',
'dbis.adapter.dtcc-nscc',
'dbis.adapter.dtcc-ficc',
'dbis.adapter.tt-route',
'dbis.adapter.ktt-evidence',
'dbis.adapter.thirdweb',
] as const;
export type GatewayRailAdapterId = (typeof GATEWAY_RAIL_ADAPTER_IDS)[number];
const factories: Record<GatewayRailAdapterId, () => GatewayAdapter> = {
'dbis.adapter.swift-fin': () => new SwiftFinAdapter(),
'dbis.adapter.swift-iso': () => new SwiftIsoAdapter(),
'dbis.adapter.swift-gpi': () => new SwiftGpiAdapter(),
'dbis.adapter.dtc-settlement': () => new DtcSettlementAdapter(),
'dbis.adapter.dtcc-nscc': () => new DtccNsccAdapter(),
'dbis.adapter.dtcc-ficc': () => new DtccFiccAdapter(),
'dbis.adapter.tt-route': () => new TtRouteAdapter(),
'dbis.adapter.ktt-evidence': () => new KttEvidenceAdapter(),
'dbis.adapter.thirdweb': () => new ThirdwebAdapter(),
};
/** All registered rail adapter IDs (stable order). */
export function listGatewayRailAdapterIds(): GatewayRailAdapterId[] {
return [...GATEWAY_RAIL_ADAPTER_IDS];
}
/** Instantiate a fresh adapter instance for the given ID (caller may `initialize()`). */
export function createGatewayRailAdapter(adapterId: string): GatewayAdapter | undefined {
if (!isGatewayRailAdapterId(adapterId)) return undefined;
return factories[adapterId]();
}
export function isGatewayRailAdapterId(id: string): id is GatewayRailAdapterId {
return (GATEWAY_RAIL_ADAPTER_IDS as readonly string[]).includes(id);
}

View File

@@ -4,6 +4,7 @@ import { AdapterReceiveResult, AdapterSendResult, AdapterValidateResult } from '
/**
* dbis.adapter.ktt-evidence
* Scaffold adapter to ingest legacy instruction artifacts as untrusted evidence.
* Rail/protocol governance: maintained under SolaceNet — see docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md
*/
export class KttEvidenceAdapter extends AdapterBase {
async validate(_: Record<string, unknown>): Promise<AdapterValidateResult> {

View File

@@ -1,10 +1,11 @@
import type { Provider, Signer } from 'ethers';
import { AdapterBase } from '../sdk/adapter-base';
import { AdapterReceiveResult, AdapterSendResult, AdapterValidateResult } from '../sdk/adapter-types';
/**
* Thirdweb Gateway Adapter
*
* Adapter for interacting with smart contracts via Thirdweb SDK.
* dbis.adapter.thirdweb
*
* Thirdweb Gateway Adapter — contract invocation via Thirdweb SDK (not a bank messaging rail).
* Supports multiple chains and contract method invocation.
*/
export class ThirdwebAdapter extends AdapterBase {
@@ -400,13 +401,13 @@ export class ThirdwebAdapter extends AdapterBase {
// Create contract instance
// In production, ABI would be provided via envelope or configuration
// For now, we'll use a minimal approach with ethers.js
const provider = contractObj.provider as ethers.Provider;
const provider = contractObj.provider as Provider;
if (!contractObj.signer) {
throw new Error('Signer not available for transaction execution');
}
const signer = contractObj.signer as ethers.Signer;
const signer = contractObj.signer as Signer;
// Build transaction
const txRequest: Record<string, unknown> = {

View File

@@ -0,0 +1,17 @@
# Gateway rails enforcement
Optional SolaceNet capability checks for **gateway** HTTP routes.
| Variable | Effect |
|----------|--------|
| `SOLACENET_GATEWAY_RAILS_ENFORCE` | When `1` or `true`, requires capability **`gateway-microservices`** for tenant-sensitive gateway operations (rail health/validate/receive, instructions, event replay). |
| `SOLACENET_DEFAULT_TENANT_ID` | Fallback tenant when `x-tenant-id` header and `body.tenantId` are absent. |
| `SOLACENET_GATEWAY_AUDIT_LOG_PATH` | Optional path for **NDJSON** lines (one JSON object per allow/deny) in addition to structured Winston logs. |
| `GATEWAY_RAIL_MUTATE_WINDOW_MS` / `GATEWAY_RAIL_MUTATE_MAX` | Extra **per-IP** limit for `POST .../rails/:adapterId/validate|receive` (default 120 per 60s). |
| `GATEWAY_RAIL_RATE_LIMIT_IN_TEST` | Set to `1` in Jest so integration tests skip the rail mutate limiter (see `jest.gateway-http.config.js`). |
Tenant resolution order: **`body.tenantId`** → **`x-tenant-id`** → **`SOLACENET_DEFAULT_TENANT_ID`** → **`system`**.
**Worker ingress:** `requireGatewayMicroservicesForWorker()` in `gateway-rails-enforcement.ts` — call from file/MQ consumers when enforcement is on.
**Related:** `gateway-rails-audit.ts`, `../routes/gateway.routes.ts`, `../adapters/gateway-adapter-registry.ts`, `docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md`, `docs/solacenet/SOLACENET_GATEWAY_RAILS_ENFORCE_RUNBOOK.md`.

View File

@@ -0,0 +1,42 @@
import fs from 'fs';
import { logger } from '@/infrastructure/monitoring/logger';
import type { ErrorCode } from '@/shared/types';
export type GatewayRailsAuditDecision = 'allow' | 'deny';
export interface GatewayRailsAuditRecord {
decision: GatewayRailsAuditDecision;
tenantId: string;
path: string;
method: string;
adapterId?: string;
errorCode?: ErrorCode | string;
correlationId?: string;
ingress?: string;
}
/**
* Structured audit for SolaceNet gateway rail capability checks (PG-GW-W03).
* Always logs to Winston; optionally appends one NDJSON line per event when
* SOLACENET_GATEWAY_AUDIT_LOG_PATH is set (operator-owned directory, rotation).
*/
export function recordGatewayRailsAudit(record: GatewayRailsAuditRecord): void {
const payload = {
auditType: 'gateway_rails_enforcement',
ts: new Date().toISOString(),
...record,
};
logger.info('Gateway rails enforcement audit', payload);
const path = process.env.SOLACENET_GATEWAY_AUDIT_LOG_PATH?.trim();
if (!path) return;
try {
fs.appendFileSync(path, `${JSON.stringify(payload)}\n`, { encoding: 'utf8', flag: 'a' });
} catch (err) {
logger.warn('SOLACENET_GATEWAY_AUDIT_LOG_PATH append failed', {
path,
error: err instanceof Error ? err.message : String(err),
});
}
}

View File

@@ -0,0 +1,124 @@
import type { Request } from 'express';
import { requireCapability } from '@/shared/solacenet/sdk';
import { DbisError } from '@/shared/types';
import { recordGatewayRailsAudit } from './gateway-rails-audit';
/**
* When SOLACENET_GATEWAY_RAILS_ENFORCE=1|true, rail mutating endpoints require
* SolaceNet capability `gateway-microservices` for the resolved tenant.
* Default is off so existing integrations keep working until explicitly enabled.
*/
export function gatewayRailsEnforcementEnabled(): boolean {
const v = process.env.SOLACENET_GATEWAY_RAILS_ENFORCE;
return v === '1' || v === 'true';
}
export function resolveGatewayTenantId(req: Request): string {
const h = req.headers['x-tenant-id'];
const fromHeader = typeof h === 'string' ? h : Array.isArray(h) ? h[0] : '';
const body = req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {};
const fromBody = typeof body.tenantId === 'string' ? body.tenantId : '';
return (
fromBody ||
fromHeader ||
process.env.SOLACENET_DEFAULT_TENANT_ID ||
'system'
);
}
function gatewayRailsAdapterIdFromRequest(req: Request): string | undefined {
const id = req.params?.adapterId;
return typeof id === 'string' ? id : undefined;
}
function correlationIdFromRequest(req: Request): string | undefined {
const c = (req as { correlationId?: string }).correlationId;
return typeof c === 'string' ? c : undefined;
}
/**
* HTTP routes: enforce `gateway-microservices` when SOLACENET_GATEWAY_RAILS_ENFORCE is on.
* Emits allow/deny audit (log + optional NDJSON file).
*/
export async function maybeRequireGatewayMicroservices(req: Request): Promise<void> {
if (!gatewayRailsEnforcementEnabled()) return;
const tenantId = resolveGatewayTenantId(req);
const adapterId = gatewayRailsAdapterIdFromRequest(req);
const correlationId = correlationIdFromRequest(req);
try {
await requireCapability('gateway-microservices', {
tenantId,
capabilityId: 'gateway-microservices',
channel: 'API',
});
recordGatewayRailsAudit({
decision: 'allow',
tenantId,
path: req.path,
method: req.method,
adapterId,
correlationId,
ingress: 'http',
});
} catch (err) {
if (err instanceof DbisError) {
recordGatewayRailsAudit({
decision: 'deny',
tenantId,
path: req.path,
method: req.method,
adapterId,
correlationId,
errorCode: err.code,
ingress: 'http',
});
}
throw err;
}
}
export interface GatewayWorkerIngressContext {
tenantId: string;
/** e.g. file-drop, mq, worker */
ingressKind: string;
/** Free-form locator (queue name, file glob, job id) */
detail?: string;
adapterId?: string;
}
/**
* Non-HTTP ingress (file workers, MQ consumers): call at start of processing when
* SOLACENET_GATEWAY_RAILS_ENFORCE is enabled. Audit only; does not replace auth on the worker.
*/
export async function requireGatewayMicroservicesForWorker(ctx: GatewayWorkerIngressContext): Promise<void> {
if (!gatewayRailsEnforcementEnabled()) return;
try {
await requireCapability('gateway-microservices', {
tenantId: ctx.tenantId,
capabilityId: 'gateway-microservices',
channel: ctx.ingressKind,
});
recordGatewayRailsAudit({
decision: 'allow',
tenantId: ctx.tenantId,
path: ctx.detail ?? ctx.ingressKind,
method: 'WORKER',
adapterId: ctx.adapterId,
ingress: ctx.ingressKind,
});
} catch (err) {
if (err instanceof DbisError) {
recordGatewayRailsAudit({
decision: 'deny',
tenantId: ctx.tenantId,
path: ctx.detail ?? ctx.ingressKind,
method: 'WORKER',
adapterId: ctx.adapterId,
errorCode: err.code,
ingress: ctx.ingressKind,
});
}
throw err;
}
}

View File

@@ -1,20 +1,266 @@
/**
* @swagger
* tags:
* name: SolaceNet Gateway Rails
* description: SolaceNet-maintained financial rail adapters (metadata, health, validate, receive)
*/
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import { v4 as uuidv4 } from 'uuid';
import { gatewayApiService } from '../edge/api-gateway.service';
import { eventStoreService } from '../data/event-store.service';
import { gatewayInboxService } from '../control/inbox.service';
import {
createGatewayRailAdapter,
isGatewayRailAdapterId,
listGatewayRailAdapterIds,
} from '../adapters/gateway-adapter-registry';
import {
gatewayRailsEnforcementEnabled,
maybeRequireGatewayMicroservices,
} from '../rails/gateway-rails-enforcement';
import { DbisError } from '@/shared/types';
const router = Router();
// Attach correlation middleware for all gateway routes
router.use(gatewayApiService.correlationMiddleware.bind(gatewayApiService));
// Stricter budget for POST validate/receive (per-IP; stacks with global /api limiter).
if (process.env.GATEWAY_RAIL_RATE_LIMIT_IN_TEST !== '1') {
const windowMs = parseInt(process.env.GATEWAY_RAIL_MUTATE_WINDOW_MS || '60000', 10);
const max = parseInt(process.env.GATEWAY_RAIL_MUTATE_MAX || '120', 10);
const railMutateLimiter = rateLimit({
windowMs,
max,
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: `Rail validate/receive limit: ${max} requests per ${Math.ceil(windowMs / 1000)}s.`,
},
timestamp: new Date(),
},
});
router.use((req, res, next) => {
if (req.method === 'POST' && /^\/rails\/[^/]+\/(validate|receive)\/?$/.test(req.path)) {
return railMutateLimiter(req, res, next);
}
next();
});
}
/**
* POST /api/v1/gateway/instructions
* Scaffold endpoint to accept a canonical instruction and emit an event.
* @swagger
* /api/v1/gateway/rails:
* get:
* summary: List rail adapter IDs
* description: SolaceNet-maintained gateway rail adapter identifiers (no live wire I/O). Uses the same API auth as other /api/v1 routes; does not call SolaceNet rail enforcement (metadata only).
* tags: [SolaceNet Gateway Rails]
* security:
* - SovereignToken: []
* responses:
* 200:
* description: Adapter list and enforcement flag
*/
router.get('/rails', async (_req, res, next) => {
try {
return res.json({
maintainer: 'SolaceNet',
enforcementEnabled: gatewayRailsEnforcementEnabled(),
adapters: listGatewayRailAdapterIds(),
governanceDoc: 'dbis_core/docs/solacenet/RAIL_AND_PROTOCOL_GOVERNANCE.md',
});
} catch (error) {
return next(error);
}
});
/**
* @swagger
* /api/v1/gateway/rails/{adapterId}/health:
* get:
* summary: Rail adapter health
* tags: [SolaceNet Gateway Rails]
* security:
* - SovereignToken: []
* parameters:
* - in: path
* name: adapterId
* required: true
* schema:
* type: string
* description: e.g. dbis.adapter.swift-fin (URL-encode dots if your client requires it)
* responses:
* 200:
* description: Health status
* 404:
* description: Unknown adapter
* 403:
* description: SolaceNet capability denied when SOLACENET_GATEWAY_RAILS_ENFORCE is enabled
*/
router.get('/rails/:adapterId/health', async (req, res, next) => {
try {
await maybeRequireGatewayMicroservices(req);
const adapterId = req.params.adapterId;
if (!isGatewayRailAdapterId(adapterId)) {
return res.status(404).json({ error: 'UNKNOWN_ADAPTER', adapterId });
}
const adapter = createGatewayRailAdapter(adapterId)!;
await adapter.initialize({}, undefined);
const health = await adapter.health();
return res.json({ adapterId, ...health });
} catch (error) {
if (error instanceof DbisError) {
return res.status(403).json({ error: error.message, code: error.code });
}
return next(error);
}
});
/**
* @swagger
* /api/v1/gateway/rails/{adapterId}/validate:
* post:
* summary: Validate canonical instruction against a rail adapter
* tags: [SolaceNet Gateway Rails]
* security:
* - SovereignToken: []
* parameters:
* - in: path
* name: adapterId
* required: true
* schema:
* type: string
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* tenantId:
* type: string
* canonicalInstruction:
* type: object
* responses:
* 200:
* description: Validation result
*/
router.post('/rails/:adapterId/validate', async (req, res, next) => {
try {
await maybeRequireGatewayMicroservices(req);
const adapterId = req.params.adapterId;
if (!isGatewayRailAdapterId(adapterId)) {
return res.status(404).json({ error: 'UNKNOWN_ADAPTER', adapterId });
}
const body = req.body || {};
const canonicalInstruction =
body.canonicalInstruction && typeof body.canonicalInstruction === 'object'
? (body.canonicalInstruction as Record<string, unknown>)
: {};
const adapter = createGatewayRailAdapter(adapterId)!;
await adapter.initialize({}, undefined);
const result = await adapter.validate(canonicalInstruction);
return res.json({ adapterId, ...result });
} catch (error) {
if (error instanceof DbisError) {
return res.status(403).json({ error: error.message, code: error.code });
}
return next(error);
}
});
/**
* @swagger
* /api/v1/gateway/rails/{adapterId}/receive:
* post:
* summary: Receive / ingest rail payload (scaffold)
* tags: [SolaceNet Gateway Rails]
* security:
* - SovereignToken: []
* parameters:
* - in: path
* name: adapterId
* required: true
* schema:
* type: string
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* tenantId:
* type: string
* payload: {}
* responses:
* 200:
* description: Receive result
*/
router.post('/rails/:adapterId/receive', async (req, res, next) => {
try {
await maybeRequireGatewayMicroservices(req);
const adapterId = req.params.adapterId;
if (!isGatewayRailAdapterId(adapterId)) {
return res.status(404).json({ error: 'UNKNOWN_ADAPTER', adapterId });
}
const payload = (req.body && 'payload' in req.body ? req.body.payload : req.body) as unknown;
const adapter = createGatewayRailAdapter(adapterId)!;
await adapter.initialize({}, undefined);
const result = await adapter.receive(payload);
return res.json({ adapterId, ...result });
} catch (error) {
if (error instanceof DbisError) {
return res.status(403).json({ error: error.message, code: error.code });
}
return next(error);
}
});
/**
* @swagger
* /api/v1/gateway/instructions:
* post:
* summary: Ingest canonical instruction (scaffold)
* description: Accepts a JSON body, deduplicates via gateway inbox (scaffold), appends InstructionReceived to the in-memory event store, returns 202. When SOLACENET_GATEWAY_RAILS_ENFORCE is set, requires gateway-microservices for the resolved tenant (header x-tenant-id or body.tenantId).
* tags: [SolaceNet Gateway Rails]
* security:
* - SovereignToken: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* tenantId:
* type: string
* description: Tenant for capability check when enforcement is enabled
* txnId:
* type: string
* description: Optional idempotency / trace id (server generates if omitted)
* rail:
* type: string
* example: CANONICAL
* amount:
* type: string
* currency:
* type: string
* metadata:
* type: object
* additionalProperties: true
* responses:
* 202:
* description: Accepted — instruction received (scaffold)
* 403:
* description: SolaceNet capability denied when enforcement enabled
*/
router.post('/instructions', async (req, res, next) => {
try {
await maybeRequireGatewayMicroservices(req);
const txnId = req.body?.txnId || `TXN-${uuidv4()}`;
const correlationId = (req as any).correlationId || `CORR-${uuidv4()}`;
@@ -36,20 +282,44 @@ router.post('/instructions', async (req, res, next) => {
return res.status(202).json({ txnId, correlationId, status: 'RECEIVED' });
} catch (error) {
if (error instanceof DbisError) {
return res.status(403).json({ error: error.message, code: error.code });
}
return next(error);
}
});
/**
* GET /api/v1/gateway/events/replay
* Simple replay endpoint for canonical events (scaffold).
* @swagger
* /api/v1/gateway/events/replay:
* get:
* summary: Replay canonical events (scaffold, in-memory)
* tags: [SolaceNet Gateway Rails]
* security:
* - SovereignToken: []
* parameters:
* - in: query
* name: from
* schema:
* type: integer
* default: 0
* description: Start index into the in-memory event buffer
* responses:
* 200:
* description: Event slice
* 403:
* description: SolaceNet capability denied when enforcement enabled
*/
router.get('/events/replay', async (req, res, next) => {
try {
await maybeRequireGatewayMicroservices(req);
const from = Number(req.query.from || 0);
const events = eventStoreService.replay(from);
return res.json({ count: events.length, events });
} catch (error) {
if (error instanceof DbisError) {
return res.status(403).json({ error: error.message, code: error.code });
}
return next(error);
}
});

View File

@@ -1,6 +1,7 @@
// IRU Marketplace Service
// Business logic for Sankofa Phoenix Marketplace
import { randomBytes } from 'crypto';
import prisma from '@/shared/database/prisma';
import { v4 as uuidv4 } from 'uuid';
import { DbisError, ErrorCode } from '@/shared/types';
@@ -150,7 +151,7 @@ export class MarketplaceService {
const existingInquiry = await prisma.iruInquiry.findFirst({
where: {
contactEmail: request.contactEmail,
offeringId: request.offeringId,
offeringId: offering.id,
status: {
in: ['submitted', 'acknowledged', 'in_review'],
},
@@ -164,13 +165,13 @@ export class MarketplaceService {
);
}
// Create inquiry
const inquiryId = `INQ-${uuidv4().substring(0, 8).toUpperCase()}`;
// Unpredictable public id (INQ- + 32 hex) for unauthenticated status lookups
const inquiryId = `INQ-${randomBytes(16).toString('hex').toUpperCase()}`;
const inquiry = await prisma.iruInquiry.create({
data: {
id: uuidv4(),
inquiryId,
offeringId: request.offeringId,
offeringId: offering.id,
organizationName: request.organizationName,
institutionalType: request.institutionalType,
jurisdiction: request.jurisdiction,
@@ -194,7 +195,7 @@ export class MarketplaceService {
variables: {
inquiryId: inquiry.inquiryId,
organizationName: inquiry.organizationName,
offeringId: inquiry.offeringId,
offeringId: offering.offeringId,
},
priority: 'high',
});
@@ -236,7 +237,42 @@ export class MarketplaceService {
}
/**
* Get inquiry status
* Public inquiry status (unauthenticated). No org/contact, qualification, risk, or notes.
*/
async getInquiryStatusPublic(inquiryId: string): Promise<{
inquiryId: string;
status: string;
offering: { name: string; capacityTier: number };
submittedAt: Date;
acknowledgedAt: Date | null;
reviewedAt: Date | null;
completedAt: Date | null;
}> {
const inquiry = await prisma.iruInquiry.findUnique({
where: { inquiryId },
include: { offering: true },
});
if (!inquiry) {
throw new DbisError(ErrorCode.NOT_FOUND, `Inquiry ${inquiryId} not found`);
}
return {
inquiryId: inquiry.inquiryId,
status: inquiry.status,
offering: {
name: inquiry.offering.name,
capacityTier: inquiry.offering.capacityTier,
},
submittedAt: inquiry.submittedAt,
acknowledgedAt: inquiry.acknowledgedAt,
reviewedAt: inquiry.reviewedAt,
completedAt: inquiry.completedAt,
};
}
/**
* Full inquiry row (operators / legacy). Prefer getInquiryStatusPublic for public HTTP.
*/
async getInquiryStatus(inquiryId: string): Promise<any> {
const inquiry = await prisma.iruInquiry.findUnique({

View File

@@ -81,7 +81,7 @@ export class NotificationService {
],
from: {
email: process.env.EMAIL_FROM || 'noreply@dbis.org',
name: 'DBIS IRU',
name: process.env.EMAIL_FROM_NAME || 'SolaceNet',
},
content: [
{

View File

@@ -24,6 +24,7 @@ export class SMTPIntegration {
const smtpUser = process.env.SMTP_USER;
const smtpPassword = process.env.SMTP_PASSWORD;
const smtpSecure = process.env.SMTP_SECURE === 'true';
const tlsRejectUnauthorized = process.env.SMTP_TLS_REJECT_UNAUTHORIZED !== 'false';
const fromEmail = request.from || process.env.EMAIL_FROM || 'noreply@dbis.org';
// In production, use nodemailer:
@@ -63,6 +64,7 @@ export class SMTPIntegration {
pass: smtpPassword,
}
: undefined,
tls: tlsRejectUnauthorized ? undefined : { rejectUnauthorized: false },
});
result = await retryWithBackoff(

View File

@@ -78,57 +78,65 @@ export class TemplateLoaderService {
'inquiry-submitted': {
id: 'inquiry-submitted',
name: 'inquiry-submitted',
subject: 'IRU Inquiry Submitted',
body: 'Your IRU inquiry has been submitted successfully. Inquiry ID: {{inquiryId}}',
subject: 'SolaceNet (IRU) inquiry submitted — Solace Bank Group PLC',
body:
'Your SolaceNet (Irrevocable Right of Use) inquiry has been submitted. Vendor: Solace Bank Group PLC. Inquiry ID: {{inquiryId}}',
variables: ['inquiryId'],
},
'inquiry-acknowledged': {
id: 'inquiry-acknowledged',
name: 'inquiry-acknowledged',
subject: 'IRU Inquiry Acknowledged',
body: 'Your IRU inquiry has been acknowledged. We will review it within 24 hours. Inquiry ID: {{inquiryId}}',
subject: 'SolaceNet inquiry acknowledged — Solace Bank Group PLC',
body:
'Your SolaceNet (IRU) inquiry has been acknowledged. We will review it within 24 hours. Inquiry ID: {{inquiryId}}',
variables: ['inquiryId'],
},
'qualification-complete': {
id: 'qualification-complete',
name: 'qualification-complete',
subject: 'IRU Qualification Complete',
body: 'Your IRU qualification is complete. Status: {{status}}. Inquiry ID: {{inquiryId}}',
subject: 'SolaceNet qualification complete — Solace Bank Group PLC',
body:
'Your SolaceNet (IRU) qualification is complete. Status: {{status}}. Inquiry ID: {{inquiryId}}',
variables: ['status', 'inquiryId'],
},
'agreement-ready': {
id: 'agreement-ready',
name: 'agreement-ready',
subject: 'IRU Agreement Ready for Signature',
body: 'Your IRU Participation Agreement is ready for signature. Agreement ID: {{agreementId}}',
subject: 'SolaceNet agreement ready for signature',
body:
'Your SolaceNet participation agreement is ready for signature. Agreement ID: {{agreementId}}',
variables: ['agreementId'],
},
'deployment-complete': {
id: 'deployment-complete',
name: 'deployment-complete',
subject: 'IRU Deployment Complete',
body: 'Your IRU infrastructure has been deployed successfully. Subscription ID: {{subscriptionId}}',
subject: 'SolaceNet deployment complete',
body:
'Your SolaceNet (IRU) infrastructure has been deployed successfully. Subscription ID: {{subscriptionId}}',
variables: ['subscriptionId'],
},
'deployment-failed': {
id: 'deployment-failed',
name: 'deployment-failed',
subject: 'IRU Deployment Failed',
body: 'Your IRU deployment has failed. Deployment ID: {{deploymentId}}. Error: {{error}}',
subject: 'SolaceNet deployment failed',
body:
'Your SolaceNet (IRU) deployment has failed. Deployment ID: {{deploymentId}}. Error: {{error}}',
variables: ['deploymentId', 'error'],
},
'payment-success': {
id: 'payment-success',
name: 'payment-success',
subject: 'Payment Successful',
body: 'Your payment for IRU subscription {{subscriptionId}} has been processed successfully. Amount: {{currency}} {{amount}}',
subject: 'Payment successful — SolaceNet',
body:
'Your payment for SolaceNet subscription {{subscriptionId}} has been processed successfully. Amount: {{currency}} {{amount}}',
variables: ['subscriptionId', 'currency', 'amount'],
},
'payment-failed': {
id: 'payment-failed',
name: 'payment-failed',
subject: 'Payment Failed',
body: 'Your payment for IRU subscription {{subscriptionId}} has failed. Transaction ID: {{transactionId}}',
subject: 'Payment failed — SolaceNet',
body:
'Your payment for SolaceNet subscription {{subscriptionId}} has failed. Transaction ID: {{transactionId}}',
variables: ['subscriptionId', 'transactionId'],
},
};
@@ -137,8 +145,8 @@ export class TemplateLoaderService {
templates[templateName] || {
id: templateName,
name: templateName,
subject: 'DBIS IRU Notification',
body: 'You have a new notification from DBIS IRU.',
subject: 'SolaceNet / Sankofa Marketplace notification',
body: 'You have a new notification regarding SolaceNet (IRU) on Sankofa Marketplace.',
variables: [],
}
);

View File

@@ -43,6 +43,8 @@ export class CardIssuingService {
await requireCapability('card-issuing', {
tenantId: request.tenantId,
programId: request.programId,
capabilityId: 'card-issuing',
channel: 'API',
});
// Risk assessment
@@ -84,6 +86,8 @@ export class CardIssuingService {
async controlCard(request: CardControlRequest): Promise<void> {
await requireCapability('card-controls', {
tenantId: request.tenantId,
capabilityId: 'card-controls',
channel: 'API',
});
logger.info(`Card control action`, {

View File

@@ -33,6 +33,8 @@ export class MobileMoneyService {
tenantId: request.tenantId,
programId: request.programId,
region: request.region,
capabilityId: 'mobile-money-connector',
channel: 'API',
});
// Route to specific capability based on transaction type
@@ -40,16 +42,22 @@ export class MobileMoneyService {
await requireCapability('mobile-money-cash-in', {
tenantId: request.tenantId,
region: request.region,
capabilityId: 'mobile-money-cash-in',
channel: 'API',
});
} else if (request.transactionType === 'cash-out') {
await requireCapability('mobile-money-cash-out', {
tenantId: request.tenantId,
region: request.region,
capabilityId: 'mobile-money-cash-out',
channel: 'API',
});
} else if (request.transactionType === 'transfer') {
await requireCapability('mobile-money-transfers', {
tenantId: request.tenantId,
region: request.region,
capabilityId: 'mobile-money-transfers',
channel: 'API',
});
}

View File

@@ -43,6 +43,7 @@ export class PaymentGatewayService {
programId: request.programId,
region: request.region,
channel: request.channel,
capabilityId: 'payment-gateway',
});
// Check limits
@@ -76,7 +77,11 @@ export class PaymentGatewayService {
* Capture a payment intent
*/
async capturePayment(intentId: string, tenantId: string): Promise<void> {
await requireCapability('payment-gateway', { tenantId });
await requireCapability('payment-gateway', {
tenantId,
capabilityId: 'payment-gateway',
channel: 'API',
});
// In production, fetch intent, calculate fees, post to ledger
logger.info(`Payment captured`, { intentId });
@@ -86,7 +91,11 @@ export class PaymentGatewayService {
* Refund a payment
*/
async refundPayment(paymentId: string, amount: string, tenantId: string): Promise<void> {
await requireCapability('payment-gateway', { tenantId });
await requireCapability('payment-gateway', {
tenantId,
capabilityId: 'payment-gateway',
channel: 'API',
});
// In production, process refund, post to ledger
logger.info(`Payment refunded`, { paymentId, amount });

View File

@@ -56,7 +56,8 @@ router.post('/mint', async (req, res) => {
tenantId: req.headers['x-tenant-id'] as string || 'default',
programId: req.headers['x-program-id'] as string || 'default',
region: req.headers['x-region'] as string || 'default',
channel: req.headers['x-channel'] as string || 'default'
channel: req.headers['x-channel'] as string || 'default',
capabilityId: 'tokenization.mint',
};
const result = await tokenizationService.mintToken(req.body, context);
@@ -84,7 +85,8 @@ router.post('/transfer', async (req, res) => {
tenantId: req.headers['x-tenant-id'] as string || 'default',
programId: req.headers['x-program-id'] as string || 'default',
region: req.headers['x-region'] as string || 'default',
channel: req.headers['x-channel'] as string || 'default'
channel: req.headers['x-channel'] as string || 'default',
capabilityId: 'tokenization.transfer',
};
const { tokenId, from, to, amount } = req.body;
@@ -113,7 +115,8 @@ router.post('/redeem', async (req, res) => {
tenantId: req.headers['x-tenant-id'] as string || 'default',
programId: req.headers['x-program-id'] as string || 'default',
region: req.headers['x-region'] as string || 'default',
channel: req.headers['x-channel'] as string || 'default'
channel: req.headers['x-channel'] as string || 'default',
capabilityId: 'tokenization.redeem',
};
const { tokenId, redeemer, amount } = req.body;
@@ -142,7 +145,8 @@ router.get('/status/:requestId', async (req, res) => {
tenantId: req.headers['x-tenant-id'] as string || 'default',
programId: req.headers['x-program-id'] as string || 'default',
region: req.headers['x-region'] as string || 'default',
channel: req.headers['x-channel'] as string || 'default'
channel: req.headers['x-channel'] as string || 'default',
capabilityId: 'tokenization.view',
};
const result = await tokenizationService.getStatus(req.params.requestId, context);
@@ -170,7 +174,8 @@ router.get('/token/:tokenId', async (req, res) => {
tenantId: req.headers['x-tenant-id'] as string || 'default',
programId: req.headers['x-program-id'] as string || 'default',
region: req.headers['x-region'] as string || 'default',
channel: req.headers['x-channel'] as string || 'default'
channel: req.headers['x-channel'] as string || 'default',
capabilityId: 'tokenization.view',
};
const result = await tokenizationService.getTokenDetails(req.params.tokenId, context);

View File

@@ -44,6 +44,8 @@ export class WalletAccountsService {
await requireCapability('wallet-accounts', {
tenantId: request.tenantId,
programId: request.programId,
capabilityId: 'wallet-accounts',
channel: 'API',
});
const wallet: WalletAccount = {
@@ -66,7 +68,11 @@ export class WalletAccountsService {
* Get wallet by ID
*/
async getWallet(walletId: string, tenantId: string): Promise<WalletAccount | null> {
await requireCapability('wallet-accounts', { tenantId });
await requireCapability('wallet-accounts', {
tenantId,
capabilityId: 'wallet-accounts',
channel: 'API',
});
// In production, fetch from database
return null;
}
@@ -77,6 +83,8 @@ export class WalletAccountsService {
async transfer(request: WalletTransferRequest): Promise<void> {
await requireCapability('p2p-transfers', {
tenantId: request.tenantId,
capabilityId: 'p2p-transfers',
channel: 'API',
});
// Post to ledger

View File

@@ -2,6 +2,7 @@
// Manages tenant/program/region/channel entitlements with allowlist support
import { v4 as uuidv4 } from 'uuid';
import type { Prisma } from '@prisma/client';
import prisma from '@/shared/database/prisma';
import { DbisError, ErrorCode } from '@/shared/types';
import { logger } from '@/infrastructure/monitoring/logger';
@@ -85,13 +86,14 @@ export class EntitlementsService {
// Check if entitlement already exists
const existing = await prisma.solacenet_entitlement.findUnique({
where: {
// Generated client types require string for nullable composite dims; DB/query accept null.
tenantId_programId_capabilityId_region_channel: {
tenantId: request.tenantId,
programId: request.programId || null,
programId: request.programId ?? null,
capabilityId: request.capabilityId,
region: request.region || null,
channel: request.channel || null,
},
region: request.region ?? null,
channel: request.channel ?? null,
} as any,
},
});
@@ -106,15 +108,16 @@ export class EntitlementsService {
data: {
id: uuidv4(),
tenantId: request.tenantId,
programId: request.programId || null,
programId: request.programId ?? undefined,
capabilityId: request.capabilityId,
region: request.region || null,
channel: request.channel || null,
stateOverride: request.stateOverride || null,
allowlist: request.allowlist || [],
region: request.region ?? undefined,
channel: request.channel ?? undefined,
stateOverride: request.stateOverride ?? undefined,
allowlist: (request.allowlist ?? []) as Prisma.InputJsonValue,
effectiveFrom: request.effectiveFrom || new Date(),
effectiveTo: request.effectiveTo || null,
metadata: request.metadata || null,
effectiveTo: request.effectiveTo ?? undefined,
metadata:
request.metadata != null ? (request.metadata as Prisma.InputJsonValue) : undefined,
},
include: {
capability: true,
@@ -140,14 +143,18 @@ export class EntitlementsService {
throw new DbisError(ErrorCode.NOT_FOUND, `Entitlement ${id} not found`);
}
const nextAllowlist = updates.allowlist ?? entitlement.allowlist;
const nextMetadata = updates.metadata ?? entitlement.metadata;
const updated = await prisma.solacenet_entitlement.update({
where: { id },
data: {
stateOverride: updates.stateOverride || entitlement.stateOverride,
allowlist: updates.allowlist || entitlement.allowlist,
stateOverride: updates.stateOverride ?? entitlement.stateOverride ?? undefined,
allowlist: nextAllowlist as Prisma.InputJsonValue,
effectiveFrom: updates.effectiveFrom || entitlement.effectiveFrom,
effectiveTo: updates.effectiveTo !== undefined ? updates.effectiveTo : entitlement.effectiveTo,
metadata: updates.metadata || entitlement.metadata,
metadata:
nextMetadata != null ? (nextMetadata as Prisma.InputJsonValue) : undefined,
},
include: {
capability: true,

View File

@@ -2,6 +2,7 @@
// Makes runtime policy decisions based on entitlements, rules, and conditions
import { v4 as uuidv4 } from 'uuid';
import type { Prisma } from '@prisma/client';
import prisma from '@/shared/database/prisma';
import { DbisError, ErrorCode } from '@/shared/types';
import { logger } from '@/infrastructure/monitoring/logger';
@@ -239,14 +240,16 @@ export class PolicyEngineService {
capabilityId: request.capabilityId,
scope: request.scope,
scopeValue: request.scopeValue || null,
condition: request.condition,
condition: request.condition as Prisma.InputJsonValue,
decision: request.decision,
limits: request.limits || null,
limits:
request.limits != null ? (request.limits as Prisma.InputJsonValue) : undefined,
reason: request.reason || null,
ticket: request.ticket || null,
priority: request.priority || 100,
status: 'active',
metadata: request.metadata || null,
metadata:
request.metadata != null ? (request.metadata as Prisma.InputJsonValue) : undefined,
},
});

View File

@@ -2,6 +2,7 @@
// Manages the registry of all capabilities with CRUD operations, dependency validation, and version management
import { v4 as uuidv4 } from 'uuid';
import type { Prisma } from '@prisma/client';
import prisma from '@/shared/database/prisma';
import { DbisError, ErrorCode } from '@/shared/types';
import { logger } from '@/infrastructure/monitoring/logger';
@@ -106,11 +107,15 @@ export class CapabilityRegistryService {
version: request.version || '1.0.0',
description: request.description,
ownerTeam: request.ownerTeam,
dependencies: request.dependencies || [],
configSchema: request.configSchema || null,
dependencies: (request.dependencies || []) as Prisma.InputJsonValue,
configSchema:
request.configSchema != null
? (request.configSchema as Prisma.InputJsonValue)
: undefined,
defaultState: request.defaultState || CapabilityState.DISABLED,
status: 'active',
metadata: request.metadata || null,
metadata:
request.metadata != null ? (request.metadata as Prisma.InputJsonValue) : undefined,
},
});
@@ -180,19 +185,30 @@ export class CapabilityRegistryService {
);
}
const updateData: Prisma.solacenet_capabilityUpdateInput = {};
if (request.name !== undefined) updateData.name = request.name;
if (request.version !== undefined) updateData.version = request.version;
if (request.description !== undefined) updateData.description = request.description;
if (request.ownerTeam !== undefined) updateData.ownerTeam = request.ownerTeam;
if (request.dependencies !== undefined) {
updateData.dependencies = request.dependencies as Prisma.InputJsonValue;
}
if (request.configSchema !== undefined) {
updateData.configSchema =
request.configSchema != null
? (request.configSchema as Prisma.InputJsonValue)
: undefined;
}
if (request.defaultState !== undefined) updateData.defaultState = request.defaultState;
if (request.status !== undefined) updateData.status = request.status;
if (request.metadata !== undefined) {
updateData.metadata =
request.metadata != null ? (request.metadata as Prisma.InputJsonValue) : undefined;
}
const capability = await prisma.solacenet_capability.update({
where: { capabilityId },
data: {
name: request.name,
version: request.version,
description: request.description,
ownerTeam: request.ownerTeam,
dependencies: request.dependencies,
configSchema: request.configSchema,
defaultState: request.defaultState,
status: request.status,
metadata: request.metadata,
},
data: updateData,
});
logger.info(`Capability updated: ${capabilityId}`);
@@ -329,7 +345,7 @@ export class CapabilityRegistryService {
return bindings.map((binding) => ({
id: binding.id,
capabilityId: binding.capabilityId,
providerId: binding.providerId,
providerId: binding.providerId ?? undefined,
region: binding.region,
config: binding.config as Record<string, unknown> | undefined,
secretsRef: binding.secretsRef,
@@ -391,7 +407,7 @@ export class CapabilityRegistryService {
capabilityId,
providerId: providerId || null,
region,
config: config || null,
config: config != null ? (config as Prisma.InputJsonValue) : undefined,
status: 'active',
},
});

View File

@@ -0,0 +1,46 @@
/**
* Legacy / rail protocols visible to the quantum translation & compatibility layer.
* Aligns with SolaceNet gateway rails where applicable (see gateway-adapter-registry).
*/
export type LegacyProtocol =
| 'SWIFT'
| 'ISO20022'
| 'ACH'
| 'SEPA'
| 'PRIVATE_BANK'
| 'KTT_EVIDENCE'
| 'TT_ROUTE'
| 'DTC_SETTLEMENT'
| 'DTCC_NSCC'
| 'DTCC_FICC'
| 'SWIFT_GPI'
| 'MOJALOOP'
| 'RTGS'
| 'CARD_NETWORK';
/** Subset used for SolaceNet-aligned scaffolds (non-SWIFT/ACH core banking). */
export const SOLACENET_EXTENDED_PROTOCOLS: readonly LegacyProtocol[] = [
'KTT_EVIDENCE',
'TT_ROUTE',
'DTC_SETTLEMENT',
'DTCC_NSCC',
'DTCC_FICC',
'SWIFT_GPI',
'MOJALOOP',
'RTGS',
'CARD_NETWORK',
] as const;
export function isSolaceNetExtendedProtocol(p: string): p is LegacyProtocol {
return (SOLACENET_EXTENDED_PROTOCOLS as readonly string[]).includes(p);
}
/** All values accepted by quantum translation / compatibility services. */
export const ALL_LEGACY_PROTOCOLS: readonly LegacyProtocol[] = [
'SWIFT',
'ISO20022',
'ACH',
'SEPA',
'PRIVATE_BANK',
...SOLACENET_EXTENDED_PROTOCOLS,
] as const;

View File

@@ -4,10 +4,10 @@
import prisma from '@/shared/database/prisma';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '@/infrastructure/monitoring/logger';
import { ALL_LEGACY_PROTOCOLS, type LegacyProtocol } from './legacy-protocol-types';
export interface CompatibilityCheckRequest {
legacyProtocol: 'SWIFT' | 'ISO20022' | 'ACH' | 'SEPA' | 'PRIVATE_BANK';
legacyProtocol: LegacyProtocol;
transactionData: any;
}
@@ -75,6 +75,22 @@ export class QuantumCompatibilityService {
);
break;
case 'KTT_EVIDENCE':
case 'TT_ROUTE':
case 'DTC_SETTLEMENT':
case 'DTCC_NSCC':
case 'DTCC_FICC':
case 'SWIFT_GPI':
case 'MOJALOOP':
case 'RTGS':
case 'CARD_NETWORK':
compatibilityScore = this.checkSolaceNetRailScaffoldCompatibility(
request.legacyProtocol,
issues,
recommendations
);
break;
default:
issues.push(`Unknown protocol: ${request.legacyProtocol}`);
compatibilityScore = 0;
@@ -90,6 +106,21 @@ export class QuantumCompatibilityService {
};
}
/**
* SolaceNet-aligned rail scaffolds: neutral score until real field contracts exist.
*/
private checkSolaceNetRailScaffoldCompatibility(
protocol: LegacyProtocol,
issues: string[],
recommendations: string[]
): number {
issues.push(`Scaffold protocol mapping for ${protocol}; validate against live rail contract before production.`);
recommendations.push(
'See dbis_core/docs/solacenet/PROTOCOL_GAPS_CHECKLIST.md and gateway-adapter-registry for adapter status.'
);
return 72;
}
/**
* Check SWIFT compatibility
*/
@@ -255,7 +286,7 @@ export class QuantumCompatibilityService {
* List all supported protocols
*/
async listSupportedProtocols(): Promise<string[]> {
return ['SWIFT', 'ISO20022', 'ACH', 'SEPA', 'PRIVATE_BANK'];
return [...ALL_LEGACY_PROTOCOLS];
}
}

View File

@@ -5,10 +5,10 @@ import prisma from '@/shared/database/prisma';
import { Decimal } from '@prisma/client/runtime/library';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '@/infrastructure/monitoring/logger';
import type { LegacyProtocol } from './legacy-protocol-types';
export interface QuantumTranslationRequest {
legacyProtocol: 'SWIFT' | 'ISO20022' | 'ACH' | 'SEPA' | 'PRIVATE_BANK';
legacyProtocol: LegacyProtocol;
transactionData: any;
amount: string;
currencyCode: string;
@@ -158,6 +158,15 @@ export class QuantumTranslationService {
currency: 'currencyCode',
},
},
KTT_EVIDENCE: { messageType: 'KTT', quantumFormat: 'ISO20022', fieldMapping: {} },
TT_ROUTE: { messageType: 'TT', quantumFormat: 'ISO20022', fieldMapping: {} },
DTC_SETTLEMENT: { messageType: 'DTC', quantumFormat: 'ISO20022', fieldMapping: {} },
DTCC_NSCC: { messageType: 'DTCC_NSCC', quantumFormat: 'ISO20022', fieldMapping: {} },
DTCC_FICC: { messageType: 'DTCC_FICC', quantumFormat: 'ISO20022', fieldMapping: {} },
SWIFT_GPI: { messageType: 'SWIFT_GPI', quantumFormat: 'ISO20022', fieldMapping: {} },
MOJALOOP: { messageType: 'MOJALOOP', quantumFormat: 'ISO20022', fieldMapping: {} },
RTGS: { messageType: 'RTGS', quantumFormat: 'ISO20022', fieldMapping: {} },
CARD_NETWORK: { messageType: 'CARD', quantumFormat: 'ISO20022', fieldMapping: {} },
};
return defaultMappings[legacyProtocol] || {};
@@ -205,6 +214,17 @@ export class QuantumTranslationService {
fxRate = new Decimal(0.85); // Example EUR rate
}
break;
case 'KTT_EVIDENCE':
case 'TT_ROUTE':
case 'DTC_SETTLEMENT':
case 'DTCC_NSCC':
case 'DTCC_FICC':
case 'SWIFT_GPI':
case 'MOJALOOP':
case 'RTGS':
case 'CARD_NETWORK':
fxRate = new Decimal(1);
break;
default:
fxRate = new Decimal(1);
}
@@ -250,6 +270,17 @@ export class QuantumTranslationService {
// Private bank has higher risk
riskScore = new Decimal(0.15);
break;
case 'KTT_EVIDENCE':
case 'TT_ROUTE':
case 'DTC_SETTLEMENT':
case 'DTCC_NSCC':
case 'DTCC_FICC':
case 'SWIFT_GPI':
case 'MOJALOOP':
case 'RTGS':
case 'CARD_NETWORK':
riskScore = new Decimal(0.1);
break;
}
// Additional risk factors from transaction data

View File

@@ -147,6 +147,11 @@ import ilcRoutes from '@/core/ledger/ilc/ilc.routes';
const app: Express = express();
// Behind NPM / load balancer: set TRUST_PROXY=1 so rate limits and req.ip use the client address
if (process.env.TRUST_PROXY === '1' || process.env.TRUST_PROXY === 'true') {
app.set('trust proxy', 1);
}
// Security middleware
app.use(helmet());
@@ -220,8 +225,19 @@ const swaggerOptions = {
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Health check endpoint (no auth required)
app.get('/health', async (req, res) => {
// Top-level service metadata so API hostnames return a clean 200 at "/".
app.get('/', (req, res) => {
res.status(200).json({
service: 'dbis-core-banking-system',
status: 'healthy',
version: '1.0.0',
docs: '/api-docs',
health: '/health',
});
});
// Health check endpoints (no auth required)
app.get(['/health', '/v1/health'], async (req, res) => {
const healthStatus: {
status: string;
timestamp: string;
@@ -478,4 +494,3 @@ app.use('/', metricsRoutes);
app.use(errorHandler);
export default app;

View File

@@ -121,3 +121,5 @@ declare module './auth.middleware' {
}
}
/** Default admin gate for IRU routes that import this symbol (see requireAdminPermission for specific checks). */
export const adminPermissionMiddleware = requireAdminPermission(AdminPermission.VIEW_GLOBAL_OVERVIEW);

View File

@@ -52,3 +52,52 @@ export function dynamicRateLimitMiddleware(
limiter(req, res, next);
}
function iruMarketplaceRateLimitNoop(_req: Request, _res: Response, next: NextFunction): void {
next();
}
/** POST /iru/marketplace/inquiries — default 10 / 15m per IP. IRU_MARKETPLACE_RATE_LIMIT_IN_TEST=1 disables in Jest. */
export function createIruMarketplaceInquiryPostLimiter() {
if (process.env.IRU_MARKETPLACE_RATE_LIMIT_IN_TEST === '1') {
return iruMarketplaceRateLimitNoop;
}
const windowMs = parseInt(process.env.IRU_MARKETPLACE_INQUIRY_WINDOW_MS || '900000', 10);
const max = parseInt(process.env.IRU_MARKETPLACE_INQUIRY_MAX || '10', 10);
return rateLimit({
windowMs,
max,
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: `Too many inquiry submissions. Try again after ${Math.ceil(windowMs / 60000)} minutes.`,
},
timestamp: new Date(),
},
});
}
/** Public GETs (offerings, inquiry status, pricing) — default 200 / min per IP */
export function createIruMarketplacePublicReadLimiter() {
if (process.env.IRU_MARKETPLACE_RATE_LIMIT_IN_TEST === '1') {
return iruMarketplaceRateLimitNoop;
}
const windowMs = parseInt(process.env.IRU_MARKETPLACE_PUBLIC_WINDOW_MS || '60000', 10);
const max = parseInt(process.env.IRU_MARKETPLACE_PUBLIC_MAX || '200', 10);
return rateLimit({
windowMs,
max,
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests. Please wait and retry.',
},
timestamp: new Date(),
},
});
}

View File

@@ -46,7 +46,13 @@ export function validateRequest(schema: {
errors,
});
throw new DbisError(ErrorCode.VALIDATION_ERROR, `Validation failed: ${errors.map((e) => e.message).join(', ')}`);
next(
new DbisError(
ErrorCode.VALIDATION_ERROR,
`Validation failed: ${errors.map((e) => e.message).join(', ')}`
)
);
return;
}
next(error);

View File

@@ -0,0 +1,129 @@
// Public IRU marketplace routes only (no inquiry/offering admin services) — safe for focused HTTP tests.
import { Router } from 'express';
import { marketplaceService } from '@/core/iru/marketplace.service';
import { validateRequest, iruValidationSchemas } from '@/integration/api-gateway/middleware/validation.middleware';
import {
createIruMarketplaceInquiryPostLimiter,
createIruMarketplacePublicReadLimiter,
} from '@/integration/api-gateway/middleware/rate-limit.middleware';
import type { InquiryRequest } from '@/core/iru/marketplace.service';
const router = Router();
const iruInquiryPostLimiter = createIruMarketplaceInquiryPostLimiter();
const iruPublicReadLimiter = createIruMarketplacePublicReadLimiter();
router.get('/offerings', iruPublicReadLimiter, async (req, res, next) => {
try {
const filters: Record<string, unknown> = {};
if (req.query.capacityTier) {
filters.capacityTier = parseInt(req.query.capacityTier as string, 10);
}
if (req.query.institutionalType) {
filters.institutionalType = req.query.institutionalType as string;
}
if (req.query.status) {
filters.status = req.query.status as string;
}
const offerings = await marketplaceService.getOfferings(
filters as Parameters<typeof marketplaceService.getOfferings>[0]
);
res.json({
success: true,
data: offerings,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
router.get('/offerings/:offeringId/pricing', iruPublicReadLimiter, async (req, res, next) => {
try {
const { offeringId } = req.params;
let usageProfile: unknown;
if (req.query.usageProfile != null && String(req.query.usageProfile).trim() !== '') {
try {
usageProfile = JSON.parse(req.query.usageProfile as string);
} catch {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_JSON',
message: 'usageProfile query parameter must be valid JSON',
},
timestamp: new Date(),
});
}
}
const pricing = await marketplaceService.calculatePricing(offeringId, usageProfile);
res.json({
success: true,
data: pricing,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
router.get('/offerings/:offeringId', iruPublicReadLimiter, async (req, res, next) => {
try {
const { offeringId } = req.params;
const offering = await marketplaceService.getOfferingById(offeringId);
if (!offering) {
return res.status(404).json({
success: false,
error: {
code: 'NOT_FOUND',
message: `Offering ${offeringId} not found`,
},
timestamp: new Date(),
});
}
res.json({
success: true,
data: offering,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
router.post(
'/inquiries',
iruInquiryPostLimiter,
validateRequest({ body: iruValidationSchemas.inquiryRequest }),
async (req, res, next) => {
try {
const result = await marketplaceService.submitInquiry(req.body as InquiryRequest);
res.status(201).json({
success: true,
data: result,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
}
);
router.get('/inquiries/:inquiryId', iruPublicReadLimiter, async (req, res, next) => {
try {
const { inquiryId } = req.params;
const inquiry = await marketplaceService.getInquiryStatusPublic(inquiryId);
res.json({
success: true,
data: inquiry,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
export default router;

View File

@@ -1,152 +1,20 @@
// IRU Marketplace API Routes
// Routes for Sankofa Phoenix Marketplace
// IRU Marketplace API — public routes + admin (inquiry/offering services).
import { Router } from 'express';
import { marketplaceService } from '@/core/iru/marketplace.service';
import iruMarketplacePublicRoutes from '@/integration/api-gateway/routes/iru-marketplace-public.routes';
import { offeringService } from '@/core/iru/offering.service';
import { inquiryService } from '@/core/iru/inquiry.service';
import { zeroTrustAuthMiddleware } from '@/integration/api-gateway/middleware/auth.middleware';
import { adminPermissionMiddleware } from '@/integration/api-gateway/middleware/admin-permission.middleware';
import { validateRequest, iruValidationSchemas } from '@/integration/api-gateway/middleware/validation.middleware';
const router = Router();
// ============================================================================
// Public Marketplace Routes (No Auth Required)
// ============================================================================
/**
* @route GET /api/v1/iru/marketplace/offerings
* @desc Get all active IRU offerings
* @access Public
*/
router.get('/offerings', async (req, res, next) => {
try {
const filters: any = {};
if (req.query.capacityTier) {
filters.capacityTier = parseInt(req.query.capacityTier as string);
}
if (req.query.institutionalType) {
filters.institutionalType = req.query.institutionalType as string;
}
if (req.query.status) {
filters.status = req.query.status as string;
}
const offerings = await marketplaceService.getOfferings(filters);
res.json({
success: true,
data: offerings,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
/**
* @route GET /api/v1/iru/marketplace/offerings/:offeringId
* @desc Get offering by ID
* @access Public
*/
router.get('/offerings/:offeringId', async (req, res, next) => {
try {
const { offeringId } = req.params;
const offering = await marketplaceService.getOfferingById(offeringId);
if (!offering) {
return res.status(404).json({
success: false,
error: {
code: 'NOT_FOUND',
message: `Offering ${offeringId} not found`,
},
timestamp: new Date(),
});
}
res.json({
success: true,
data: offering,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
/**
* @route POST /api/v1/iru/marketplace/inquiries
* @desc Submit initial inquiry
* @access Public
*/
router.post(
'/inquiries',
validateRequest({ body: iruValidationSchemas.inquiryRequest }),
async (req, res, next) => {
try {
const result = await marketplaceService.submitInquiry(req.body);
res.status(201).json({
success: true,
data: result,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
}
);
/**
* @route GET /api/v1/iru/marketplace/inquiries/:inquiryId
* @desc Get inquiry status
* @access Public (with inquiry ID)
*/
router.get('/inquiries/:inquiryId', async (req, res, next) => {
try {
const { inquiryId } = req.params;
const inquiry = await marketplaceService.getInquiryStatus(inquiryId);
res.json({
success: true,
data: inquiry,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
/**
* @route GET /api/v1/iru/marketplace/offerings/:offeringId/pricing
* @desc Calculate pricing for an offering
* @access Public
*/
router.get('/offerings/:offeringId/pricing', async (req, res, next) => {
try {
const { offeringId } = req.params;
const usageProfile = req.query.usageProfile
? JSON.parse(req.query.usageProfile as string)
: undefined;
const pricing = await marketplaceService.calculatePricing(offeringId, usageProfile);
res.json({
success: true,
data: pricing,
timestamp: new Date(),
});
} catch (error) {
return next(error);
}
});
router.use(iruMarketplacePublicRoutes);
// ============================================================================
// Admin Routes (Auth Required)
// ============================================================================
/**
* @route POST /api/v1/iru/marketplace/admin/offerings
* @desc Create new IRU offering
* @access Admin
*/
router.post(
'/admin/offerings',
zeroTrustAuthMiddleware,
@@ -165,11 +33,6 @@ router.post(
}
);
/**
* @route PUT /api/v1/iru/marketplace/admin/offerings/:offeringId
* @desc Update IRU offering
* @access Admin
*/
router.put(
'/admin/offerings/:offeringId',
zeroTrustAuthMiddleware,
@@ -189,11 +52,6 @@ router.put(
}
);
/**
* @route DELETE /api/v1/iru/marketplace/admin/offerings/:offeringId
* @desc Delete IRU offering
* @access Admin
*/
router.delete(
'/admin/offerings/:offeringId',
zeroTrustAuthMiddleware,
@@ -213,11 +71,6 @@ router.delete(
}
);
/**
* @route GET /api/v1/iru/marketplace/admin/offerings/:offeringId/stats
* @desc Get offering statistics
* @access Admin
*/
router.get(
'/admin/offerings/:offeringId/stats',
zeroTrustAuthMiddleware,
@@ -237,18 +90,13 @@ router.get(
}
);
/**
* @route GET /api/v1/iru/marketplace/admin/inquiries
* @desc Get all inquiries with filters
* @access Admin
*/
router.get(
'/admin/inquiries',
zeroTrustAuthMiddleware,
adminPermissionMiddleware,
async (req, res, next) => {
try {
const filters: any = {};
const filters: Record<string, unknown> = {};
if (req.query.status) {
filters.status = req.query.status as string;
}
@@ -256,13 +104,13 @@ router.get(
filters.offeringId = req.query.offeringId as string;
}
if (req.query.capacityTier) {
filters.capacityTier = parseInt(req.query.capacityTier as string);
filters.capacityTier = parseInt(req.query.capacityTier as string, 10);
}
if (req.query.limit) {
filters.limit = parseInt(req.query.limit as string);
filters.limit = parseInt(req.query.limit as string, 10);
}
if (req.query.offset) {
filters.offset = parseInt(req.query.offset as string);
filters.offset = parseInt(req.query.offset as string, 10);
}
const inquiries = await inquiryService.getInquiries(filters);
@@ -277,11 +125,6 @@ router.get(
}
);
/**
* @route GET /api/v1/iru/marketplace/admin/inquiries/:inquiryId
* @desc Get inquiry by ID with full details
* @access Admin
*/
router.get(
'/admin/inquiries/:inquiryId',
zeroTrustAuthMiddleware,
@@ -301,11 +144,6 @@ router.get(
}
);
/**
* @route POST /api/v1/iru/marketplace/admin/inquiries/:inquiryId/acknowledge
* @desc Acknowledge inquiry
* @access Admin
*/
router.post(
'/admin/inquiries/:inquiryId/acknowledge',
zeroTrustAuthMiddleware,
@@ -325,11 +163,6 @@ router.post(
}
);
/**
* @route PUT /api/v1/iru/marketplace/admin/inquiries/:inquiryId
* @desc Update inquiry
* @access Admin
*/
router.put(
'/admin/inquiries/:inquiryId',
zeroTrustAuthMiddleware,