Skip to content

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:

  1. The domain/client/runtime layer owns typed banking workflows.
  2. The C ABI projects those workflows into stable C handles, descriptors, and generated metadata.
  3. 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, and src/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, and src/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.hpp and shared DTO headers
  • backend resolution and operation dispatch
  • continuation token construction and parsing
  • read operation orchestration in client_readonly.cpp and client/internal/readonly_operations.hpp
  • write operation orchestration in client_payments.cpp, client_direct_debit.cpp, client_standing_orders.cpp, and client/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.hpp and common_factory.* create runtime backend adapters.
  • common_outcome.hpp carries normalized statuses and follow-up-required values.
  • fints_common_adapter.cpp and ebics_common_adapter.cpp adapt 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.h is the public declaration header.
  • base.cpp handles runtime init/shutdown, errors, strings, buffers, and version helpers.
  • backend_vtable.cpp and backend-specific C API files dispatch operations through client/runtime backends.
  • operation_descriptor.hpp projects operation metadata.
  • request_descriptor.hpp maps C request structs into typed client/protocol request objects.
  • result_descriptor.hpp, typed_populate.cpp, and backend_result.cpp populate typed result handles.
  • handle_owner.hpp provides 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.cpp for local profile/config operations
  • cli/commands_capi.cpp for banking operations that go through the C ABI
  • cli/json_output.cpp for schema-v2 JSON envelopes
  • cli/profile_registry.cpp for profile resolution
  • cli/secret_input.cpp for 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.*, and backend.* parse capability data and resolve protocol-family support.
  • auth.* and auth_helpers.* build authenticated exchanges and TAN-aware sessions.
  • readonly.* builds and parses account, balance, transaction, holding, TAN media, and standing-order reads.
  • transfer.*, transfer_*, and fints41_* 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.hpp declares transport::Transport.
  • https_transport.cpp implements 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, and status_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_revolut
  • src/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.

  1. 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.

  2. Add protocol/backend behavior. For FinTS, add builders/parsers in src/galanthus/protocol and update capability projection if needed. For EBICS, add exchange code under src/galanthus/ebics/protocol and envelope code under src/galanthus/ebics/envelope. For plugin providers, add or extend host adapter support under src/galanthus/plugin.

  3. Add typed client request/result handling. Put shared request/result DTOs in src/galanthus/client headers. Add runner code in the relevant client implementation file or private internal helper header. The client runner is responsible for producing a typed client::Outcome<T>, including interruption, follow-up, direct output, and rejection mapping.

  4. Add C ABI request and result projection when the operation is public. Update gln_capi.h, then reflect the shape in request_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.

  5. 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.

  6. Update generated artifacts if their source of truth changed. Consult generated_artifact_policy.md before committing any generated file. Typical gates include ABI manifest checks, generated C API schema checks, library function docs checks, and binding generator checks.

  7. 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

  1. A public caller invokes CLI, C++ wrapper, another binding, or raw C ABI.
  2. The C ABI maps request structs into typed client requests with request descriptors.
  3. The client resolves the backend from state and opens or reuses the FinTS runtime backend.
  4. Protocol code bootstraps/authenticates, builds an authenticated request, and sends it through transport::Transport.
  5. wire::parse_message() and protocol parsers extract statuses and payloads.
  6. The client maps successful domain rows to typed results, maps rejections to failures, or creates continuations for TAN/pagination.
  7. 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

  • success The operation finished and owns a typed result or direct output.
  • rejected The provider returned a structured rejection.
  • action_required The operation is blocked on TAN, decoupled TAN polling, VoP confirmation, or another resumable action. The result envelope owns a continuation handle.
  • resumed A caller provides continuation input and the stored token payload is parsed back into the operation-specific resume artifact.
  • failed The 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 matching gln_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_t slots are caller-allocated and reset by gln_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_t owns copied secret bytes. Internal code may borrow a std::string_view only for the duration of a call through gln_secret_borrow_internal.
  • gln_backend_result_t owns 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_*, or GLIBC_TUNABLES
  • staged-bundle time-of-check/time-of-use changes
  • stale temporary staging directories
  • plugin exceptions crossing into host control flow

