Galanthus Architecture Reference
This document is the stable internal code map for Galanthus. It is meant for maintainers and LLM agents that need to find the current source of truth before changing code.
Public API and command details live elsewhere:
- CLI reference: reference/index.md
- C ABI and library reference: library/index.md
- Binding references: bindings/index.md
- Generated artifact policy: generated_artifact_policy.md
- Runtime follow-up semantics: runtime_common_follow_up_semantics.md
- Scripted transport gate: test_transport_gate.md
Architecture Shape
Galanthus has three main boundaries:
- The domain/client/runtime layer owns typed banking workflows.
- The C ABI projects those workflows into stable C handles, descriptors, and generated metadata.
- CLI, C++ wrappers, and language bindings adapt the C ABI for callers.
Protocol implementations stay below the client layer:
- FinTS uses
src/galanthus/protocol,src/galanthus/wire,src/galanthus/camt, andsrc/galanthus/mt940. - EBICS uses
src/galanthus/ebics, including the envelope, protocol, crypto, and domain subtrees plus root-level status formatting. - REST/plugin-backed providers use
src/galanthus/rest,src/galanthus/plugin,src/galanthus/plugin_revolut, andsrc/galanthus/plugin_wise.
The CLI and bindings must not duplicate banking workflow rules. When a caller needs behavior that is awkward through the public C++ wrapper, fix the C API and wrapper boundary rather than bypassing it.
Code Map
Operation Model
src/galanthus/operation/model.hpp is the dependency-neutral operation source
of truth. It owns:
- stable operation ids
- backend family support bits
- result delivery kind
- result kind
- lifecycle flags
- FinTS capability candidate metadata
- CLI labels and continuation save stems
src/galanthus/c_api/operation_descriptor.hpp projects that model into C ABI
operation descriptors and keeps enum ids aligned with gln_capi.h through
static assertions. Operation metadata consumers should read this projection
instead of copying operation tables.
Client Layer
src/galanthus/client is the workflow facade. It owns:
- public typed client requests/results in
client.hppand shared DTO headers - backend resolution and operation dispatch
- continuation token construction and parsing
- read operation orchestration in
client_readonly.cppandclient/internal/readonly_operations.hpp - write operation orchestration in
client_payments.cpp,client_direct_debit.cpp,client_standing_orders.cpp, andclient/internal/payment_operations.hpp - result mapping and interruption/follow-up conversion
The client layer is the normal place for adding banking behavior that should be shared by CLI, C ABI, and wrappers.
Runtime Common Layer
src/galanthus/runtime contains backend-neutral helpers:
common_backend.hppandcommon_factory.*create runtime backend adapters.common_outcome.hppcarries normalized statuses and follow-up-required values.fints_common_adapter.cppandebics_common_adapter.cppadapt protocol families into runtime/common outcomes.resume_envelope.*wraps backend continuation payloads for persistence through the public continuation-store interface.
Runtime helpers copy backend-provided metadata. They do not invent provider
workflow guidance; that rule is defined in
runtime_common_follow_up_semantics.md.
C ABI Layer
src/galanthus/c_api owns the public C surface:
gln_capi.his the public declaration header.base.cpphandles runtime init/shutdown, errors, strings, buffers, and version helpers.backend_vtable.cppand backend-specific C API files dispatch operations through client/runtime backends.operation_descriptor.hppprojects operation metadata.request_descriptor.hppmaps C request structs into typed client/protocol request objects.result_descriptor.hpp,typed_populate.cpp, andbackend_result.cpppopulate typed result handles.handle_owner.hppprovides internal RAII wrappers for C handles while C API entrypoints still expose explicit destroy/release functions.
Generated ABI metadata and wrapper projections are governed by
generated_artifact_policy.md.
CLI Layer
cli/main.cpp builds the CLI11 command tree. Command implementations live in:
cli/commands.cppfor local profile/config operationscli/commands_capi.cppfor banking operations that go through the C ABIcli/json_output.cppfor schema-v2 JSON envelopescli/profile_registry.cppfor profile resolutioncli/secret_input.cppfor PIN/TAN input
CLI behavior is covered by contract tests and snapshot tests. CLI command names, visible labels, and operation metadata must stay descriptor-backed.
FinTS Protocol
src/galanthus/protocol owns FinTS protocol behavior:
bootstrap.*,bpd_parser.*, andbackend.*parse capability data and resolve protocol-family support.auth.*andauth_helpers.*build authenticated exchanges and TAN-aware sessions.readonly.*builds and parses account, balance, transaction, holding, TAN media, and standing-order reads.transfer.*,transfer_*, andfints41_*build and parse payments, standing orders, direct debits, VoP, and FinTS 4.1 XML flows.resume.*serializes FinTS resume artifacts.operation_capability.*projects operation capability from BPD/descriptor data.
src/galanthus/wire is the low-level FinTS message representation and parser.
src/galanthus/camt and src/galanthus/mt940 parse statement payloads.
Banking Transport
src/galanthus/transport owns the narrow HTTPS transport seam used by FinTS
and EBICS protocol exchanges:
transport.hppdeclarestransport::Transport.https_transport.cppimplements the libcurl-backed POST transport.
REST-like provider HTTP behavior belongs under src/galanthus/rest; do not
route plugin or REST provider behavior through the banking transport seam.
EBICS Protocol
src/galanthus/ebics owns EBICS behavior:
client.*is the EBICS client facade.protocol/*owns HEV, key-management, HPB/HTD/BTD download, and BTU upload exchange lifecycles.envelope/*owns XML request/response envelopes, signature envelopes, namespace-aware lookup, shared static/mutable header helpers, BTF service writers, and OrderData writers.crypto/*owns EBICS RSA key storage, signing, certificates, AES-CBC helpers, key hashes, and file-backed key stores.domain/*,result.hpp, andstatus_format.*carry EBICS data and status models.
EBICS XML helpers are internal C++ helpers. Changes there normally do not imply C ABI or binding updates unless request/result shapes, operation contracts, or generated metadata change.
Plugin And REST Providers
src/galanthus/rest owns HTTP client and token-store primitives for REST-like
providers. src/galanthus/plugin owns host-side plugin loading, approval,
trust checks, host API implementation, call guards, and plugin-backed adapter
logic. src/galanthus/plugin_sdk is the public plugin ABI consumed by plugins.
Provider implementations:
src/galanthus/plugin_revolutsrc/galanthus/plugin_wise
Built-in plugin DLLs are installed with manifests copied by CMake. The plugin
DLLs do not link the full galanthus_lib; shared token blob code is compiled
through object libraries where the host and plugin must agree on storage shape.
Storage, Secrets, And Platform
src/galanthus/storage owns durable encrypted files, file locking, protected
keys, local caches, and atomic writes. src/galanthus/domain/secure_string.hpp,
secure_erase.hpp, and secure_erase_guard.hpp own in-memory secret erasure
helpers. src/galanthus/platform owns secure random and environment helpers.
Build, Scripts, And Generated Outputs
CMakeLists.txt defines the core library, C ABI library, CLI, plugins, and
tool targets. tests/CMakeLists.txt defines test executables, CTest entries,
generated-artifact checks, binding checks, and docs reference rendering.
Generated docs, ABI metadata, and binding projections are source-controlled only
when generated_artifact_policy.md says they are committed artifacts.
Adding A Banking Operation
Use this path when adding a real banking operation or changing an operation contract.
-
Define the operation in
src/galanthus/operation/model.hpp. Add the operation id, backend support, result delivery, result kind, lifecycle flags, CLI labels, continuation stem, and capability candidates. -
Add protocol/backend behavior. For FinTS, add builders/parsers in
src/galanthus/protocoland update capability projection if needed. For EBICS, add exchange code undersrc/galanthus/ebics/protocoland envelope code undersrc/galanthus/ebics/envelope. For plugin providers, add or extend host adapter support undersrc/galanthus/plugin. -
Add typed client request/result handling. Put shared request/result DTOs in
src/galanthus/clientheaders. Add runner code in the relevant client implementation file or private internal helper header. The client runner is responsible for producing a typedclient::Outcome<T>, including interruption, follow-up, direct output, and rejection mapping. -
Add C ABI request and result projection when the operation is public. Update
gln_capi.h, then reflect the shape inrequest_descriptor.hpp,result_descriptor.hpp,backend_result.cpp,typed_populate.cpp, default helpers, accessors, and C API tests. Public ABI changes must be reflected in generated metadata and bindings in the same slice. -
Update CLI and bindings only through the contract source of truth. CLI command metadata should come from operation descriptors. C++ wrapper changes belong under
bindings/cpp/include/galanthus/capi/; other language binding generated files are updated through their generator/check targets. -
Update generated artifacts if their source of truth changed. Consult
generated_artifact_policy.mdbefore committing any generated file. Typical gates include ABI manifest checks, generated C API schema checks, library function docs checks, and binding generator checks. -
Add focused tests at every changed boundary. Use operation-model tests for descriptor projection, protocol tests for request/response behavior, client tests for workflow outcomes, C API tests for handle/result contracts, CLI tests for command behavior, and binding tests for wrapper reflection.
Do not add compatibility shims for an unreleased shape. If an old path is replaced, delete it in the same slice.
Read And Write Lifecycle
FinTS Read Operations
- A public caller invokes CLI, C++ wrapper, another binding, or raw C ABI.
- The C ABI maps request structs into typed client requests with request descriptors.
- The client resolves the backend from state and opens or reuses the FinTS runtime backend.
- Protocol code bootstraps/authenticates, builds an authenticated request, and
sends it through
transport::Transport. wire::parse_message()and protocol parsers extract statuses and payloads.- The client maps successful domain rows to typed results, maps rejections to failures, or creates continuations for TAN/pagination.
- The C ABI returns a
gln_backend_result_t; wrappers borrow views from that result until it is destroyed.
Pagination is a continuation-capable lifecycle. The operation descriptor must mark pagination, and tests must cover both first page and resume/page-token behavior when the operation supports it.
FinTS Write Operations
Write operations follow the same entry path, then add:
- payment/request validation before protocol serialization
- optional dry-run behavior where the operation model marks it
- TAN interruption support
- VoP confirmation support for operations that can require confirmation
- provider statuses and warning propagation
If the bank requires TAN or VoP confirmation, the client returns an action-required outcome with a continuation token. The caller resumes the saved continuation through the same backend result envelope rather than calling a protocol function directly.
EBICS Operations
EBICS setup and operational calls use src/galanthus/ebics/client.* and
protocol exchange helpers. Envelope code builds namespace-correct XML request
bodies, signs authenticated envelopes, parses response envelopes, and verifies
bank or subscriber signatures where required.
EBICS key-management operations may return follow-up-required outcomes when bank keys are downloaded but not yet trusted. That is not a resumable continuation; it is a typed result with manual follow-up metadata.
Plugin Operations
Plugin-backed operations enter through the C ABI/client runtime path, load an approved plugin, validate capabilities against its vtable, call through guarded host-side adapters, and map plugin JSON or typed DTOs into client outcomes. Plugin exceptions and plugin-reported errors are normalized into client failures before they reach the C ABI.
Continuation And Resume State Machine
Continuations represent unfinished operations. Follow-up-required results do not use this state machine.
States
successThe operation finished and owns a typed result or direct output.rejectedThe provider returned a structured rejection.action_requiredThe operation is blocked on TAN, decoupled TAN polling, VoP confirmation, or another resumable action. The result envelope owns a continuation handle.resumedA caller provides continuation input and the stored token payload is parsed back into the operation-specific resume artifact.failedThe continuation is malformed, stale, mismatched, unsupported, or rejected by the provider.
Artifacts
Protocol-level FinTS continuations use protocol::Resume_artifact in
src/galanthus/protocol/resume.*. The artifact stores the operation command,
payload kind, stage, dialog counters, BPD/UPD snapshot, TAN data, command args,
pagination index, accumulated JSON, and optional VoP state.
Client continuations in src/galanthus/client/client_continuation.cpp wrap that
payload with GLCT0001 metadata from k_client_continuation_magic. The wrapper
metadata must match the inner artifact command and payload kind, otherwise
parsing fails.
Runtime continuation stores use runtime::Resume_envelope in
src/galanthus/runtime/resume_envelope.*. The envelope has schema version 1,
backend identity, action type, command, created-at timestamp, and base64 backend
payload. Loading rejects bad magic, bad schema, future timestamps, stale
timestamps, invalid base64, and path traversal outside the store directory.
The C ABI exposes continuations as gln_continuation_t handles and
continuation stores. Public callers save, load, inspect, describe, resume, and
remove continuations through C ABI functions; they must not parse internal
artifact bytes.
C ABI Ownership And Lifetime
The C ABI is explicit about ownership:
- Handles created by
gln_open_*,gln_create_*, or result-taking functions must be destroyed with their matchinggln_destroy_*function. - Strings returned as owned outputs must be released with
gln_release_string. - Buffers returned as owned outputs must be released with
gln_release_buffer. gln_error_tslots are caller-allocated and reset bygln_release_error; setting a new error clears the previous contents first.- Result payload pointers returned from
gln_get_*functions are borrowed from the owning result handle. They become invalid when the result handle is destroyed. gln_secret_towns copied secret bytes. Internal code may borrow astd::string_viewonly for the duration of a call throughgln_secret_borrow_internal.gln_backend_result_towns typed result handles, continuation handles, interrupt details, provider statuses, warnings, and attached error details.
Internal C API code should use c_api::Handle_owner when it temporarily owns a
public handle before returning or destroying it. The generated C++ wrapper uses
galanthus::capi::Owner<T> and generated ownership traits; application code
should prefer that wrapper over raw gln_* ownership calls.
Any public ABI change must update gln_capi.h, expected symbols, ABI manifest,
generated docs, and affected bindings in the same change.
Plugin Trust Threat Model
Plugins are native code and must be treated as privileged once loaded. The host therefore focuses on preventing accidental or unauthorized loading rather than pretending an already-loaded plugin is sandboxed.
Threats covered by the current design:
- loading a plugin outside configured roots
- loading a plugin whose binary or manifest changed after approval
- loading a plugin with undeclared or unsupported capability bits
- accepting a vtable that does not match declared capabilities
- loading a plugin with private native dependencies from the staged bundle
- native-loader environment overrides such as
LD_*,DYLD_*, orGLIBC_TUNABLES - staged-bundle time-of-check/time-of-use changes
- stale temporary staging directories
- plugin exceptions crossing into host control flow
Controls:
plugin_loader.cppcanonicalizes plugin paths and requires the resolved path to stay inside a configured plugin root.- Plugin manifests are strict JSON objects with exactly the expected fields and supported schema versions.
plugin_validation.cppverifies capability bits and vtable/capability parity.plugin_trust.cppstages plugin bundles, hashes binary/manifest/dependency content, writes pin-store records, verifies pins on load, restricts staging permissions, and removes stale staging directories.plugin_native_dep_verifier.cpprejects native dependencies outside the host allowlist.plugin_call_guard.cppcatches plugin exceptions and normalizes them to client failures.
Approving a plugin is explicit. Loading must not create a pin implicitly.
Storage, Durability, And Secret Invariants
Storage invariants apply to state, continuation envelopes, local caches, token stores, protected sidecar keys, and EBICS key blobs.
- Persisted secret-bearing payload blobs are encrypted with AES-256-GCM through
storage::pack_encryptedand authenticated on load throughstorage::unpack_versioned. - Protected sidecar keys are purpose-scoped key material rather than
pack_encryptedpayloads. On Windows they are protected with DPAPI (CryptProtectData/CryptUnprotectData, current-user scope), and theProtected_key_purposeis bound into both the protected-file header and the DPAPI entropy so a key minted for one purpose does not unprotect under another. On POSIX they are raw, unencrypted 32-byte key files that must be owner-readable and must have no group or world permission bits; the loader refuses to read a key file whose mode exposes any group/world bit. POSIX raw key files give no cryptographic protection of the key itself beyond file permissions. - Versioned magic strings distinguish payload families. Current examples
include
GALANTHUS_FINTS_STATE,GALANTHUS_RESUME,GALANTHUS_ENVELOPE_V1,GALANTHUS_EBICS_KEYS, token-store magic, and local cache magic. - Compressed payloads are bounded by
k_max_decompressed_sizeandk_max_file_size. - Storage callers pass
Protected_key_purposewhen loading or creating keys. The per-purpose resolver (resolve_key_path->purpose_key_filename) is reached only by the three stores that pass aNULLoverride down to it, and for those it derives the default sidecar filename next to the payload file (FINTS_STATE->.galanthus_fints_state.key,REST_TOKEN_STORE->.galanthus_rest_token_store.key,CAPI_TOKEN_STORE->.galanthus_capi_token_store.key). Windows protected key files additionally bind the purpose into the protected-file header and DPAPI entropy. POSIX raw key files are not self-tagged, so purpose separation there relies on the distinct resolved paths plus owner-only permissions. - The other five file-backed stores construct their own override key path and
pass it down, so a
NULLargument yields the store's own default rather than the matchingpurpose_key_filenameentry: the local cache uses<cache_path>.key, the file-backed continuation store uses<directory>/.galanthus_continuations.key, the runtime resume-envelope continuation store uses<key_source_path>.resume.key, the file-backed EBICS key store uses<key_blob_path>.sidecar.key, and the plugin secret blob uses<blob_path>.key. Theirpurpose_key_filenameentries (.galanthus_local_cache.key,.galanthus_resume_envelope.key,.galanthus_ebics_key_store.key,.galanthus_plugin_secret_blob.key) keep the resolver total over the purpose enum but are never the in-use sidecar path for those stores. - Writes go through owner-only durable file helpers and atomic replacement where the storage path requires it.
File_lockserializes access with an OS-backed lock file plus an in-process guard keyed by the canonical target path.- Local cache payload and key paths must be distinct.
- EBICS key stores lock the key blob while loading, generating, saving, resetting, staging pending bank keys, and promoting trusted bank keys.
- Secret vectors and secure strings are wiped through
secure_erase,Secure_string, andsecure_erase_guardwhen ownership leaves scope or an error path exits early.
Sidecar key protection separates local principals; it does not defend a payload against an actor who can already read both halves. The encrypted payload and its sidecar key together are sufficient to recover the plaintext, so a local actor who can read both the encrypted state/token/cache/key-blob file and its sidecar key file can decrypt the payload. On Windows, DPAPI ties unprotection to the current user account, so a different local user who copies both files cannot unprotect the key; on POSIX, the owner-only file permissions are the entire boundary, and any principal that can read both files can decrypt. Galanthus does not claim protection against an actor with full local read of both files.
This has a direct backup implication: a backup that captures both the encrypted payload files and the sidecar key files exposes the whole stack to anyone who can read that backup, because the backup contains both halves of the pair. Operators who back these files up are responsible for protecting the backup at rest (for example, encrypting the backup or excluding the sidecar key files from backups that travel with the payload).
Do not add a persistence format, key purpose, magic string, or generated policy exception without updating the relevant source-of-truth docs and tests in the same change.
Test Fixture Map
The test suite is organized by behavior boundary.
| Area | Primary files |
|---|---|
| Wire parser/serializer | tests/test_wire_parser.cpp, tests/test_wire_serializer.cpp |
| Encoding helpers | tests/test_encoding.cpp |
| Operation model and descriptors | tests/test_operation_model.cpp, operation descriptor checks in tests/test_backend.cpp |
| FinTS bootstrap/auth/BPD | tests/test_bootstrap.cpp, tests/test_auth.cpp, tests/test_bpd_fixtures.cpp, tests/fixtures/bpd/**, tests/support/bpd_fixture_support.hpp |
| FinTS 4.1 XML and activation | tests/test_fints41_readonly.cpp, tests/test_fints41_transfer.cpp, tests/test_backend.cpp |
| Banking transport and OAuth listener | tests/test_transport.cpp, tests/test_oauth_listener.cpp |
| Read-only operations | tests/test_readonly_accounts_balances.cpp, tests/test_readonly_transactions.cpp, tests/test_readonly_holdings.cpp, tests/test_readonly_tan_media.cpp, tests/test_readonly_standing_orders.cpp, tests/support/readonly_test_support.hpp |
| Payments and standing orders | tests/test_transfer_single.cpp, tests/test_transfer_batch.cpp, tests/test_direct_debit.cpp, tests/test_standing_orders.cpp, tests/test_prepaid_topup.cpp, tests/support/transfer_test_support.hpp |
| Client workflows | tests/test_client.cpp, tests/test_client_payments.cpp, tests/test_client_continuation.cpp |
| EBICS envelope/protocol | tests/test_ebics_foundation.cpp, tests/test_ebics_uploads.cpp, tests/support/ebics_test_support.hpp, tests/support/ebics_split_support.hpp |
| Continuations and resume envelopes | tests/test_resume.cpp, tests/test_resume_envelope.cpp, tests/support/resume_test_support.hpp, tests/support/continuation_test_support.hpp |
| Storage and durability | tests/test_storage.cpp, tests/storage_lock_helper.cpp, storage durability hooks in src/galanthus/storage |
| CAMT and MT940 payloads | tests/test_camt.cpp, tests/test_mt940.cpp |
| Runtime common and REST providers | tests/test_runtime_common.cpp, tests/test_rest.cpp, tests/test_revolut_backend.cpp, tests/test_wise_backend.cpp |
| Plugin trust and adapters | tests/test_plugin_loader.cpp, tests/plugin/**, tests/support/plugin_backend_params_access.hpp |
| CLI contracts | tests/test_cli_contract.cpp, tests/test_gln_cli.cpp, tests/test_cli_profile_registry.cpp, tests/test_cli_json_output.cpp, tests/test_cli_runtime_payments.cpp, tests/cli_snapshots/** |
| C ABI | tests/c_api/**, tests/c_api/expected_symbols.txt |
| Generated docs and bindings | generator/check tests declared in tests/CMakeLists.txt |
Rules for new fixtures:
- Prefer shared support headers under
tests/supportwhen more than two tests need the same setup. - Keep raw wire/XML fixtures close to the protocol family that uses them unless they are a cross-family corpus.
- Scripted bank-response replay requires
GALANTHUS_ENABLE_TEST_TRANSPORTand is guarded bytest_transport_gate.md. - CLI snapshots are generated artifacts and are governed by
generated_artifact_policy.md. - BPD corpus expectations live under
tests/fixtures/bpdand are schema checked.
Maintainer Reading Order
For operation work:
src/galanthus/operation/model.hppsrc/galanthus/c_api/operation_descriptor.hpp- relevant client runner under
src/galanthus/client - relevant protocol family under
src/galanthus/protocol,src/galanthus/ebics, orsrc/galanthus/plugin - C ABI request/result descriptors if the public ABI changes
- focused tests in the matching row of the test fixture map
For persistence or secret work:
src/galanthus/storage/storage.hppsrc/galanthus/storage/durability.hpp- the concrete storage file (
state.cpp,resume_file.cpp,local_cache.cpp,protected_key.cpp, token store, or EBICS key store) tests/test_storage.cppand affected C API/storage tests
For plugin work:
src/galanthus/plugin_sdk/gln_plugin_abi.hsrc/galanthus/plugin/plugin_loader.*src/galanthus/plugin/plugin_trust.*src/galanthus/plugin/plugin_validation.*- provider adapter and provider plugin code
tests/test_plugin_loader.cppandtests/plugin/**
For public API work:
src/galanthus/c_api/gln_capi.h- C API descriptors and result population
docs/generated_artifact_policy.md- generated metadata/docs/bindings checks
- wrapper tests for every affected binding
Documentation Gates
For top-level narrative documentation edits, run:
git diff --check- a stale-claim grep over this file and nearby stable docs
- manual verification for new internal links and source-map claims
If a change touches generated docs, public ABI metadata, CLI references, or
bindings, run the corresponding render/generator/check targets listed in
generated_artifact_policy.md and tests/CMakeLists.txt.