Swift Sentinel is the Spring Boot backend in this repo: it ingests SWIFT FIN MT and ISO 20022 (MX / CBPR-style) traffic, validates and enriches it, transforms it through versioned mapping profiles, routes outcomes through governance and repair, and serves operator REST APIs plus the embedded dashboard SPA at /dashboard/app/.
REST intake, optional file hot-folder, and optional MQ adapters all converge on IntakeOrchestrationService, so the same orchestration handles messages regardless of how they arrive. HTTP entry points optionally wrap expensive work behind ComputeExecutionService for compute governance—see IntakeController.
End-to-end flow is ingress → classify → validation → corridor-specific enrichment/transform → governance tail. Validation is deterministic rule + fact pipelines; governance is repair queue, approvals, lifecycle ledgers, and optional settlement-graph hooks where the corridor demands it.
flowchart TB
subgraph ingress ["Ingress channels"]
ch[REST / file inbox / MQ]
ch --> track[Ingestion tracking]
track --> orch[IntakeOrchestrationService]
end
subgraph shared ["Shared stages"]
httpwrap[ComputeExecutionService on REST only]
orch --> sniff[classify FIN block 2 or MX hint]
sniff --> mtval[MtValidationEngine + corridor context]
end
subgraph byfamily ["Branch by MT family"]
mtval --> pay[MT103 / MT202 settlement + CBPR transforms]
mtval --> ini[MT101 / MT102 batch initiation]
mtval --> st[MT940 / MT950 statements]
mtval --> free[MT199 / MT299 / MT999 ops free-format]
end
subgraph tail ["Governance tail"]
pay --> gov[Compliance + policy overlays]
ini --> gov
st --> gov
free --> gov
gov --> done[Accepted / repair / rejected + ledgers]
end
Legend: Ingress includes correlation via ingestion events where the channel participates. Paths after branching still share compliance, ComplianceGateService, enforcement, and OperatorGovernanceService semantics; transforms for MX emission are TransformService plus MappingProfileCatalog (not every MT family produces full CBPR payloads on every outcome).
Thin HTTP sketch (not the full macro flow above):
flowchart LR
IC[IntakeController] --> CE[ComputeExecutionService]
CE --> IO[IntakeOrchestrationService]
This is the vertical decision stack inside validation (orthogonal to “which MT family?” in the previous diagram).
- Issue tiers —
ValidationTierclassifies each finding as STRUCTURAL (parser / shape integrity), SEMANTIC (financial meaning), or COMPLIANCE (policy).ValidationDispatchPolicyexplains how semantic and compliance issues relate to suppressing financial XML / MX dispatch, withPolicyEnforcerproducing L5 dispatch decisions (PolicyEnforcer.java). See alsodocs/adrs/001-transform-envelope-authority-contract.mdand the OpenAPI sketch atopenapi.json. - Structural vs semantic stages inside L2 —
MtValidationRulePipelinerunsSTRUCTURALrules on raw/block4 context vsSEMANTICmultiplexed rules on parsed payloads.MtValidationEngineis the L2 orchestrator and keeps those families separated. - PRE-IR → semantic IR —
PreIrGateResultgates whetherMtValidationEngine.attachPreIrAndSemanticIrmay attachMtSemanticIrBundle. On reject, semantic IR stays off theMtValidationContext; on allow, the bundle may be populated. - Semantic IR artifact — MT semantic intermediate representation is built by the sibling
swiftbridge-mt-semanticlibrary (MtSemanticIrBuilder,MtSemanticIrBundle; packagecom.swiftbridge.mt.semantic). Maven buildsbank-core-javathrough the reactor parent so that module resolves locally (see repopom.xml). - L0 / ladder —
MtValidationContextdocuments execution mode driving L0 (XSD) and L1–L3 pipeline activation**. L3 derives the financial slice;ValidationEnforcergoverns whether blocking L2 outcomes abort downstream L4 construction (MtValidationEnginejavadoc). L5 is policy enforcement / dispatch verdict. Exact activation varies by message type and engine mode. - Corridor lens —
CorridorResolver+CbprProfileResolverinterpret rails / CBPR context layered onMtValidationContext.
Validation stack sketch:
flowchart TB
subgraph tiers ["Finding taxonomy"]
STRUCTURAL --> SEMANTIC
SEMANTIC --> COMPLIANCE
end
PRE_IR[PRE-IR gate] --> L2[L2 rule pipeline]
L2 --> SEM_IR[Semantic IR bundle optional]
SEM_IR --> L3[L3 financial model]
L3 --> L4[L4 downstream build gated by ValidationEnforcer]
L4 --> L5[PolicyEnforcer L5 dispatch]
Enums and routing use FIN block 2 wire identifiers. Supported types appear in MtMessageType: MtMessageType.java
| Enum | Wire (block 2) |
|---|---|
| MT101 | 101 |
| MT102 | 102 |
| MT103 | 103 |
| MT202 | 202 |
| MT199 | 199 |
| MT299 | 299 |
| MT999 | 999 |
| MT940 | 940 |
| MT950 | 950 |
Transform routes registered in MappingProfileCatalog (non-exhaustive summary; authoritative list lives in MappingProfileCatalog.java):
| Direction / route | ISO message (schema id) |
|---|---|
| MT103 → MX | pacs.008.001.08 |
| MT202 → MX | pacs.009.001.08 |
| MT101 / MT102 → MX | pain.001.001.09 |
| MT940 / MT950 ↔ MX | camt.053.001.08 |
Reverse MX→MT mappings are likewise catalogued (e.g. pacs.008.001.08→MT103).
Reality check: initiation, payment, statement, and free-format corridors do not all share identical intake branches—consult IntakeOrchestrationService and transform services before assuming parity.
Implements the layers described above—this table maps folders to intent (names stay SwiftBridge-prefixed in bytecode).
| Area | Role |
|---|---|
api |
REST adapters (@RestController), ApiProblems, ApiObservabilityExceptionHandler |
config |
Spring beans, SecurityConfig, @ConfigurationProperties |
domain |
Records / domain payloads (intake, governance, validation reports) isolated from controllers |
service |
Stateless @Service domains: intake, transform, validation, policy, governance, intelligence, replay, etc. |
canonical |
Syntax-independent canonical model (see package-info.java; purity enforced via ArchUnit CanonicalPurityArchitectureTest) |
security |
JWT decoding, IAM admission PAT filter, JDBC identity repositories |
observability |
SafeLoggerFactory, forensic crypto properties, retention |
compute/runtime |
Compute boundary helpers used by controllers |
ui |
SPA shell / dashboard routing controllers |
Additional architecture regression suites live under src/test/java/com/swiftbridge/core/architecture.
Configured in SecurityConfig: SecurityConfig.java
- Spring Security 6 with
@EnableMethodSecuritywhere service methods declare pre/post checks. - CSRF disabled on this chain—clients authenticate with bearer tokens rather than cookie sessions (browser CSRF defenses are layered at the SPA / gateway when applicable).
- CORS via
corsConfigurationSource: sane localhost defaults plus optional patterns / permissive toggle for tunnels. - Authentication
- OAuth2 Resource Server JWT bearer validation with a custom JWT→**
GrantedAuthority** converter (roles,realm_access.rolesfor Keycloak-shaped tokens). IamAdmissionAuthenticationEntryPointon the JWT authentication entry point for IAM-friendly error surfaces.PersonalAccessTokenAuthenticationFilterruns beforeBearerTokenAuthenticationFilter(PersonalAccessTokenAuthenticationFilter.java).TenantContextFilterinstalls tenant scope beforeAuthorizationFilter.
- OAuth2 Resource Server JWT bearer validation with a custom JWT→**
- Authorization: explicit
authorizeHttpRequestsmatchers per REST path verb; dashboards static assets/dashboard/app/assets/**areWebSecurityCustomizer#ignoringso ES module loads are not intercepted by Bearer filters (SecurityConfig.java** comments). - Passwords:
BCryptPasswordEncoderbean (SecurityConfig). Signup/login viaAuthApiControllerpersists bcrypt hashes (AuthApiController.java).
Important: application.yml documents that JWT login flows use POST /api/v1/auth/login. HTTP Basic is not wired on this resource-server filter chain—call APIs with Authorization: Bearer <token> (login-issued JWT, enterprise issuer, PAT, etc.). The spring.security.user.*/APP_BASIC_* settings still populate the UserDetailsService bean backed by InMemoryUserDetailsManager (SecurityConfig) for Spring conventions, but do not imply Basic-auth API access.
flowchart LR
Client[Client] --> PAT[PersonalAccessToken filter]
PAT --> Jwt[OAuth2 JWT resource server]
Jwt --> Tenant[TenantContext filter]
Tenant --> AuthZ[authorizeHttpRequests]
| Concern | Implementation |
|---|---|
| TLS | Terminate HTTPS at ingress (reverse proxy, service mesh)—not mandated inside Boot itself. |
| Forensic symmetric seal | ForensicCryptoService AES-256-GCM (AES/GCM/NoPadding) with random IVs; gated on bank.observability.forensic.aes-key-base64 (ForensicCryptoService.java / BankObservabilityProperties) |
| Evidence packs metadata | EvidencePackProperties default encryptionStandard = AES-256-GCM (EvidencePackProperties.java) |
Operational ownership of KMS / key material stays with your deployment process.
SafeLoggerFactory/SafeLoggerprovide redaction-conscious logging hooks used by ingestion and ledger pathways (e.g.MessageLifecycleLedgerServiceconstructs loggers via the factory (SafeLoggerFactory.java).- Prefer SLF4J parameterized APIs everywhere else (for example
logger.info("processed {}", correlationId)). - Expose actuator health/prometheus per
application.yml;/api/v1/system/healthand actuator health matchers stay permit-all inSecurityConfig.
-
Constructor injection on controllers/services (see
IntakeControllerpattern). -
@EnableAsync,@EnableScheduling, and@EnableConfigurationPropertiesonBankCoreApplication(BankCoreApplication.java). -
Bean Validation (
@ValidDTOs) on auth endpoints (AuthApiController). -
Centralized RFC7807-ish problems:
ApiObservabilityExceptionHandlerplusApiProblems. -
Flyway-managed schema boots with the app (
spring.flyway.enabled: trueinapplication.yml). -
Testcontainers-backed integration suites locally (Docker required) plus
com.swiftbridge.core.architecture.*regression tests guarding boundaries (canonical purity, corridors, governance). -
Canonical package intent (syntax-neutral financial entities, no sideways service imports) summarized in
canonical/package-info.java. -
Further reading:
docs/adrs/001-transform-envelope-authority-contract.md(transform envelope authority).
- Docker (for PostgreSQL)
- Java 21
cd bank-core-java
docker compose up -dMaven must use the repo-root reactor (../pom.xml) so sibling swiftbridge-mt-semantic builds and resolves locally (that artifact is not on Maven Central).
cd bank-core-java
./mvnw -f ../pom.xml -pl bank-core-java spring-boot:run- Prefer
POST /api/v1/auth/loginJSON credentials or your enterprise JWT issuer (SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI/JWK_SET_URI). - Or mint local HMAC dev tokens (
APP_JWT_SECRETper below).
Development-only defaults mirrored in application.yml (spring.security.user) populate UserDetailsService but remember: API calls still need Bearer JWTs, not Basic auth.
Overrides:
APP_BASIC_USER/APP_BASIC_PASSWORD/APP_BASIC_ROLES(mapped intospring.security.user+bank.security.local-basic-roles; seeapplication.yml)
- Resource server support is enabled.
- For enterprise IdP, set:
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI- or
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI
- Role claims accepted:
roles(array)realm_access.roles(Keycloak style)
- Roles are mapped to Spring authorities as
ROLE_<UPPERCASE_ROLE>.
- Local decoder uses
APP_JWT_SECRET. - Example token generation:
node -e "const c=require('crypto');const h=(b)=>Buffer.from(b).toString('base64url');const s=process.env.APP_JWT_SECRET||'swift-bridge-local-hmac-secret-change-me-32chars';const header=h(JSON.stringify({alg:'HS256',typ:'JWT'}));const payload=h(JSON.stringify({sub:'ops-user-1',roles:['admin','operator'],exp:Math.floor(Date.now()/1000)+3600}));const sig=c.createHmac('sha256',s).update(header+'.'+payload).digest('base64url');console.log(header+'.'+payload+'.'+sig);"- Use with:
curl -H "Authorization: Bearer <token>" http://127.0.0.1:8088/api/v1/intake/metricscd bank-core-java
./mvnw -f ../pom.xml testIntegration tests use Testcontainers and require Docker running locally.
Run only the scripted E2E bank simulation (File + MQ + repair + maker-checker + replay + dispatch):
cd bank-core-java
./mvnw -f ../pom.xml -Dtest=BankSimulationE2ETest testcd bank-core-java && docker compose up -d && ./mvnw -f ../pom.xml test && ./mvnw -f ../pom.xml -pl bank-core-java spring-boot:runcd bank-core-java
cp .env.prod.example .env.prod
# Edit .env.prod with real secrets and issuer settings
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --buildStop:
docker compose --env-file .env.prod -f docker-compose.prod.yml down- Health:
http://127.0.0.1:8088/api/v1/system/health - Intake process:
POST /api/v1/intake/process - Intake metrics:
GET /api/v1/intake/metrics - Ingestion events:
GET /api/v1/intake/ingestion/events - Ingestion stats:
GET /api/v1/intake/ingestion/stats - Lifecycle ledger events:
GET /api/v1/intake/lifecycle/events - Operational alerts:
GET /api/v1/intake/alerts - Ingestion controls read:
GET /api/v1/intake/controls - Ingestion controls update:
POST /api/v1/intake/controls - MQ harness status:
GET /api/v1/intake/mq/status - MQ harness process:
POST /api/v1/intake/mq/smoke/process - MQ harness publish:
POST /api/v1/intake/mq/smoke/publish - MQ harness DLQ replay:
POST /api/v1/intake/mq/smoke/replay-dlq - Evidence pack generate:
POST /api/v1/governance/evidence-pack/generate - Evidence pack latest:
GET /api/v1/governance/evidence-pack/latest - Evidence pack history:
GET /api/v1/governance/evidence-pack/history - Dashboard playground:
GET /dashboard/playground(paste MT and dry-run conversion + issues) - Transform envelope authority (ADR): docs/adrs/001-transform-envelope-authority-contract.md —
decisionCorelatch;issueRegistrydiagnostics;validationReportL2 trace; OpenAPI sketch in /contracts/openapi/openapi.json
Runbook:
docs/runbooks/audit-evidence-pack.mddocs/runbooks/e2e-bank-simulation.md- Governance cases:
GET /api/v1/governance/repair-cases - Mapping profile catalog:
GET /api/v1/transform/profiles - Operator UI (production):
http://127.0.0.1:8088/dashboard/app/(Spring Boot, React SPA atdashboard-ui/). Rebuild the SPA intosrc/main/resources/static/dashboard/app/: runpnpm installat the repository root (pnpm workspace), then./mvnw -f ../pom.xml -pl bank-core-java generate-resources -P dashboard-ui(or package with that profile). - Legacy operator UI (Node static, archived):
cd legacy/typescript-mvp && npm install && npm run devthenhttp://127.0.0.1:3847/ops.html(deprecation banner displayed; set API base tohttp://127.0.0.1:8088inlegacy/typescript-mvp/public/ops.config.js). Retained only so existing bookmarks keep resolving — slated for removal at Phase 8 sign-off.
The backend can poll a local inbox directory and auto-route messages through intake.
Enable with env vars:
export BANK_INGESTION_FILE_ENABLED=true
export BANK_INGESTION_FILE_INBOX=./data/ingestion/inbox
export BANK_INGESTION_FILE_ARCHIVE=./data/ingestion/archive
export BANK_INGESTION_FILE_ERROR=./data/ingestion/error
export BANK_INGESTION_FILE_POLL_MS=5000Then run:
cd bank-core-java
./mvnw -f ../pom.xml -pl bank-core-java spring-boot:runDrop MT/MX payload files into the inbox directory. Processed files are moved to archive (accepted) or error (repair/rejected/failed).
The MQ adapter is wired through Spring JMS and shares the same intake and tracking pipeline.
Enable and configure:
export BANK_INGESTION_MQ_ENABLED=true
export BANK_INGESTION_MQ_INBOUND_QUEUE=SWIFT.IN
export BANK_INGESTION_MQ_DLQ=SWIFT.DLQ
export BANK_INGESTION_MQ_MAX_ATTEMPTS=3
export BANK_INGESTION_MQ_BACKOFF_MS=1000
export BANK_INGESTION_MQ_HARNESS_ENABLED=true
export IBM_MQ_QUEUE_MANAGER=QM1
export IBM_MQ_CHANNEL=DEV.APP.SVRCONN
export IBM_MQ_CONN_NAME=127.0.0.1(1414)
export IBM_MQ_USER=app
export IBM_MQ_PASSWORD=change-meNotes:
- IBM MQ Spring starter auto-provides JMS
ConnectionFactorywhen IBM MQ properties are set. - On repeated processing failure, payload is published to DLQ with source reference and reason header text.
- Transform endpoints accept
?profile=v1or?profile=v1.1(default isv1.1). v1vsv1.1affects MT103 ingest most visibly:v1.1uses Prowide (ProwideMt103Parser→SwiftMessage.parse).v1keeps the legacy hand-extracted tags path inTransformService(extractTag/ substring on:32A:) for backwards compatibility—not Prowide.- Other Prowide-backed FIN families (profile independent in code paths shown):
- MT202 →
ProwideMt202Parser. - MT101 / MT102 initiation →
ProwideMt101102Parser(after PRE-IR allows lexical ingress).
- MT202 →
- MT940 / MT950 statements use deterministic in-house parsers (block‑4 extraction,
:61:line lexer, statement canonical build inTransformService/Mt940Field61LineParser, etc.)—not the ProwideSwiftMessagefaçade used for 103/202/101/102 canonical projection. - Deterministic mapping errors are returned as JSON:
errorCodefieldmessage