Controls:

  • plugin_loader.cpp canonicalizes 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.cpp verifies capability bits and vtable/capability parity.
  • plugin_trust.cpp stages 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.cpp rejects native dependencies outside the host allowlist.
  • plugin_call_guard.cpp catches 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_encrypted and authenticated on load through storage::unpack_versioned.
  • Protected sidecar keys are purpose-scoped key material rather than pack_encrypted payloads. On Windows they are protected with DPAPI (CryptProtectData/CryptUnprotectData, current-user scope), and the Protected_key_purpose is 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_size and k_max_file_size.
  • Storage callers pass Protected_key_purpose when loading or creating keys. The per-purpose resolver (resolve_key_path -> purpose_key_filename) is reached only by the three stores that pass a NULL override 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 NULL argument yields the store's own default rather than the matching purpose_key_filename entry: 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. Their purpose_key_filename entries (.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_lock serializes 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, and secure_erase_guard when 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.

AreaPrimary files
Wire parser/serializertests/test_wire_parser.cpp, tests/test_wire_serializer.cpp
Encoding helperstests/test_encoding.cpp
Operation model and descriptorstests/test_operation_model.cpp, operation descriptor checks in tests/test_backend.cpp
FinTS bootstrap/auth/BPDtests/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 activationtests/test_fints41_readonly.cpp, tests/test_fints41_transfer.cpp, tests/test_backend.cpp
Banking transport and OAuth listenertests/test_transport.cpp, tests/test_oauth_listener.cpp
Read-only operationstests/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 orderstests/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 workflowstests/test_client.cpp, tests/test_client_payments.cpp, tests/test_client_continuation.cpp
EBICS envelope/protocoltests/test_ebics_foundation.cpp, tests/test_ebics_uploads.cpp, tests/support/ebics_test_support.hpp, tests/support/ebics_split_support.hpp
Continuations and resume envelopestests/test_resume.cpp, tests/test_resume_envelope.cpp, tests/support/resume_test_support.hpp, tests/support/continuation_test_support.hpp
Storage and durabilitytests/test_storage.cpp, tests/storage_lock_helper.cpp, storage durability hooks in src/galanthus/storage
CAMT and MT940 payloadstests/test_camt.cpp, tests/test_mt940.cpp
Runtime common and REST providerstests/test_runtime_common.cpp, tests/test_rest.cpp, tests/test_revolut_backend.cpp, tests/test_wise_backend.cpp
Plugin trust and adapterstests/test_plugin_loader.cpp, tests/plugin/**, tests/support/plugin_backend_params_access.hpp
CLI contractstests/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 ABItests/c_api/**, tests/c_api/expected_symbols.txt
Generated docs and bindingsgenerator/check tests declared in tests/CMakeLists.txt

Rules for new fixtures:

  • Prefer shared support headers under tests/support when 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_TRANSPORT and is guarded by test_transport_gate.md.
  • CLI snapshots are generated artifacts and are governed by generated_artifact_policy.md.
  • BPD corpus expectations live under tests/fixtures/bpd and are schema checked.

Maintainer Reading Order

For operation work:

  1. src/galanthus/operation/model.hpp
  2. src/galanthus/c_api/operation_descriptor.hpp
  3. relevant client runner under src/galanthus/client
  4. relevant protocol family under src/galanthus/protocol, src/galanthus/ebics, or src/galanthus/plugin
  5. C ABI request/result descriptors if the public ABI changes
  6. focused tests in the matching row of the test fixture map

For persistence or secret work:

  1. src/galanthus/storage/storage.hpp
  2. src/galanthus/storage/durability.hpp
  3. the concrete storage file (state.cpp, resume_file.cpp, local_cache.cpp, protected_key.cpp, token store, or EBICS key store)
  4. tests/test_storage.cpp and affected C API/storage tests

For plugin work:

  1. src/galanthus/plugin_sdk/gln_plugin_abi.h
  2. src/galanthus/plugin/plugin_loader.*
  3. src/galanthus/plugin/plugin_trust.*
  4. src/galanthus/plugin/plugin_validation.*
  5. provider adapter and provider plugin code
  6. tests/test_plugin_loader.cpp and tests/plugin/**

For public API work:

  1. src/galanthus/c_api/gln_capi.h
  2. C API descriptors and result population
  3. docs/generated_artifact_policy.md
  4. generated metadata/docs/bindings checks
  5. 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.