diff --git a/Client/CsobApiAccessor.cs b/Client/CsobApiAccessor.cs
new file mode 100644
index 0000000..88cb3e0
--- /dev/null
+++ b/Client/CsobApiAccessor.cs
@@ -0,0 +1,42 @@
+using Csob.Configuration;
+using Csob.Credentials;
+
+namespace Csob.Client;
+
+///
+/// Scoped accessor that resolves the credentials for the current request and lazily builds a
+/// single (bound to the request's mutual-TLS client) shared by all
+/// services handling that request.
+///
+public sealed class CsobApiAccessor
+{
+ private readonly RequestCredentialsProvider _credentialsProvider;
+ private readonly CsobHttpClientProvider _httpClientProvider;
+ private readonly CsobSettings _settings;
+ private CsobApiClient? _client;
+
+ public CsobApiAccessor(
+ RequestCredentialsProvider credentialsProvider,
+ CsobHttpClientProvider httpClientProvider,
+ CsobSettings settings)
+ {
+ _credentialsProvider = credentialsProvider;
+ _httpClientProvider = httpClientProvider;
+ _settings = settings;
+ }
+
+ public CsobApiClient Client
+ {
+ get
+ {
+ if (_client is null)
+ {
+ var credentials = _credentialsProvider.Resolve();
+ var http = _httpClientProvider.GetClient(credentials.Certificate!);
+ _client = new CsobApiClient(http, credentials, _settings);
+ }
+
+ return _client;
+ }
+ }
+}
diff --git a/Client/CsobApiClient.cs b/Client/CsobApiClient.cs
new file mode 100644
index 0000000..3587c93
--- /dev/null
+++ b/Client/CsobApiClient.cs
@@ -0,0 +1,130 @@
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Csob.Configuration;
+using Csob.Credentials;
+
+namespace Csob.Client;
+
+///
+/// Thin HTTP wrapper around the ČSOB PSD2 resource API for a single request. It attaches the
+/// mandatory COBS headers (Authorization, APIKEY, TPP-Name, X-Request-ID,
+/// Date, User-Involved), sends the call over the per-request mutual-TLS client and
+/// returns the response JSON verbatim () so no field is lost in translation.
+/// Non-success responses become a .
+///
+public sealed class CsobApiClient
+{
+ /// Web defaults (camelCase) match the COBS JSON contract; null properties are omitted on write.
+ public static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
+ {
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ private readonly HttpClient _http;
+ private readonly CsobCredentials _credentials;
+ private readonly string _baseUrl;
+
+ public CsobApiClient(HttpClient http, CsobCredentials credentials, CsobSettings settings)
+ {
+ _http = http;
+ _credentials = credentials;
+ _baseUrl = settings.ApiBaseUrl.TrimEnd('/');
+ }
+
+ public Task GetAsync(string path, IReadOnlyDictionary? query, CancellationToken ct)
+ => SendAsync(HttpMethod.Get, path, query, body: null, ct);
+
+ public Task PostAsync(string path, object? body, CancellationToken ct)
+ => SendAsync(HttpMethod.Post, path, query: null, body, ct);
+
+ public Task PutAsync(string path, object? body, CancellationToken ct)
+ => SendAsync(HttpMethod.Put, path, query: null, body, ct);
+
+ public Task DeleteAsync(string path, CancellationToken ct)
+ => SendAsync(HttpMethod.Delete, path, query: null, body: null, ct);
+
+ private async Task SendAsync(
+ HttpMethod method, string path, IReadOnlyDictionary? query, object? body, CancellationToken ct)
+ {
+ using var request = new HttpRequestMessage(method, _baseUrl + path + BuildQuery(query));
+ ApplyHeaders(request);
+
+ if (body is not null)
+ {
+ var json = body is JsonNode node ? node.ToJsonString(JsonOptions) : JsonSerializer.Serialize(body, JsonOptions);
+ request.Content = new StringContent(json, Encoding.UTF8, "application/json");
+ }
+
+ using var response = await _http.SendAsync(request, ct);
+ var payload = await response.Content.ReadAsStringAsync(ct);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new CsobApiException(response.StatusCode, ParseErrorCodes(payload), payload);
+ }
+
+ return string.IsNullOrWhiteSpace(payload) ? null : JsonNode.Parse(payload);
+ }
+
+ private void ApplyHeaders(HttpRequestMessage request)
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _credentials.AccessToken);
+ request.Headers.Date = DateTimeOffset.UtcNow;
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ request.Headers.TryAddWithoutValidation("APIKEY", _credentials.ApiKey);
+ request.Headers.TryAddWithoutValidation("TPP-Name", _credentials.TppName);
+ request.Headers.TryAddWithoutValidation("X-Request-ID", Guid.NewGuid().ToString());
+ request.Headers.TryAddWithoutValidation("User-Involved", _credentials.UserInvolved ? "true" : "false");
+
+ if (!string.IsNullOrWhiteSpace(_credentials.UserIpAddress))
+ {
+ request.Headers.TryAddWithoutValidation("User-IP-Address", _credentials.UserIpAddress);
+ }
+ }
+
+ private static string BuildQuery(IReadOnlyDictionary? query)
+ {
+ if (query is null)
+ {
+ return string.Empty;
+ }
+
+ var parts = query
+ .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
+ .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}")
+ .ToArray();
+
+ return parts.Length == 0 ? string.Empty : "?" + string.Join("&", parts);
+ }
+
+ /// Best-effort parse of the COBS error shape { "errors": [ { "error": "CODE" } ] }.
+ private static IReadOnlyList ParseErrorCodes(string payload)
+ {
+ if (string.IsNullOrWhiteSpace(payload))
+ {
+ return Array.Empty();
+ }
+
+ try
+ {
+ var node = JsonNode.Parse(payload);
+ if (node?["errors"] is JsonArray errors)
+ {
+ return errors
+ .Select(e => e?["error"]?.GetValue())
+ .Where(code => !string.IsNullOrWhiteSpace(code))
+ .Select(code => code!)
+ .ToArray();
+ }
+ }
+ catch (JsonException)
+ {
+ // Upstream returned a non-JSON error body; the raw payload is still carried on the exception.
+ }
+
+ return Array.Empty();
+ }
+}
diff --git a/Client/CsobApiException.cs b/Client/CsobApiException.cs
new file mode 100644
index 0000000..89253de
--- /dev/null
+++ b/Client/CsobApiException.cs
@@ -0,0 +1,26 @@
+using System.Net;
+
+namespace Csob.Client;
+
+///
+/// Raised when the ČSOB PSD2 API returns a non-success HTTP status. Carries the upstream status
+/// and the raw error body so the middleware can surface it without leaking credentials.
+///
+public sealed class CsobApiException : Exception
+{
+ public HttpStatusCode StatusCode { get; }
+
+ /// Machine-readable error codes parsed from the ČSOB errors[].error array (best effort).
+ public IReadOnlyList ErrorCodes { get; }
+
+ /// Raw response body (already credential-free — it is the upstream's own error payload).
+ public string? RawBody { get; }
+
+ public CsobApiException(HttpStatusCode statusCode, IReadOnlyList errorCodes, string? rawBody)
+ : base($"ČSOB PSD2 API returned {(int)statusCode} ({statusCode}).")
+ {
+ StatusCode = statusCode;
+ ErrorCodes = errorCodes;
+ RawBody = rawBody;
+ }
+}
diff --git a/Client/CsobApiPaths.cs b/Client/CsobApiPaths.cs
new file mode 100644
index 0000000..abca975
--- /dev/null
+++ b/Client/CsobApiPaths.cs
@@ -0,0 +1,44 @@
+namespace Csob.Client;
+
+///
+/// Central registry of ČSOB PSD2 resource path templates (relative to CSOB_API_BASE_URL).
+///
+/// Paths follow the Czech Open Banking Standard (COBS) as implemented by ČSOB. The account-scoped
+/// AISP paths and the PISP /my/payments/.../sign/{signId} authorization flow are confirmed
+/// against the ČSOB developer portal; the remaining COBS resources use the same /my/ prefix.
+/// Keep every path here so a portal-specific correction is a single-file change.
+///
+public static class CsobApiPaths
+{
+ // ----- AISP (Account Information) -----
+ public const string Accounts = "/my/accounts";
+ public static string Account(string id) => $"/my/accounts/{Esc(id)}";
+ public static string Balance(string accountId) => $"/my/accounts/{Esc(accountId)}/balance";
+ public static string Transactions(string accountId) => $"/my/accounts/{Esc(accountId)}/transactions";
+ public static string AwaitingTransactions(string accountId) => $"/my/accounts/{Esc(accountId)}/transactions/awaiting";
+ public static string AccountStandingOrders(string accountId) => $"/my/accounts/{Esc(accountId)}/standingorders";
+ public static string AccountStandingOrder(string accountId, string standingOrderId) => $"/my/accounts/{Esc(accountId)}/standingorders/{Esc(standingOrderId)}";
+ public static string AccountDirectDebits(string accountId) => $"/my/accounts/{Esc(accountId)}/directdebits";
+
+ // ----- PISP (Payment Initiation) -----
+ public const string Payments = "/my/payments";
+ public static string Payment(string id) => $"/my/payments/{Esc(id)}";
+ public static string PaymentStatus(string id) => $"/my/payments/{Esc(id)}/status";
+ public static string PaymentSign(string id, string signId) => $"/my/payments/{Esc(id)}/sign/{Esc(signId)}";
+
+ public const string StandingOrders = "/my/standingorders";
+ public static string StandingOrder(string id) => $"/my/standingorders/{Esc(id)}";
+ public static string StandingOrderStatus(string id) => $"/my/standingorders/{Esc(id)}/status";
+ public static string StandingOrderSign(string id, string signId) => $"/my/standingorders/{Esc(id)}/sign/{Esc(signId)}";
+
+ public const string DirectDebits = "/my/directdebits";
+ public static string DirectDebit(string id) => $"/my/directdebits/{Esc(id)}";
+ public static string DirectDebitStatus(string id) => $"/my/directdebits/{Esc(id)}/status";
+ public static string DirectDebitSign(string id, string signId) => $"/my/directdebits/{Esc(id)}/sign/{Esc(signId)}";
+
+ // ----- Common (Consents) -----
+ public const string Consents = "/consents";
+ public static string Consent(string id) => $"/consents/{Esc(id)}";
+
+ private static string Esc(string segment) => Uri.EscapeDataString(segment);
+}
diff --git a/Client/CsobHttpClientProvider.cs b/Client/CsobHttpClientProvider.cs
new file mode 100644
index 0000000..aeba1f8
--- /dev/null
+++ b/Client/CsobHttpClientProvider.cs
@@ -0,0 +1,77 @@
+using System.Collections.Concurrent;
+using System.Security.Cryptography.X509Certificates;
+using Csob.Configuration;
+
+namespace Csob.Client;
+
+///
+/// Provides instances configured for mutual TLS with a per-request eIDAS
+/// client certificate. ČSOB requires the client certificate at the transport layer, so a single
+/// shared client cannot be used across tenants.
+///
+/// Clients are cached by certificate thumbprint and reused for connection pooling. In practice the
+/// certificate identifies the TPP application (not the PSU), so the number of distinct certificates
+/// is small and bounded by the set of calling tenants. Certificates and clients live in memory only
+/// and never touch disk.
+///
+public sealed class CsobHttpClientProvider : IDisposable
+{
+ private readonly CsobSettings _settings;
+ private readonly ConcurrentDictionary _clients = new();
+ private readonly object _buildLock = new();
+
+ public CsobHttpClientProvider(CsobSettings settings) => _settings = settings;
+
+ ///
+ /// Returns a (cached) mutual-TLS presenting .
+ /// A fresh certificate instance is built per request; if an equivalent one (same thumbprint) is
+ /// already cached, the redundant instance is disposed so it does not leak.
+ ///
+ public HttpClient GetClient(X509Certificate2 certificate)
+ {
+ var thumbprint = certificate.Thumbprint;
+
+ if (_clients.TryGetValue(thumbprint, out var existing))
+ {
+ certificate.Dispose();
+ return existing;
+ }
+
+ lock (_buildLock)
+ {
+ if (_clients.TryGetValue(thumbprint, out existing))
+ {
+ certificate.Dispose();
+ return existing;
+ }
+
+ var client = BuildClient(certificate);
+ _clients[thumbprint] = client;
+ return client;
+ }
+ }
+
+ private HttpClient BuildClient(X509Certificate2 certificate)
+ {
+ var handler = new SocketsHttpHandler
+ {
+ PooledConnectionLifetime = TimeSpan.FromMinutes(10),
+ };
+ handler.SslOptions.ClientCertificates = new X509CertificateCollection { certificate };
+
+ return new HttpClient(handler, disposeHandler: true)
+ {
+ Timeout = TimeSpan.FromSeconds(_settings.RequestTimeoutSeconds),
+ };
+ }
+
+ public void Dispose()
+ {
+ foreach (var client in _clients.Values)
+ {
+ client.Dispose();
+ }
+
+ _clients.Clear();
+ }
+}
diff --git a/Configuration/CsobSettings.cs b/Configuration/CsobSettings.cs
new file mode 100644
index 0000000..6a4f684
--- /dev/null
+++ b/Configuration/CsobSettings.cs
@@ -0,0 +1,54 @@
+namespace Csob.Configuration;
+
+///
+/// Service configuration resolved from environment variables.
+///
+/// This service is a stateless, multi-tenant proxy in front of the ČSOB PSD2 (Open Banking)
+/// API. Unlike the sibling iDoklad service, it holds no per-client secrets: the eIDAS
+/// client certificate, OAuth client id/secret, access token, API key and TPP name are all
+/// supplied per request as HTTP headers by the calling client service
+/// (see ). Only non-secret infrastructure
+/// configuration (API/OAuth base URLs, app metadata, reverse-proxy prefix, timeout) lives here.
+///
+public sealed class CsobSettings
+{
+ public string AppName { get; init; } = GetEnv("APP_NAME", "ČSOB PSD2 Service");
+ public string AppVersion { get; init; } = GetEnv("APP_VERSION", "1.0.0");
+
+ /// Public reverse-proxy prefix (e.g. /apps/csob) injected by AppFactory.
+ public string RootPath { get; init; } = GetEnv("ROOT_PATH", string.Empty);
+
+ ///
+ /// Base URL of the ČSOB PSD2 resource API. Production default; override for any other
+ /// environment. All AISP/PISP/consent path templates are appended to this base.
+ ///
+ public string ApiBaseUrl { get; init; } =
+ GetEnv("CSOB_API_BASE_URL", "https://api.csob.cz/api/csob/psd2/v1");
+
+ ///
+ /// OAuth2 authorization endpoint (Authorization Code flow, PSU redirect). Production default;
+ /// verify against the current ČSOB developer portal as the host may change.
+ ///
+ public string OAuthAuthorizeUrl { get; init; } =
+ GetEnv("CSOB_OAUTH_AUTHORIZE_URL", "https://identita.csob.cz/mep/fs/fl/oauth2/auth");
+
+ ///
+ /// OAuth2 token endpoint (code->token and refresh). Production default; verify against the
+ /// current ČSOB developer portal.
+ ///
+ public string OAuthTokenUrl { get; init; } =
+ GetEnv("CSOB_OAUTH_TOKEN_URL", "https://api.csob.cz/api/csob/oauth2/v1/token");
+
+ /// Upstream HTTP request timeout in seconds.
+ public int RequestTimeoutSeconds { get; init; } =
+ ParseInt(GetEnv("CSOB_REQUEST_TIMEOUT_SECONDS", "100"), 100);
+
+ private static string GetEnv(string name, string fallback)
+ {
+ var value = Environment.GetEnvironmentVariable(name);
+ return string.IsNullOrWhiteSpace(value) ? fallback : value;
+ }
+
+ private static int ParseInt(string value, int fallback) =>
+ int.TryParse(value, out var parsed) && parsed > 0 ? parsed : fallback;
+}
diff --git a/Controllers/AccountsController.cs b/Controllers/AccountsController.cs
new file mode 100644
index 0000000..cecb457
--- /dev/null
+++ b/Controllers/AccountsController.cs
@@ -0,0 +1,61 @@
+using Microsoft.AspNetCore.Mvc;
+using Csob.Services;
+
+namespace Csob.Controllers;
+
+///
+/// AISP – account information. All endpoints require the ČSOB credential headers
+/// (eIDAS certificate, access token, API key, TPP name). See the Swagger description for details.
+///
+[ApiController]
+[Route("accounts")]
+[Produces("application/json")]
+[Tags("AISP – Accounts")]
+public sealed class AccountsController : ControllerBase
+{
+ private readonly AccountsService _service;
+
+ public AccountsController(AccountsService service) => _service = service;
+
+ /// List the PSU's payment accounts (paged).
+ [HttpGet]
+ public async Task List(
+ [FromQuery] int? page, [FromQuery] int? size, [FromQuery] string? sort, [FromQuery] string? order, CancellationToken ct)
+ => Ok(await _service.ListAsync(page, size, sort, order, ct));
+
+ /// Get the balance(s) of an account.
+ [HttpGet("{id}/balance")]
+ public async Task Balance(string id, [FromQuery] string? currency, CancellationToken ct)
+ => Ok(await _service.BalanceAsync(id, currency, ct));
+
+ /// List booked transactions of an account (paged, optional date range).
+ [HttpGet("{id}/transactions")]
+ public async Task Transactions(
+ string id,
+ [FromQuery] int? page, [FromQuery] int? size, [FromQuery] string? sort, [FromQuery] string? order,
+ [FromQuery] string? dateFrom, [FromQuery] string? dateTo, CancellationToken ct)
+ => Ok(await _service.TransactionsAsync(id, new TransactionQuery
+ {
+ Page = page, Size = size, Sort = sort, Order = order, DateFrom = dateFrom, DateTo = dateTo,
+ }, ct));
+
+ /// List awaiting (pending) transactions of an account.
+ [HttpGet("{id}/transactions/awaiting")]
+ public async Task Awaiting(string id, [FromQuery] int? page, [FromQuery] int? size, CancellationToken ct)
+ => Ok(await _service.AwaitingTransactionsAsync(id, page, size, ct));
+
+ /// List the account's existing standing orders.
+ [HttpGet("{id}/standing-orders")]
+ public async Task StandingOrders(string id, [FromQuery] int? page, [FromQuery] int? size, CancellationToken ct)
+ => Ok(await _service.StandingOrdersAsync(id, page, size, ct));
+
+ /// Get a standing-order detail for the account.
+ [HttpGet("{id}/standing-orders/{standingOrderId}")]
+ public async Task StandingOrderDetail(string id, string standingOrderId, CancellationToken ct)
+ => Ok(await _service.StandingOrderDetailAsync(id, standingOrderId, ct));
+
+ /// List the account's direct-debit mandates.
+ [HttpGet("{id}/direct-debits")]
+ public async Task DirectDebits(string id, [FromQuery] int? page, [FromQuery] int? size, CancellationToken ct)
+ => Ok(await _service.DirectDebitsAsync(id, page, size, ct));
+}
diff --git a/Controllers/ConsentsController.cs b/Controllers/ConsentsController.cs
new file mode 100644
index 0000000..62b3879
--- /dev/null
+++ b/Controllers/ConsentsController.cs
@@ -0,0 +1,35 @@
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+using Csob.Services;
+
+namespace Csob.Controllers;
+
+///
+/// Common – PSU consent lifecycle. Requires the ČSOB credential headers. The consent body is
+/// forwarded to ČSOB as raw JSON (COBS consent shape).
+///
+[ApiController]
+[Route("consents")]
+[Produces("application/json")]
+[Tags("Consents")]
+public sealed class ConsentsController : ControllerBase
+{
+ private readonly ConsentsService _service;
+
+ public ConsentsController(ConsentsService service) => _service = service;
+
+ /// Create a consent.
+ [HttpPost]
+ public async Task Create([FromBody] JsonNode request, CancellationToken ct)
+ => Ok(await _service.CreateAsync(request, ct));
+
+ /// Get a consent detail.
+ [HttpGet("{id}")]
+ public async Task Detail(string id, CancellationToken ct)
+ => Ok(await _service.DetailAsync(id, ct));
+
+ /// Revoke a consent.
+ [HttpDelete("{id}")]
+ public async Task Delete(string id, CancellationToken ct)
+ => Ok(await _service.DeleteAsync(id, ct));
+}
diff --git a/Controllers/DirectDebitsController.cs b/Controllers/DirectDebitsController.cs
new file mode 100644
index 0000000..d8b5bfd
--- /dev/null
+++ b/Controllers/DirectDebitsController.cs
@@ -0,0 +1,56 @@
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Csob.Services;
+
+namespace Csob.Controllers;
+
+///
+/// PISP – direct-debit mandate initiation and its authorization (sign) flow. Requires the ČSOB
+/// credential headers. The mandate body is forwarded to ČSOB as raw JSON (COBS direct-debit shape).
+///
+[ApiController]
+[Route("direct-debits")]
+[Produces("application/json")]
+[Tags("PISP – Direct debits")]
+public sealed class DirectDebitsController : ControllerBase
+{
+ private readonly DirectDebitsService _service;
+
+ public DirectDebitsController(DirectDebitsService service) => _service = service;
+
+ /// Initiate a direct-debit mandate.
+ [HttpPost]
+ public async Task Initiate([FromBody] JsonNode request, CancellationToken ct)
+ => Ok(await _service.InitiateAsync(request, ct));
+
+ /// Get the direct-debit mandate detail.
+ [HttpGet("{id}")]
+ public async Task Detail(string id, CancellationToken ct)
+ => Ok(await _service.DetailAsync(id, ct));
+
+ /// Get the direct-debit instruction status.
+ [HttpGet("{id}/status")]
+ public async Task Status(string id, CancellationToken ct)
+ => Ok(await _service.StatusAsync(id, ct));
+
+ /// Revoke a direct-debit mandate.
+ [HttpDelete("{id}")]
+ public async Task Cancel(string id, CancellationToken ct)
+ => Ok(await _service.CancelAsync(id, ct));
+
+ /// Start the authorization (SCA) of a direct-debit mandate. Returns the PSU redirect details.
+ [HttpPost("{id}/sign/{signId}")]
+ public async Task StartSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct)
+ => Ok(await _service.StartSignAsync(id, signId, body, ct));
+
+ /// Get the current state of an authorization (sign) transaction.
+ [HttpGet("{id}/sign/{signId}")]
+ public async Task SignStatus(string id, string signId, CancellationToken ct)
+ => Ok(await _service.SignStatusAsync(id, signId, ct));
+
+ /// Finalize an authorization (sign) transaction.
+ [HttpPut("{id}/sign/{signId}")]
+ public async Task FinalizeSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct)
+ => Ok(await _service.FinalizeSignAsync(id, signId, body, ct));
+}
diff --git a/Controllers/MetaController.cs b/Controllers/MetaController.cs
new file mode 100644
index 0000000..3f45dad
--- /dev/null
+++ b/Controllers/MetaController.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Mvc;
+using Csob.Configuration;
+
+namespace Csob.Controllers;
+
+/// Service metadata endpoints. These do not require ČSOB credentials.
+[ApiController]
+[Produces("application/json")]
+[Tags("Meta")]
+public sealed class MetaController : ControllerBase
+{
+ private readonly CsobSettings _settings;
+
+ public MetaController(CsobSettings settings) => _settings = settings;
+
+ /// Liveness probe.
+ [HttpGet("/health")]
+ public IActionResult Health() => Ok(new { status = "ok" });
+
+ /// Service name, version and the configured upstream endpoints (no secrets).
+ [HttpGet("/version")]
+ public IActionResult Version() => Ok(new
+ {
+ app = _settings.AppName,
+ version = _settings.AppVersion,
+ language = "dotnet",
+ api = "ČSOB PSD2 (Czech Open Banking Standard / COBS)",
+ root_path = _settings.RootPath,
+ });
+
+ ///
+ /// Reports the non-secret configuration. This service is stateless and multi-tenant: it holds
+ /// no credentials, so there is nothing per-client to report — every secret is supplied per
+ /// request via headers.
+ ///
+ [HttpGet("/status")]
+ public IActionResult Status() => Ok(new
+ {
+ stateless = true,
+ api_base_url = _settings.ApiBaseUrl,
+ oauth_authorize_url = _settings.OAuthAuthorizeUrl,
+ oauth_token_url = _settings.OAuthTokenUrl,
+ request_timeout_seconds = _settings.RequestTimeoutSeconds,
+ });
+}
diff --git a/Controllers/OAuthController.cs b/Controllers/OAuthController.cs
new file mode 100644
index 0000000..fec8b08
--- /dev/null
+++ b/Controllers/OAuthController.cs
@@ -0,0 +1,39 @@
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+using Csob.Models;
+using Csob.Services;
+
+namespace Csob.Controllers;
+
+///
+/// OAuth2 Authorization Code helper for the ČSOB PSD2 PSU consent flow. Build the authorization URL,
+/// redirect the PSU to it, then exchange the returned code for tokens. The token/refresh calls run
+/// over mutual TLS, so they require the X-CSOB-Certificate header; client id/secret are also
+/// supplied per request (this service is multi-tenant).
+///
+[ApiController]
+[Route("oauth")]
+[Produces("application/json")]
+[Tags("OAuth2")]
+public sealed class OAuthController : ControllerBase
+{
+ private readonly OAuthService _service;
+
+ public OAuthController(OAuthService service) => _service = service;
+
+ /// Build the ČSOB authorization URL to which the PSU must be redirected.
+ [HttpGet("authorization-url")]
+ public IActionResult AuthorizationUrl(
+ [FromQuery] string redirectUri, [FromQuery] string? scope, [FromQuery] string? state)
+ => Ok(_service.BuildAuthorizationUrl(redirectUri, scope, state));
+
+ /// Exchange an authorization code for an access/refresh token (mutual TLS).
+ [HttpPost("token")]
+ public async Task> Token([FromBody] TokenExchangeRequest request, CancellationToken ct)
+ => Ok(await _service.ExchangeCodeAsync(request, ct));
+
+ /// Refresh an access token using a refresh token (mutual TLS).
+ [HttpPost("refresh")]
+ public async Task> Refresh([FromBody] TokenRefreshRequest request, CancellationToken ct)
+ => Ok(await _service.RefreshAsync(request, ct));
+}
diff --git a/Controllers/PaymentsController.cs b/Controllers/PaymentsController.cs
new file mode 100644
index 0000000..9109584
--- /dev/null
+++ b/Controllers/PaymentsController.cs
@@ -0,0 +1,58 @@
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Csob.Models;
+using Csob.Services;
+
+namespace Csob.Controllers;
+
+///
+/// PISP – single payment initiation and its authorization (sign) flow. Requires the ČSOB credential
+/// headers. After POST /payments the response carries signInfo.signId; use it with the
+/// sign endpoints to drive Strong Customer Authentication (SCA).
+///
+[ApiController]
+[Route("payments")]
+[Produces("application/json")]
+[Tags("PISP – Payments")]
+public sealed class PaymentsController : ControllerBase
+{
+ private readonly PaymentsService _service;
+
+ public PaymentsController(PaymentsService service) => _service = service;
+
+ /// Initiate a domestic (DMCT) or SEPA (ESCT) payment.
+ [HttpPost]
+ public async Task Initiate([FromBody] PaymentInitiationRequest request, CancellationToken ct)
+ => Ok(await _service.InitiateAsync(request, ct));
+
+ /// Get the full payment detail.
+ [HttpGet("{id}")]
+ public async Task Detail(string id, CancellationToken ct)
+ => Ok(await _service.DetailAsync(id, ct));
+
+ /// Get the payment instruction status.
+ [HttpGet("{id}/status")]
+ public async Task Status(string id, CancellationToken ct)
+ => Ok(await _service.StatusAsync(id, ct));
+
+ /// Cancel a not-yet-authorized payment.
+ [HttpDelete("{id}")]
+ public async Task Cancel(string id, CancellationToken ct)
+ => Ok(await _service.CancelAsync(id, ct));
+
+ /// Start the authorization (SCA) of a payment. Returns the PSU redirect details.
+ [HttpPost("{id}/sign/{signId}")]
+ public async Task StartSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct)
+ => Ok(await _service.StartSignAsync(id, signId, body, ct));
+
+ /// Get the current state of an authorization (sign) transaction.
+ [HttpGet("{id}/sign/{signId}")]
+ public async Task SignStatus(string id, string signId, CancellationToken ct)
+ => Ok(await _service.SignStatusAsync(id, signId, ct));
+
+ /// Finalize an authorization (sign) transaction.
+ [HttpPut("{id}/sign/{signId}")]
+ public async Task FinalizeSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct)
+ => Ok(await _service.FinalizeSignAsync(id, signId, body, ct));
+}
diff --git a/Controllers/StandingOrdersController.cs b/Controllers/StandingOrdersController.cs
new file mode 100644
index 0000000..46bd51c
--- /dev/null
+++ b/Controllers/StandingOrdersController.cs
@@ -0,0 +1,54 @@
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Csob.Models;
+using Csob.Services;
+
+namespace Csob.Controllers;
+
+/// PISP – standing-order initiation and its authorization (sign) flow. Requires the ČSOB credential headers.
+[ApiController]
+[Route("standing-orders")]
+[Produces("application/json")]
+[Tags("PISP – Standing orders")]
+public sealed class StandingOrdersController : ControllerBase
+{
+ private readonly StandingOrdersService _service;
+
+ public StandingOrdersController(StandingOrdersService service) => _service = service;
+
+ /// Initiate a standing order.
+ [HttpPost]
+ public async Task Initiate([FromBody] StandingOrderInitiationRequest request, CancellationToken ct)
+ => Ok(await _service.InitiateAsync(request, ct));
+
+ /// Get the standing-order detail.
+ [HttpGet("{id}")]
+ public async Task Detail(string id, CancellationToken ct)
+ => Ok(await _service.DetailAsync(id, ct));
+
+ /// Get the standing-order instruction status.
+ [HttpGet("{id}/status")]
+ public async Task Status(string id, CancellationToken ct)
+ => Ok(await _service.StatusAsync(id, ct));
+
+ /// Cancel a standing order.
+ [HttpDelete("{id}")]
+ public async Task Cancel(string id, CancellationToken ct)
+ => Ok(await _service.CancelAsync(id, ct));
+
+ /// Start the authorization (SCA) of a standing order. Returns the PSU redirect details.
+ [HttpPost("{id}/sign/{signId}")]
+ public async Task StartSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct)
+ => Ok(await _service.StartSignAsync(id, signId, body, ct));
+
+ /// Get the current state of an authorization (sign) transaction.
+ [HttpGet("{id}/sign/{signId}")]
+ public async Task SignStatus(string id, string signId, CancellationToken ct)
+ => Ok(await _service.SignStatusAsync(id, signId, ct));
+
+ /// Finalize an authorization (sign) transaction.
+ [HttpPut("{id}/sign/{signId}")]
+ public async Task FinalizeSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct)
+ => Ok(await _service.FinalizeSignAsync(id, signId, body, ct));
+}
diff --git a/Credentials/CredentialConstants.cs b/Credentials/CredentialConstants.cs
new file mode 100644
index 0000000..1230e09
--- /dev/null
+++ b/Credentials/CredentialConstants.cs
@@ -0,0 +1,44 @@
+namespace Csob.Credentials;
+
+///
+/// Names of the HTTP headers that carry per-request ČSOB credentials and context.
+///
+/// This service is multi-tenant: it stores no credentials itself. Every sensitive value is
+/// supplied per request in a header (never the query string or body) and is forwarded to ČSOB.
+/// Headers must therefore only be sent over TLS. Nothing here is logged or persisted.
+///
+public static class CredentialConstants
+{
+ ///
+ /// eIDAS client certificate (QWAC) as a Base64-encoded PKCS#12 / PFX bundle, including the
+ /// private key and the full chain. Used to establish the mutual-TLS connection to ČSOB.
+ /// Analogous to Node's https.Agent({ pfx, passphrase }).
+ ///
+ public const string CertificateHeader = "X-CSOB-Certificate";
+
+ /// Optional passphrase protecting the PFX in .
+ public const string CertificatePasswordHeader = "X-CSOB-Certificate-Password";
+
+ /// OAuth2 Bearer access token obtained for the PSU; forwarded as Authorization: Bearer.
+ public const string AccessTokenHeader = "X-Access-Token";
+
+ /// ČSOB application API key; forwarded as the APIKEY header.
+ public const string ApiKeyHeader = "X-API-Key";
+
+ /// TPP (third-party provider) organisation name; forwarded as the TPP-Name header.
+ public const string TppNameHeader = "X-TPP-Name";
+
+ /// OAuth2 client id of the registered TPP application (used by the OAuth helper endpoints).
+ public const string ClientIdHeader = "X-CSOB-Client-Id";
+
+ /// OAuth2 client secret of the registered TPP application (used by the OAuth helper endpoints).
+ public const string ClientSecretHeader = "X-CSOB-Client-Secret";
+
+ // Optional PSU (end-user) context, forwarded verbatim to ČSOB when present.
+
+ /// Whether the PSU is online/involved in the request; forwarded as User-Involved (default false).
+ public const string UserInvolvedHeader = "X-User-Involved";
+
+ /// PSU IP address; forwarded as User-IP-Address.
+ public const string UserIpAddressHeader = "X-User-IP-Address";
+}
diff --git a/Credentials/CsobCredentials.cs b/Credentials/CsobCredentials.cs
new file mode 100644
index 0000000..4f20b6e
--- /dev/null
+++ b/Credentials/CsobCredentials.cs
@@ -0,0 +1,31 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace Csob.Credentials;
+
+///
+/// Fully resolved set of per-request credentials and PSU context used to call the ČSOB PSD2 API.
+/// Built from request headers by ; never logged.
+///
+public sealed record CsobCredentials
+{
+ /// OAuth2 Bearer access token (forwarded as Authorization: Bearer).
+ public required string AccessToken { get; init; }
+
+ /// ČSOB application API key (forwarded as APIKEY).
+ public required string ApiKey { get; init; }
+
+ /// TPP organisation name (forwarded as TPP-Name).
+ public required string TppName { get; init; }
+
+ ///
+ /// eIDAS client certificate (with private key) for mutual TLS. Optional at the type level so
+ /// metadata endpoints can resolve context, but required for any real upstream call.
+ ///
+ public X509Certificate2? Certificate { get; init; }
+
+ /// Whether the PSU is online for this request (User-Involved); defaults to false.
+ public bool UserInvolved { get; init; }
+
+ /// Optional PSU IP address (User-IP-Address).
+ public string? UserIpAddress { get; init; }
+}
diff --git a/Credentials/MissingCredentialsException.cs b/Credentials/MissingCredentialsException.cs
new file mode 100644
index 0000000..b174e10
--- /dev/null
+++ b/Credentials/MissingCredentialsException.cs
@@ -0,0 +1,16 @@
+namespace Csob.Credentials;
+
+///
+/// Raised when a request does not provide the credential headers required to call ČSOB.
+/// Translated to HTTP 401 by the exception-handling middleware.
+///
+public sealed class MissingCredentialsException : Exception
+{
+ public IReadOnlyList MissingHeaders { get; }
+
+ public MissingCredentialsException(IReadOnlyList missingHeaders)
+ : base("Incomplete ČSOB credentials. Provide the missing values as request headers (TLS only).")
+ {
+ MissingHeaders = missingHeaders;
+ }
+}
diff --git a/Credentials/RequestCredentialsProvider.cs b/Credentials/RequestCredentialsProvider.cs
new file mode 100644
index 0000000..a6743d4
--- /dev/null
+++ b/Credentials/RequestCredentialsProvider.cs
@@ -0,0 +1,97 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Csob.Credentials;
+
+///
+/// Resolves the ČSOB credentials and PSU context for the current request, exclusively from HTTP
+/// headers (this service stores no secrets). Missing required headers produce a 401; a malformed
+/// certificate or wrong passphrase produces a 400 (via ).
+///
+public sealed class RequestCredentialsProvider
+{
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public RequestCredentialsProvider(IHttpContextAccessor httpContextAccessor)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ /// Reads a single request header, returning null when absent or blank.
+ public string? Header(string name)
+ {
+ var headers = _httpContextAccessor.HttpContext?.Request.Headers;
+ if (headers is not null && headers.TryGetValue(name, out var value))
+ {
+ var raw = value.ToString();
+ if (!string.IsNullOrWhiteSpace(raw))
+ {
+ return raw;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Builds the eIDAS client certificate from the Base64 PFX header (+ optional passphrase).
+ /// Returns null when no certificate header is present.
+ ///
+ /// The header is not valid Base64 or the PFX/passphrase is invalid.
+ public X509Certificate2? TryBuildCertificate()
+ {
+ var base64 = Header(CredentialConstants.CertificateHeader);
+ if (base64 is null)
+ {
+ return null;
+ }
+
+ byte[] raw;
+ try
+ {
+ raw = Convert.FromBase64String(base64.Trim());
+ }
+ catch (FormatException ex)
+ {
+ throw new CryptographicException($"{CredentialConstants.CertificateHeader} is not valid Base64.", ex);
+ }
+
+ var password = Header(CredentialConstants.CertificatePasswordHeader);
+
+ // EphemeralKeySet keeps the private key in memory only — never written to the machine key store / disk.
+ return new X509Certificate2(raw, password, X509KeyStorageFlags.EphemeralKeySet);
+ }
+
+ ///
+ /// Resolves the full credential set required for an AISP/PISP/consent call. Throws
+ /// if any required header is absent.
+ ///
+ public CsobCredentials Resolve()
+ {
+ var accessToken = Header(CredentialConstants.AccessTokenHeader);
+ var apiKey = Header(CredentialConstants.ApiKeyHeader);
+ var tppName = Header(CredentialConstants.TppNameHeader);
+ var certificate = TryBuildCertificate();
+
+ var missing = new List();
+ if (string.IsNullOrWhiteSpace(accessToken)) missing.Add(CredentialConstants.AccessTokenHeader);
+ if (string.IsNullOrWhiteSpace(apiKey)) missing.Add(CredentialConstants.ApiKeyHeader);
+ if (string.IsNullOrWhiteSpace(tppName)) missing.Add(CredentialConstants.TppNameHeader);
+ if (certificate is null) missing.Add(CredentialConstants.CertificateHeader);
+ if (missing.Count > 0)
+ {
+ certificate?.Dispose();
+ throw new MissingCredentialsException(missing);
+ }
+
+ return new CsobCredentials
+ {
+ AccessToken = accessToken!,
+ ApiKey = apiKey!,
+ TppName = tppName!,
+ Certificate = certificate,
+ UserInvolved = string.Equals(Header(CredentialConstants.UserInvolvedHeader), "true", StringComparison.OrdinalIgnoreCase),
+ UserIpAddress = Header(CredentialConstants.UserIpAddressHeader),
+ };
+ }
+}
diff --git a/Csob.csproj b/Csob.csproj
index 24ec0e8..b80d4ff 100644
--- a/Csob.csproj
+++ b/Csob.csproj
@@ -3,5 +3,13 @@
net8.0
enable
enable
+ Csob
+ true
+
+ $(NoWarn);1591
+
+
+
+
diff --git a/Infrastructure/CredentialHeadersOperationFilter.cs b/Infrastructure/CredentialHeadersOperationFilter.cs
new file mode 100644
index 0000000..f56993c
--- /dev/null
+++ b/Infrastructure/CredentialHeadersOperationFilter.cs
@@ -0,0 +1,70 @@
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using Csob.Controllers;
+using Csob.Credentials;
+
+namespace Csob.Infrastructure;
+
+///
+/// Documents the per-request credential headers in Swagger. Metadata endpoints need none; the
+/// OAuth helper needs the certificate + client id/secret; every other (AISP/PISP/consent) endpoint
+/// needs the certificate + access token + API key + TPP name. Headers are marked optional at the
+/// schema level (the service validates them at runtime) but the descriptions state what is required.
+///
+public sealed class CredentialHeadersOperationFilter : IOperationFilter
+{
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ var declaringType = context.MethodInfo.DeclaringType;
+
+ // Metadata endpoints (health/version/status) do not talk to ČSOB.
+ if (declaringType == typeof(MetaController))
+ {
+ return;
+ }
+
+ operation.Parameters ??= new List();
+
+ AddHeader(operation, CredentialConstants.CertificateHeader,
+ "eIDAS client certificate (QWAC) as a Base64-encoded PFX/PKCS#12 bundle incl. private key and chain. Used for mutual TLS to ČSOB. SENSITIVE — TLS only.");
+ AddHeader(operation, CredentialConstants.CertificatePasswordHeader,
+ "Passphrase protecting the PFX in X-CSOB-Certificate (omit if the PFX has no password). SENSITIVE.");
+
+ if (declaringType == typeof(OAuthController))
+ {
+ AddHeader(operation, CredentialConstants.ClientIdHeader,
+ "OAuth2 client id of the registered TPP application. Required.");
+ AddHeader(operation, CredentialConstants.ClientSecretHeader,
+ "OAuth2 client secret of the registered TPP application. Required for token/refresh. SENSITIVE.");
+ return;
+ }
+
+ AddHeader(operation, CredentialConstants.AccessTokenHeader,
+ "OAuth2 Bearer access token obtained for the PSU. Forwarded as 'Authorization: Bearer'. Required. SENSITIVE.");
+ AddHeader(operation, CredentialConstants.ApiKeyHeader,
+ "ČSOB application API key. Forwarded as 'APIKEY'. Required.");
+ AddHeader(operation, CredentialConstants.TppNameHeader,
+ "TPP organisation name. Forwarded as 'TPP-Name'. Required.");
+ AddHeader(operation, CredentialConstants.UserInvolvedHeader,
+ "Optional. 'true' if the PSU is online for this request (forwarded as 'User-Involved'). Defaults to false.", example: "false");
+ AddHeader(operation, CredentialConstants.UserIpAddressHeader,
+ "Optional. PSU IP address (forwarded as 'User-IP-Address').");
+ }
+
+ private static void AddHeader(OpenApiOperation operation, string name, string description, string? example = null)
+ {
+ operation.Parameters.Add(new OpenApiParameter
+ {
+ Name = name,
+ In = ParameterLocation.Header,
+ Required = false,
+ Description = description,
+ Schema = new OpenApiSchema
+ {
+ Type = "string",
+ Example = example is null ? null : new OpenApiString(example),
+ },
+ });
+ }
+}
diff --git a/Infrastructure/ExceptionHandlingMiddleware.cs b/Infrastructure/ExceptionHandlingMiddleware.cs
new file mode 100644
index 0000000..3419c6c
--- /dev/null
+++ b/Infrastructure/ExceptionHandlingMiddleware.cs
@@ -0,0 +1,127 @@
+using System.Net;
+using System.Security.Cryptography;
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+using Csob.Client;
+using Csob.Credentials;
+
+namespace Csob.Infrastructure;
+
+///
+/// Translates domain exceptions into JSON responses. Credentials are
+/// never logged — only upstream status codes and error codes (the upstream's own payload). Errors
+/// are always logged (no silent failures).
+///
+public sealed class ExceptionHandlingMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (MissingCredentialsException ex)
+ {
+ await WriteProblem(context, HttpStatusCode.Unauthorized, ex.Message, new Dictionary
+ {
+ ["missingHeaders"] = ex.MissingHeaders,
+ });
+ }
+ catch (CryptographicException ex)
+ {
+ // Invalid Base64 PFX or wrong passphrase in the certificate header. Do not log the value.
+ _logger.LogWarning("Client certificate could not be loaded: {Message}", ex.Message);
+ await WriteProblem(context, HttpStatusCode.BadRequest,
+ "The provided client certificate could not be loaded. Check X-CSOB-Certificate (Base64 PFX) and X-CSOB-Certificate-Password.", null);
+ }
+ catch (CsobApiException ex)
+ {
+ _logger.LogWarning("ČSOB PSD2 API call failed: {Status} {Codes}", (int)ex.StatusCode, string.Join(",", ex.ErrorCodes));
+ await WriteProblem(context, ex.StatusCode, ex.Message, new Dictionary
+ {
+ ["csobErrorCodes"] = ex.ErrorCodes,
+ ["upstreamBody"] = ParseBody(ex.RawBody),
+ });
+ }
+ catch (TaskCanceledException) when (!context.RequestAborted.IsCancellationRequested)
+ {
+ _logger.LogWarning("ČSOB PSD2 API call timed out.");
+ await WriteProblem(context, HttpStatusCode.GatewayTimeout, "The ČSOB PSD2 API did not respond in time.", null);
+ }
+ catch (HttpRequestException ex)
+ {
+ // Network failure, or the mutual-TLS handshake was rejected (e.g. wrong/expired eIDAS certificate).
+ _logger.LogWarning(ex, "ČSOB PSD2 API unreachable or TLS handshake failed.");
+ await WriteProblem(context, HttpStatusCode.BadGateway,
+ "The ČSOB PSD2 API is unreachable or the mutual-TLS handshake failed.", null);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unhandled error while processing the request.");
+ await WriteProblem(context, HttpStatusCode.InternalServerError, "An unexpected error occurred.", null);
+ }
+ }
+
+ private static JsonNode? ParseBody(string? rawBody)
+ {
+ if (string.IsNullOrWhiteSpace(rawBody))
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonNode.Parse(rawBody);
+ }
+ catch (System.Text.Json.JsonException)
+ {
+ return JsonValue.Create(rawBody);
+ }
+ }
+
+ private static async Task WriteProblem(HttpContext context, HttpStatusCode status, string detail, IDictionary? extensions)
+ {
+ if (context.Response.HasStarted)
+ {
+ return;
+ }
+
+ var problem = new ProblemDetails
+ {
+ Status = (int)status,
+ Title = ReasonPhrase(status),
+ Detail = detail,
+ };
+
+ if (extensions is not null)
+ {
+ foreach (var (key, value) in extensions)
+ {
+ problem.Extensions[key] = value;
+ }
+ }
+
+ context.Response.Clear();
+ context.Response.StatusCode = (int)status;
+ context.Response.ContentType = "application/problem+json";
+ await context.Response.WriteAsJsonAsync(problem);
+ }
+
+ private static string ReasonPhrase(HttpStatusCode status) => status switch
+ {
+ HttpStatusCode.Unauthorized => "Unauthorized",
+ HttpStatusCode.BadRequest => "Bad Request",
+ HttpStatusCode.BadGateway => "Upstream ČSOB PSD2 API error",
+ HttpStatusCode.GatewayTimeout => "Upstream ČSOB PSD2 API timeout",
+ _ => status.ToString(),
+ };
+}
diff --git a/Models/Common.cs b/Models/Common.cs
new file mode 100644
index 0000000..c9b760a
--- /dev/null
+++ b/Models/Common.cs
@@ -0,0 +1,91 @@
+using System.Text.Json.Nodes;
+
+namespace Csob.Models;
+
+// Reusable building blocks shared by the typed PISP request bodies. Field names follow the COBS
+// JSON contract (camelCase via System.Text.Json web defaults). Deeply variable sub-trees (party
+// identification, structured remittance) are typed as JsonNode so any valid COBS payload passes
+// through unchanged.
+
+/// Monetary amount with ISO 4217 currency.
+public sealed class Amount
+{
+ public decimal Value { get; set; }
+ public string Currency { get; set; } = "CZK";
+}
+
+/// Wrapper matching the amount object whose instructedAmount holds the value/currency.
+public sealed class AmountContainer
+{
+ public Amount InstructedAmount { get; set; } = new();
+}
+
+/// Account number identification. For domestic/SEPA writes only iban is required.
+public sealed class AccountIdentification
+{
+ public string? Iban { get; set; }
+ public string? Other { get; set; }
+}
+
+/// Account reference with optional currency (debtorAccount/creditorAccount).
+public sealed class AccountReference
+{
+ public AccountIdentification Identification { get; set; } = new();
+ public string? Currency { get; set; }
+}
+
+public sealed class ServiceLevel
+{
+ /// Payment scheme: DMCT (domestic), ESCT (SEPA), XBCT (cross-border), EXCT, NXCT.
+ public string? Code { get; set; }
+}
+
+public sealed class PaymentTypeInformation
+{
+ /// NORM (default), HIGH (express) or INST (instant).
+ public string? InstructionPriority { get; set; }
+ public ServiceLevel? ServiceLevel { get; set; }
+}
+
+public sealed class FinancialInstitutionIdentification
+{
+ public string? Bic { get; set; }
+}
+
+public sealed class Agent
+{
+ public FinancialInstitutionIdentification? FinancialInstitutionIdentification { get; set; }
+}
+
+public sealed class PostalAddress
+{
+ public string? StreetName { get; set; }
+ public string? BuildingNumber { get; set; }
+ public string? PostCode { get; set; }
+ public string? TownName { get; set; }
+ public string? Country { get; set; }
+}
+
+/// A party (debtor/creditor/ultimate*). Identification is variable, so kept as raw JSON.
+public sealed class Party
+{
+ public string? Name { get; set; }
+ public PostalAddress? PostalAddress { get; set; }
+ public JsonNode? Identification { get; set; }
+}
+
+///
+/// Remittance information. unstructured is free text (Czech symbols may be encoded as
+/// /VS/.../SS/.../KS/...); structured varies (its reference may be a string or
+/// an array) so it is passed through as raw JSON.
+///
+public sealed class RemittanceInformation
+{
+ public string? Unstructured { get; set; }
+ public JsonNode? Structured { get; set; }
+}
+
+public sealed class Purpose
+{
+ public string? Proprietary { get; set; }
+}
diff --git a/Models/OAuth.cs b/Models/OAuth.cs
new file mode 100644
index 0000000..b466b01
--- /dev/null
+++ b/Models/OAuth.cs
@@ -0,0 +1,25 @@
+namespace Csob.Models;
+
+/// Body for POST /oauth/token (Authorization Code grant). Secrets travel in headers.
+public sealed class TokenExchangeRequest
+{
+ /// Authorization code returned to the redirect URI after PSU consent.
+ public string Code { get; set; } = string.Empty;
+ /// Redirect URI registered for the TPP app; must match the one used to obtain the code.
+ public string RedirectUri { get; set; } = string.Empty;
+}
+
+/// Body for POST /oauth/refresh (Refresh Token grant).
+public sealed class TokenRefreshRequest
+{
+ public string RefreshToken { get; set; } = string.Empty;
+}
+
+/// Response of GET /oauth/authorization-url.
+public sealed class AuthorizationUrlResponse
+{
+ /// Fully-built ČSOB authorization URL to which the PSU must be redirected.
+ public string AuthorizationUrl { get; set; } = string.Empty;
+ /// The opaque state value echoed back on the redirect (CSRF protection).
+ public string? State { get; set; }
+}
diff --git a/Models/Payments.cs b/Models/Payments.cs
new file mode 100644
index 0000000..6daf7ef
--- /dev/null
+++ b/Models/Payments.cs
@@ -0,0 +1,38 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Csob.Models;
+
+public sealed class PaymentIdentification
+{
+ /// Unique instruction id assigned by the TPP (idempotency key). Max 35 chars.
+ public string? InstructionIdentification { get; set; }
+ public string? EndToEndIdentification { get; set; }
+}
+
+///
+/// Payment initiation request (POST /my/payments). Covers domestic (DMCT) and SEPA (ESCT)
+/// payments — SEPA-only fields (creditor address, agent, ultimate parties, purpose) are optional.
+/// Any additional COBS field not modelled here is preserved via .
+///
+public sealed class PaymentInitiationRequest
+{
+ public PaymentIdentification? PaymentIdentification { get; set; }
+ public PaymentTypeInformation? PaymentTypeInformation { get; set; }
+ public AmountContainer Amount { get; set; } = new();
+ public string? RequestedExecutionDate { get; set; }
+
+ public Party? UltimateDebtor { get; set; }
+ public AccountReference DebtorAccount { get; set; } = new();
+ public Agent? CreditorAgent { get; set; }
+ public Party? Creditor { get; set; }
+ public AccountReference CreditorAccount { get; set; } = new();
+ public Party? UltimateCreditor { get; set; }
+
+ public RemittanceInformation? RemittanceInformation { get; set; }
+ public Purpose? Purpose { get; set; }
+
+ /// Any COBS fields not explicitly modelled are forwarded to ČSOB unchanged.
+ [JsonExtensionData]
+ public Dictionary? AdditionalData { get; set; }
+}
diff --git a/Models/StandingOrders.cs b/Models/StandingOrders.cs
new file mode 100644
index 0000000..4dd0a10
--- /dev/null
+++ b/Models/StandingOrders.cs
@@ -0,0 +1,48 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace Csob.Models;
+
+public sealed class StandingOrderIdentification
+{
+ public string? InstructionIdentification { get; set; }
+ public string? TransactionIdentification { get; set; }
+}
+
+public sealed class StandingOrderExecution
+{
+ /// DAILY, WEEKLY, MONTHLY, BI_MONTHLY, QUARTERLY, HALFYEARLY, YEARLY, SINGLE, IRREGULAR.
+ public string? Interval { get; set; }
+ /// Day within the interval (e.g. day-of-month "25").
+ public string? IntervalDue { get; set; }
+ /// e.g. MAX_AMOUNT_EXCEEDED, UNTIL_CANCELLATION.
+ public string? Mode { get; set; }
+ public string? ModeDue { get; set; }
+}
+
+public sealed class StandingOrderDetail
+{
+ public string? Alias { get; set; }
+ public StandingOrderExecution? Execution { get; set; }
+ /// Optional exceptions block (stoppages/breaks); shape varies, kept as raw JSON.
+ public JsonNode? Exceptions { get; set; }
+ /// Optional validity block (lastExecutionDate/maxAmount); kept as raw JSON.
+ public JsonNode? Validity { get; set; }
+}
+
+/// Standing-order initiation request (POST /my/standingorders).
+public sealed class StandingOrderInitiationRequest
+{
+ public StandingOrderIdentification? StandingOrderIdentification { get; set; }
+ public PaymentTypeInformation? PaymentTypeInformation { get; set; }
+ public AmountContainer Amount { get; set; } = new();
+ public string? RequestedExecutionDate { get; set; }
+ public StandingOrderDetail? StandingOrder { get; set; }
+ public AccountReference DebtorAccount { get; set; } = new();
+ public AccountReference CreditorAccount { get; set; } = new();
+ public RemittanceInformation? RemittanceInformation { get; set; }
+
+ [JsonExtensionData]
+ public Dictionary? AdditionalData { get; set; }
+}
diff --git a/Program.cs b/Program.cs
index 6ae7e8b..bdf1406 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,22 +1,116 @@
+using System.Text.Json.Serialization;
+using Microsoft.OpenApi.Models;
+using Csob.Client;
+using Csob.Configuration;
+using Csob.Credentials;
+using Csob.Infrastructure;
+using Csob.Services;
+
var builder = WebApplication.CreateBuilder(args);
+
+// Configuration resolved from environment variables (non-secret infra config only).
+var settings = new CsobSettings();
+builder.Services.AddSingleton(settings);
+
+// The eIDAS client certificate is supplied per request as a Base64 PFX header, which is large.
+// Raise Kestrel's header size limit so the certificate header is not rejected.
+builder.WebHost.ConfigureKestrel(options =>
+{
+ options.Limits.MaxRequestHeadersTotalSize = 1024 * 1024; // 1 MiB
+});
+
+builder.Services.AddHttpContextAccessor();
+
+// Builds/caches mutual-TLS HttpClients by certificate thumbprint; shared across requests.
+builder.Services.AddSingleton();
+
+// Per-request credential resolution and API access.
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+// Agenda services.
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+builder.Services
+ .AddControllers()
+ .AddJsonOptions(options =>
+ {
+ // camelCase (web defaults) matches the COBS JSON contract; omit null properties on write.
+ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+ });
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(options =>
+{
+ options.SwaggerDoc("v1", new OpenApiInfo
+ {
+ Title = settings.AppName,
+ Version = settings.AppVersion,
+ Description =
+ "Multi-tenant REST integration with the ČSOB PSD2 (Open Banking) API, following the " +
+ "Czech Open Banking Standard (COBS). Covers AISP (account information), PISP (payment, " +
+ "standing-order and direct-debit initiation with the sign/SCA flow), consents and the " +
+ "OAuth2 Authorization Code helper.\n\n" +
+ "**Credentials.** This service stores no secrets — every sensitive value is supplied per " +
+ "request in a header and forwarded to ČSOB over TLS (never the query string or body):\n\n" +
+ "- `X-CSOB-Certificate` — eIDAS client certificate (QWAC) as a Base64 PFX (mutual TLS). Required.\n" +
+ "- `X-CSOB-Certificate-Password` — PFX passphrase (optional).\n" +
+ "- `X-Access-Token` — OAuth2 Bearer access token for the PSU (AISP/PISP/consents). Required.\n" +
+ "- `X-API-Key` — ČSOB application API key (sent upstream as `APIKEY`). Required.\n" +
+ "- `X-TPP-Name` — TPP organisation name (sent upstream as `TPP-Name`). Required.\n" +
+ "- `X-CSOB-Client-Id` / `X-CSOB-Client-Secret` — OAuth2 app credentials (OAuth endpoints only).\n\n" +
+ "Obtain the access token via the `/oauth/*` endpoints (Authorization Code flow with PSU redirect).",
+ });
+ options.OperationFilter();
+
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml");
+ if (File.Exists(xmlPath))
+ {
+ options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
+ }
+});
+
var app = builder.Build();
-var rootPath = Environment.GetEnvironmentVariable("ROOT_PATH");
-if (!string.IsNullOrWhiteSpace(rootPath))
+// ROOT_PATH carries the public reverse-proxy prefix (e.g. /apps/csob). AppFactory's Caddy uses
+// handle_path, which strips that prefix before the request reaches the container, so PathBase ends
+// up empty and the OpenAPI server URL must come from ROOT_PATH directly (see the filter below).
+var publicPrefix = string.IsNullOrWhiteSpace(settings.RootPath) ? null : "/" + settings.RootPath.Trim('/');
+
+if (publicPrefix is not null)
{
- app.UsePathBase(rootPath);
+ app.UsePathBase(publicPrefix);
}
-app.MapGet("/", () => Results.Json(new
-{
- name = "csob",
- service = "csob",
- status = "ok"
-}));
+app.UseMiddleware();
-app.MapGet("/health", () => Results.Json(new
+// Serve the OpenAPI document under the same /docs prefix as the UI so a relative endpoint resolves
+// correctly both locally and behind a reverse-proxy prefix (ROOT_PATH).
+app.UseSwagger(options =>
{
- status = "ok"
-}));
+ options.RouteTemplate = "docs/{documentName}/swagger.json";
+ // Advertise the public prefix (e.g. /apps/csob) as the OpenAPI server so Swagger UI "Try it out"
+ // targets {prefix}/accounts, not the host root. The prefix comes from ROOT_PATH because
+ // handle_path has already stripped it from the request, leaving PathBase empty. Fall back to
+ // PathBase (a proxy that keeps the prefix) and finally "/".
+ options.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
+ {
+ var serverUrl = publicPrefix ?? (httpReq.PathBase.HasValue ? httpReq.PathBase.Value : "/");
+ swaggerDoc.Servers = new List { new() { Url = serverUrl } };
+ });
+});
+app.UseSwaggerUI(options =>
+{
+ options.RoutePrefix = "docs";
+ options.SwaggerEndpoint("v1/swagger.json", $"{settings.AppName} v1");
+ options.DocumentTitle = $"{settings.AppName} – API docs";
+});
+
+app.MapControllers();
app.Run();
diff --git a/README.md b/README.md
index 6b19598..624e17e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,90 @@
# csob
-.NET API služba vytvořená přes CSBot Services Portal.
+Multi-tenant REST integrace na **ČSOB PSD2 (Open Banking) API** podle Czech Open Banking Standard (COBS).
+.NET 8 služba běžící v AppFactory za reverse proxy `/apps/csob`.
+
+Pokrývá **AISP** (informace o účtech), **PISP** (iniciace plateb, trvalých příkazů a inkas vč. sign/SCA flow),
+**consents** a **OAuth2** helper (Authorization Code flow).
+
+## Architektura
+
+Služba je **bezstavový proxy** – neukládá žádné credentials. Volající klientská služba předává veškeré
+citlivé údaje (eIDAS certifikát, OAuth client id/secret, access token, APIKEY, TPP name) **per-request
+v HTTP hlavičkách** (jen přes TLS). mTLS se na základě certifikátu z hlavičky staví per-request a klienti
+se cachují podle thumbprintu certifikátu. Nic se neloguje ani neukládá na disk.
+
+Strukturně zrcadlí sesterskou službu `idoklad` (Configuration → env, Credentials → hlavičky, Client factory
++ Accessor, Services, Controllers, Infrastructure middleware + Swagger operation filter, `/docs`).
## Endpointy
-- GET /
-- GET /health
+Veřejně dostupné přes `https://services.csbot.cz/apps/csob/...`.
+
+### Meta (bez credentials)
+- `GET /health` – liveness
+- `GET /version` – název/verze + API info
+- `GET /status` – ne-secret konfigurace (base/oauth URL, timeout)
+- `GET /docs` – Swagger UI
+- `GET /docs/v1/swagger.json` – OpenAPI dokument
+
+### OAuth2
+- `GET /oauth/authorization-url` – sestaví authorize URL pro přesměrování PSU
+- `POST /oauth/token` – výměna `code` → access/refresh token (mTLS)
+- `POST /oauth/refresh` – obnova access tokenu (mTLS)
+
+### AISP – účty
+- `GET /accounts`
+- `GET /accounts/{id}/balance`
+- `GET /accounts/{id}/transactions`
+- `GET /accounts/{id}/transactions/awaiting`
+- `GET /accounts/{id}/standing-orders`
+- `GET /accounts/{id}/standing-orders/{standingOrderId}`
+- `GET /accounts/{id}/direct-debits`
+
+### PISP – platby / trvalé příkazy / inkasa
+Pro každou agendu: `POST` (iniciace), `GET /{id}` (detail), `GET /{id}/status`, `DELETE /{id}` (zrušení),
+a sign/SCA flow `POST|GET|PUT /{id}/sign/{signId}`.
+- `/payments`
+- `/standing-orders`
+- `/direct-debits`
+
+### Consents
+- `POST /consents`, `GET /consents/{id}`, `DELETE /consents/{id}`
+
+## Credential hlavičky (per-request, jen přes TLS)
+
+| Hlavička | Význam | Předáno do ČSOB jako |
+|---|---|---|
+| `X-CSOB-Certificate` | eIDAS klientský certifikát (QWAC), **Base64 PFX/PKCS#12** vč. privátního klíče a chainu | mutual TLS |
+| `X-CSOB-Certificate-Password` | heslo k PFX (volitelné) | – |
+| `X-Access-Token` | OAuth2 Bearer access token PSU (AISP/PISP/consents) | `Authorization: Bearer` |
+| `X-API-Key` | ČSOB application API key | `APIKEY` |
+| `X-TPP-Name` | název TPP organizace | `TPP-Name` |
+| `X-CSOB-Client-Id` / `X-CSOB-Client-Secret` | OAuth2 app credentials (jen `/oauth/*`) | OAuth form |
+| `X-User-Involved`, `X-User-IP-Address` | volitelný PSU kontext | `User-Involved`, `User-IP-Address` |
+
+Hlavičky `X-Request-ID` a `Date` generuje služba automaticky.
+
+## Konfigurace (environment variables – pouze ne-secret)
+
+| Proměnná | Default |
+|---|---|
+| `APP_NAME` | `ČSOB PSD2 Service` |
+| `APP_VERSION` | `1.0.0` |
+| `ROOT_PATH` | (prázdné; AppFactory nastaví `/apps/csob`) |
+| `CSOB_API_BASE_URL` | `https://api.csob.cz/api/csob/psd2/v1` |
+| `CSOB_OAUTH_AUTHORIZE_URL` | `https://identita.csob.cz/mep/fs/fl/oauth2/auth` |
+| `CSOB_OAUTH_TOKEN_URL` | `https://api.csob.cz/api/csob/oauth2/v1/token` |
+| `CSOB_REQUEST_TIMEOUT_SECONDS` | `100` |
+
+> **Žádné secrets v env.** Certifikát, client id/secret, access token a APIKEY jdou výhradně per-request hlavičkami.
+
+## Lokální spuštění
+
+```bash
+dotnet run --project Csob.csproj
+# nebo
+ROOT_PATH=/apps/csob ASPNETCORE_URLS=http://0.0.0.0:8080 dotnet run
+```
+
+Podrobnosti k OAuth/SCA flow a kompletní seznam endpointů viz [documentation/](documentation/).
diff --git a/Services/AccountsService.cs b/Services/AccountsService.cs
new file mode 100644
index 0000000..d208086
--- /dev/null
+++ b/Services/AccountsService.cs
@@ -0,0 +1,65 @@
+using System.Text.Json.Nodes;
+using Csob.Client;
+
+namespace Csob.Services;
+
+/// AISP – account information (accounts, balance, transactions, standing orders, direct debits).
+public sealed class AccountsService
+{
+ private readonly CsobApiAccessor _accessor;
+
+ public AccountsService(CsobApiAccessor accessor) => _accessor = accessor;
+
+ public Task ListAsync(int? page, int? size, string? sort, string? order, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.Accounts, Paging(page, size, sort, order), ct);
+
+ public Task BalanceAsync(string accountId, string? currency, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.Balance(accountId),
+ new Dictionary { ["currency"] = currency }, ct);
+
+ public Task TransactionsAsync(string accountId, TransactionQuery query, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.Transactions(accountId), query.ToDictionary(), ct);
+
+ public Task AwaitingTransactionsAsync(string accountId, int? page, int? size, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.AwaitingTransactions(accountId), Paging(page, size, null, null), ct);
+
+ public Task StandingOrdersAsync(string accountId, int? page, int? size, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.AccountStandingOrders(accountId), Paging(page, size, null, null), ct);
+
+ public Task StandingOrderDetailAsync(string accountId, string standingOrderId, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.AccountStandingOrder(accountId, standingOrderId), null, ct);
+
+ public Task DirectDebitsAsync(string accountId, int? page, int? size, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.AccountDirectDebits(accountId), Paging(page, size, null, null), ct);
+
+ private static Dictionary Paging(int? page, int? size, string? sort, string? order) => new()
+ {
+ ["page"] = page?.ToString(),
+ ["size"] = size?.ToString(),
+ ["sort"] = sort,
+ ["order"] = order,
+ };
+}
+
+/// Optional filters for the transactions listing (forwarded as query parameters).
+public sealed class TransactionQuery
+{
+ public int? Page { get; set; }
+ public int? Size { get; set; }
+ public string? Sort { get; set; }
+ public string? Order { get; set; }
+ /// ISO date (YYYY-MM-DD) lower bound.
+ public string? DateFrom { get; set; }
+ /// ISO date (YYYY-MM-DD) upper bound.
+ public string? DateTo { get; set; }
+
+ public Dictionary ToDictionary() => new()
+ {
+ ["page"] = Page?.ToString(),
+ ["size"] = Size?.ToString(),
+ ["sort"] = Sort,
+ ["order"] = Order,
+ ["dateFrom"] = DateFrom,
+ ["dateTo"] = DateTo,
+ };
+}
diff --git a/Services/ConsentsService.cs b/Services/ConsentsService.cs
new file mode 100644
index 0000000..d73a3ad
--- /dev/null
+++ b/Services/ConsentsService.cs
@@ -0,0 +1,24 @@
+using System.Text.Json.Nodes;
+using Csob.Client;
+
+namespace Csob.Services;
+
+///
+/// Common – PSU consent lifecycle (create / detail / revoke). The consent request body varies
+/// across COBS profiles, so it is accepted and forwarded as raw JSON.
+///
+public sealed class ConsentsService
+{
+ private readonly CsobApiAccessor _accessor;
+
+ public ConsentsService(CsobApiAccessor accessor) => _accessor = accessor;
+
+ public Task CreateAsync(JsonNode request, CancellationToken ct)
+ => _accessor.Client.PostAsync(CsobApiPaths.Consents, request, ct);
+
+ public Task DetailAsync(string id, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.Consent(id), null, ct);
+
+ public Task DeleteAsync(string id, CancellationToken ct)
+ => _accessor.Client.DeleteAsync(CsobApiPaths.Consent(id), ct);
+}
diff --git a/Services/DirectDebitsService.cs b/Services/DirectDebitsService.cs
new file mode 100644
index 0000000..b5a9f61
--- /dev/null
+++ b/Services/DirectDebitsService.cs
@@ -0,0 +1,36 @@
+using System.Text.Json.Nodes;
+using Csob.Client;
+
+namespace Csob.Services;
+
+///
+/// PISP – direct-debit mandate initiation, detail/status, revocation and the sign (SCA) flow.
+/// The mandate creation body varies across COBS profiles, so it is accepted and forwarded as raw JSON.
+///
+public sealed class DirectDebitsService
+{
+ private readonly CsobApiAccessor _accessor;
+
+ public DirectDebitsService(CsobApiAccessor accessor) => _accessor = accessor;
+
+ public Task InitiateAsync(JsonNode request, CancellationToken ct)
+ => _accessor.Client.PostAsync(CsobApiPaths.DirectDebits, request, ct);
+
+ public Task DetailAsync(string id, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.DirectDebit(id), null, ct);
+
+ public Task StatusAsync(string id, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.DirectDebitStatus(id), null, ct);
+
+ public Task CancelAsync(string id, CancellationToken ct)
+ => _accessor.Client.DeleteAsync(CsobApiPaths.DirectDebit(id), ct);
+
+ public Task StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
+ => _accessor.Client.PostAsync(CsobApiPaths.DirectDebitSign(id, signId), body, ct);
+
+ public Task SignStatusAsync(string id, string signId, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.DirectDebitSign(id, signId), null, ct);
+
+ public Task FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
+ => _accessor.Client.PutAsync(CsobApiPaths.DirectDebitSign(id, signId), body, ct);
+}
diff --git a/Services/OAuthService.cs b/Services/OAuthService.cs
new file mode 100644
index 0000000..d3c2335
--- /dev/null
+++ b/Services/OAuthService.cs
@@ -0,0 +1,114 @@
+using System.Net;
+using System.Text.Json.Nodes;
+using Csob.Client;
+using Csob.Configuration;
+using Csob.Credentials;
+using Csob.Models;
+
+namespace Csob.Services;
+
+///
+/// OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and
+/// exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual
+/// TLS, so the client certificate header is required for the token/refresh calls. Client id/secret
+/// are taken from per-request headers (this service is multi-tenant and stores no app credentials).
+///
+public sealed class OAuthService
+{
+ private readonly RequestCredentialsProvider _credentials;
+ private readonly CsobHttpClientProvider _httpClientProvider;
+ private readonly CsobSettings _settings;
+
+ public OAuthService(
+ RequestCredentialsProvider credentials,
+ CsobHttpClientProvider httpClientProvider,
+ CsobSettings settings)
+ {
+ _credentials = credentials;
+ _httpClientProvider = httpClientProvider;
+ _settings = settings;
+ }
+
+ /// Builds the authorization URL to which the PSU must be redirected.
+ public AuthorizationUrlResponse BuildAuthorizationUrl(string redirectUri, string? scope, string? state)
+ {
+ var clientId = RequireHeader(CredentialConstants.ClientIdHeader);
+
+ var query = new Dictionary
+ {
+ ["response_type"] = "code",
+ ["client_id"] = clientId,
+ ["redirect_uri"] = redirectUri,
+ ["scope"] = scope,
+ ["state"] = state,
+ };
+
+ var url = _settings.OAuthAuthorizeUrl + "?" + string.Join("&", query
+ .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
+ .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}"));
+
+ return new AuthorizationUrlResponse { AuthorizationUrl = url, State = state };
+ }
+
+ public Task ExchangeCodeAsync(TokenExchangeRequest request, CancellationToken ct)
+ => PostTokenAsync(new Dictionary
+ {
+ ["grant_type"] = "authorization_code",
+ ["code"] = request.Code,
+ ["redirect_uri"] = request.RedirectUri,
+ }, ct);
+
+ public Task RefreshAsync(TokenRefreshRequest request, CancellationToken ct)
+ => PostTokenAsync(new Dictionary
+ {
+ ["grant_type"] = "refresh_token",
+ ["refresh_token"] = request.RefreshToken,
+ }, ct);
+
+ private async Task PostTokenAsync(Dictionary form, CancellationToken ct)
+ {
+ var clientId = RequireHeader(CredentialConstants.ClientIdHeader);
+ var clientSecret = RequireHeader(CredentialConstants.ClientSecretHeader);
+
+ var certificate = _credentials.TryBuildCertificate()
+ ?? throw new MissingCredentialsException(new[] { CredentialConstants.CertificateHeader });
+
+ form["client_id"] = clientId;
+ form["client_secret"] = clientSecret;
+
+ var http = _httpClientProvider.GetClient(certificate);
+
+ using var content = new FormUrlEncodedContent(form);
+ using var response = await http.PostAsync(_settings.OAuthTokenUrl, content, ct);
+ var payload = await response.Content.ReadAsStringAsync(ct);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new CsobApiException(response.StatusCode, ParseOAuthError(payload), payload);
+ }
+
+ return string.IsNullOrWhiteSpace(payload) ? null : JsonNode.Parse(payload);
+ }
+
+ private string RequireHeader(string name)
+ => _credentials.Header(name) ?? throw new MissingCredentialsException(new[] { name });
+
+ private static IReadOnlyList ParseOAuthError(string payload)
+ {
+ if (string.IsNullOrWhiteSpace(payload))
+ {
+ return Array.Empty();
+ }
+
+ try
+ {
+ // OAuth2 errors use { "error": "...", "error_description": "..." }.
+ var error = JsonNode.Parse(payload)?["error"]?.GetValue();
+ return string.IsNullOrWhiteSpace(error) ? Array.Empty() : new[] { error! };
+ }
+ catch (System.Text.Json.JsonException)
+ {
+ return Array.Empty();
+ }
+ }
+}
diff --git a/Services/PaymentsService.cs b/Services/PaymentsService.cs
new file mode 100644
index 0000000..e9064a4
--- /dev/null
+++ b/Services/PaymentsService.cs
@@ -0,0 +1,37 @@
+using System.Text.Json.Nodes;
+using Csob.Client;
+using Csob.Models;
+
+namespace Csob.Services;
+
+/// PISP – single payment initiation, status/detail, cancellation and the sign (SCA) flow.
+public sealed class PaymentsService
+{
+ private readonly CsobApiAccessor _accessor;
+
+ public PaymentsService(CsobApiAccessor accessor) => _accessor = accessor;
+
+ public Task InitiateAsync(PaymentInitiationRequest request, CancellationToken ct)
+ => _accessor.Client.PostAsync(CsobApiPaths.Payments, request, ct);
+
+ public Task DetailAsync(string id, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.Payment(id), null, ct);
+
+ public Task StatusAsync(string id, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.PaymentStatus(id), null, ct);
+
+ public Task CancelAsync(string id, CancellationToken ct)
+ => _accessor.Client.DeleteAsync(CsobApiPaths.Payment(id), ct);
+
+ /// Starts transaction authorization (SCA). Returns the redirect details for the PSU.
+ public Task StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
+ => _accessor.Client.PostAsync(CsobApiPaths.PaymentSign(id, signId), body, ct);
+
+ /// Gets the current state of an authorization (sign) transaction.
+ public Task SignStatusAsync(string id, string signId, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.PaymentSign(id, signId), null, ct);
+
+ /// Finalizes an authorization (sign) transaction.
+ public Task FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
+ => _accessor.Client.PutAsync(CsobApiPaths.PaymentSign(id, signId), body, ct);
+}
diff --git a/Services/StandingOrdersService.cs b/Services/StandingOrdersService.cs
new file mode 100644
index 0000000..44ee1aa
--- /dev/null
+++ b/Services/StandingOrdersService.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Nodes;
+using Csob.Client;
+using Csob.Models;
+
+namespace Csob.Services;
+
+/// PISP – standing-order initiation, detail/status, cancellation and the sign (SCA) flow.
+public sealed class StandingOrdersService
+{
+ private readonly CsobApiAccessor _accessor;
+
+ public StandingOrdersService(CsobApiAccessor accessor) => _accessor = accessor;
+
+ public Task InitiateAsync(StandingOrderInitiationRequest request, CancellationToken ct)
+ => _accessor.Client.PostAsync(CsobApiPaths.StandingOrders, request, ct);
+
+ public Task DetailAsync(string id, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.StandingOrder(id), null, ct);
+
+ public Task StatusAsync(string id, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.StandingOrderStatus(id), null, ct);
+
+ public Task CancelAsync(string id, CancellationToken ct)
+ => _accessor.Client.DeleteAsync(CsobApiPaths.StandingOrder(id), ct);
+
+ public Task StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
+ => _accessor.Client.PostAsync(CsobApiPaths.StandingOrderSign(id, signId), body, ct);
+
+ public Task SignStatusAsync(string id, string signId, CancellationToken ct)
+ => _accessor.Client.GetAsync(CsobApiPaths.StandingOrderSign(id, signId), null, ct);
+
+ public Task FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
+ => _accessor.Client.PutAsync(CsobApiPaths.StandingOrderSign(id, signId), body, ct);
+}
diff --git a/bin/Release/net8.0/Csob.deps.json b/bin/Release/net8.0/Csob.deps.json
new file mode 100644
index 0000000..103545a
--- /dev/null
+++ b/bin/Release/net8.0/Csob.deps.json
@@ -0,0 +1,106 @@
+{
+ "runtimeTarget": {
+ "name": ".NETCoreApp,Version=v8.0",
+ "signature": ""
+ },
+ "compilationOptions": {},
+ "targets": {
+ ".NETCoreApp,Version=v8.0": {
+ "Csob/1.0.0": {
+ "dependencies": {
+ "Swashbuckle.AspNetCore": "6.9.0"
+ },
+ "runtime": {
+ "Csob.dll": {}
+ }
+ },
+ "Microsoft.OpenApi/1.6.14": {
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.OpenApi.dll": {
+ "assemblyVersion": "1.6.14.0",
+ "fileVersion": "1.6.14.0"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore/6.9.0": {
+ "dependencies": {
+ "Swashbuckle.AspNetCore.Swagger": "6.9.0",
+ "Swashbuckle.AspNetCore.SwaggerGen": "6.9.0",
+ "Swashbuckle.AspNetCore.SwaggerUI": "6.9.0"
+ }
+ },
+ "Swashbuckle.AspNetCore.Swagger/6.9.0": {
+ "dependencies": {
+ "Microsoft.OpenApi": "1.6.14"
+ },
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
+ "assemblyVersion": "6.9.0.0",
+ "fileVersion": "6.9.0.799"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": {
+ "dependencies": {
+ "Swashbuckle.AspNetCore.Swagger": "6.9.0"
+ },
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
+ "assemblyVersion": "6.9.0.0",
+ "fileVersion": "6.9.0.799"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": {
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
+ "assemblyVersion": "6.9.0.0",
+ "fileVersion": "6.9.0.799"
+ }
+ }
+ }
+ }
+ },
+ "libraries": {
+ "Csob/1.0.0": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "Microsoft.OpenApi/1.6.14": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==",
+ "path": "microsoft.openapi/1.6.14",
+ "hashPath": "microsoft.openapi.1.6.14.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore/6.9.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-lvI+XHF21tkwXd2nDCLGJsdhdUYsY3Ax2fWUlvw81Oa6EedtnIAf5tThy8ZnPcz/9/TwsLgjgtX9ifOCIjbEPA==",
+ "path": "swashbuckle.aspnetcore/6.9.0",
+ "hashPath": "swashbuckle.aspnetcore.6.9.0.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore.Swagger/6.9.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-P316kpxx5DnDvJwNWW8iTAXkh9DVenAxFGe9v4OUS0gil+vitH7F1feXhCtVeHN/616EFNTMh4pV2lcr9kkw/w==",
+ "path": "swashbuckle.aspnetcore.swagger/6.9.0",
+ "hashPath": "swashbuckle.aspnetcore.swagger.6.9.0.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-FjeMR3fBzwVc5plfYjoHw9ptf8SOWMupvO9X35J5EgzT3L9dRqSxa+cBKzL8PwCyemY0xNrggQSB5+MFWx1axg==",
+ "path": "swashbuckle.aspnetcore.swaggergen/6.9.0",
+ "hashPath": "swashbuckle.aspnetcore.swaggergen.6.9.0.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-0OxlWBFLl2gUESZX/K7QCTz9KctKy0VxHTvLIBcyWGD4z/fv5MCMW02qzYGcReLJr4yBnNDRzApKtLh6oBpe9A==",
+ "path": "swashbuckle.aspnetcore.swaggerui/6.9.0",
+ "hashPath": "swashbuckle.aspnetcore.swaggerui.6.9.0.nupkg.sha512"
+ }
+ }
+}
\ No newline at end of file
diff --git a/bin/Release/net8.0/Csob.dll b/bin/Release/net8.0/Csob.dll
new file mode 100644
index 0000000..f133367
Binary files /dev/null and b/bin/Release/net8.0/Csob.dll differ
diff --git a/bin/Release/net8.0/Csob.exe b/bin/Release/net8.0/Csob.exe
new file mode 100644
index 0000000..b47b671
Binary files /dev/null and b/bin/Release/net8.0/Csob.exe differ
diff --git a/bin/Release/net8.0/Csob.pdb b/bin/Release/net8.0/Csob.pdb
new file mode 100644
index 0000000..fab13fe
Binary files /dev/null and b/bin/Release/net8.0/Csob.pdb differ
diff --git a/bin/Release/net8.0/Csob.runtimeconfig.json b/bin/Release/net8.0/Csob.runtimeconfig.json
new file mode 100644
index 0000000..6a48a7e
--- /dev/null
+++ b/bin/Release/net8.0/Csob.runtimeconfig.json
@@ -0,0 +1,20 @@
+{
+ "runtimeOptions": {
+ "tfm": "net8.0",
+ "frameworks": [
+ {
+ "name": "Microsoft.NETCore.App",
+ "version": "8.0.0"
+ },
+ {
+ "name": "Microsoft.AspNetCore.App",
+ "version": "8.0.0"
+ }
+ ],
+ "configProperties": {
+ "System.GC.Server": true,
+ "System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
+ "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/bin/Release/net8.0/Csob.staticwebassets.endpoints.json b/bin/Release/net8.0/Csob.staticwebassets.endpoints.json
new file mode 100644
index 0000000..5576e88
--- /dev/null
+++ b/bin/Release/net8.0/Csob.staticwebassets.endpoints.json
@@ -0,0 +1 @@
+{"Version":1,"ManifestType":"Build","Endpoints":[]}
\ No newline at end of file
diff --git a/bin/Release/net8.0/Csob.xml b/bin/Release/net8.0/Csob.xml
new file mode 100644
index 0000000..3c80cbb
--- /dev/null
+++ b/bin/Release/net8.0/Csob.xml
@@ -0,0 +1,502 @@
+
+
+
+ Csob
+
+
+
+
+ Scoped accessor that resolves the credentials for the current request and lazily builds a
+ single (bound to the request's mutual-TLS client) shared by all
+ services handling that request.
+
+
+
+
+ Thin HTTP wrapper around the ČSOB PSD2 resource API for a single request. It attaches the
+ mandatory COBS headers (Authorization, APIKEY, TPP-Name, X-Request-ID,
+ Date, User-Involved), sends the call over the per-request mutual-TLS client and
+ returns the response JSON verbatim () so no field is lost in translation.
+ Non-success responses become a .
+
+
+
+ Web defaults (camelCase) match the COBS JSON contract; null properties are omitted on write.
+
+
+ Best-effort parse of the COBS error shape { "errors": [ { "error": "CODE" } ] }.
+
+
+
+ Raised when the ČSOB PSD2 API returns a non-success HTTP status. Carries the upstream status
+ and the raw error body so the middleware can surface it without leaking credentials.
+
+
+
+ Machine-readable error codes parsed from the ČSOB errors[].error array (best effort).
+
+
+ Raw response body (already credential-free — it is the upstream's own error payload).
+
+
+
+ Central registry of ČSOB PSD2 resource path templates (relative to CSOB_API_BASE_URL).
+
+ Paths follow the Czech Open Banking Standard (COBS) as implemented by ČSOB. The account-scoped
+ AISP paths and the PISP /my/payments/.../sign/{signId} authorization flow are confirmed
+ against the ČSOB developer portal; the remaining COBS resources use the same /my/ prefix.
+ Keep every path here so a portal-specific correction is a single-file change.
+
+
+
+
+ Provides instances configured for mutual TLS with a per-request eIDAS
+ client certificate. ČSOB requires the client certificate at the transport layer, so a single
+ shared client cannot be used across tenants.
+
+ Clients are cached by certificate thumbprint and reused for connection pooling. In practice the
+ certificate identifies the TPP application (not the PSU), so the number of distinct certificates
+ is small and bounded by the set of calling tenants. Certificates and clients live in memory only
+ and never touch disk.
+
+
+
+
+ Returns a (cached) mutual-TLS presenting .
+ A fresh certificate instance is built per request; if an equivalent one (same thumbprint) is
+ already cached, the redundant instance is disposed so it does not leak.
+
+
+
+
+ Service configuration resolved from environment variables.
+
+ This service is a stateless, multi-tenant proxy in front of the ČSOB PSD2 (Open Banking)
+ API. Unlike the sibling iDoklad service, it holds no per-client secrets: the eIDAS
+ client certificate, OAuth client id/secret, access token, API key and TPP name are all
+ supplied per request as HTTP headers by the calling client service
+ (see ). Only non-secret infrastructure
+ configuration (API/OAuth base URLs, app metadata, reverse-proxy prefix, timeout) lives here.
+
+
+
+ Public reverse-proxy prefix (e.g. /apps/csob) injected by AppFactory.
+
+
+
+ Base URL of the ČSOB PSD2 resource API. Production default; override for any other
+ environment. All AISP/PISP/consent path templates are appended to this base.
+
+
+
+
+ OAuth2 authorization endpoint (Authorization Code flow, PSU redirect). Production default;
+ verify against the current ČSOB developer portal as the host may change.
+
+
+
+
+ OAuth2 token endpoint (code->token and refresh). Production default; verify against the
+ current ČSOB developer portal.
+
+
+
+ Upstream HTTP request timeout in seconds.
+
+
+
+ AISP – account information. All endpoints require the ČSOB credential headers
+ (eIDAS certificate, access token, API key, TPP name). See the Swagger description for details.
+
+
+
+ List the PSU's payment accounts (paged).
+
+
+ Get the balance(s) of an account.
+
+
+ List booked transactions of an account (paged, optional date range).
+
+
+ List awaiting (pending) transactions of an account.
+
+
+ List the account's existing standing orders.
+
+
+ Get a standing-order detail for the account.
+
+
+ List the account's direct-debit mandates.
+
+
+
+ Common – PSU consent lifecycle. Requires the ČSOB credential headers. The consent body is
+ forwarded to ČSOB as raw JSON (COBS consent shape).
+
+
+
+ Create a consent.
+
+
+ Get a consent detail.
+
+
+ Revoke a consent.
+
+
+
+ PISP – direct-debit mandate initiation and its authorization (sign) flow. Requires the ČSOB
+ credential headers. The mandate body is forwarded to ČSOB as raw JSON (COBS direct-debit shape).
+
+
+
+ Initiate a direct-debit mandate.
+
+
+ Get the direct-debit mandate detail.
+
+
+ Get the direct-debit instruction status.
+
+
+ Revoke a direct-debit mandate.
+
+
+ Start the authorization (SCA) of a direct-debit mandate. Returns the PSU redirect details.
+
+
+ Get the current state of an authorization (sign) transaction.
+
+
+ Finalize an authorization (sign) transaction.
+
+
+ Service metadata endpoints. These do not require ČSOB credentials.
+
+
+ Liveness probe.
+
+
+ Service name, version and the configured upstream endpoints (no secrets).
+
+
+
+ Reports the non-secret configuration. This service is stateless and multi-tenant: it holds
+ no credentials, so there is nothing per-client to report — every secret is supplied per
+ request via headers.
+
+
+
+
+ OAuth2 Authorization Code helper for the ČSOB PSD2 PSU consent flow. Build the authorization URL,
+ redirect the PSU to it, then exchange the returned code for tokens. The token/refresh calls run
+ over mutual TLS, so they require the X-CSOB-Certificate header; client id/secret are also
+ supplied per request (this service is multi-tenant).
+
+
+
+ Build the ČSOB authorization URL to which the PSU must be redirected.
+
+
+ Exchange an authorization code for an access/refresh token (mutual TLS).
+
+
+ Refresh an access token using a refresh token (mutual TLS).
+
+
+
+ PISP – single payment initiation and its authorization (sign) flow. Requires the ČSOB credential
+ headers. After POST /payments the response carries signInfo.signId; use it with the
+ sign endpoints to drive Strong Customer Authentication (SCA).
+
+
+
+ Initiate a domestic (DMCT) or SEPA (ESCT) payment.
+
+
+ Get the full payment detail.
+
+
+ Get the payment instruction status.
+
+
+ Cancel a not-yet-authorized payment.
+
+
+ Start the authorization (SCA) of a payment. Returns the PSU redirect details.
+
+
+ Get the current state of an authorization (sign) transaction.
+
+
+ Finalize an authorization (sign) transaction.
+
+
+ PISP – standing-order initiation and its authorization (sign) flow. Requires the ČSOB credential headers.
+
+
+ Initiate a standing order.
+
+
+ Get the standing-order detail.
+
+
+ Get the standing-order instruction status.
+
+
+ Cancel a standing order.
+
+
+ Start the authorization (SCA) of a standing order. Returns the PSU redirect details.
+
+
+ Get the current state of an authorization (sign) transaction.
+
+
+ Finalize an authorization (sign) transaction.
+
+
+
+ Names of the HTTP headers that carry per-request ČSOB credentials and context.
+
+ This service is multi-tenant: it stores no credentials itself. Every sensitive value is
+ supplied per request in a header (never the query string or body) and is forwarded to ČSOB.
+ Headers must therefore only be sent over TLS. Nothing here is logged or persisted.
+
+
+
+
+ eIDAS client certificate (QWAC) as a Base64-encoded PKCS#12 / PFX bundle, including the
+ private key and the full chain. Used to establish the mutual-TLS connection to ČSOB.
+ Analogous to Node's https.Agent({ pfx, passphrase }).
+
+
+
+ Optional passphrase protecting the PFX in .
+
+
+ OAuth2 Bearer access token obtained for the PSU; forwarded as Authorization: Bearer.
+
+
+ ČSOB application API key; forwarded as the APIKEY header.
+
+
+ TPP (third-party provider) organisation name; forwarded as the TPP-Name header.
+
+
+ OAuth2 client id of the registered TPP application (used by the OAuth helper endpoints).
+
+
+ OAuth2 client secret of the registered TPP application (used by the OAuth helper endpoints).
+
+
+ Whether the PSU is online/involved in the request; forwarded as User-Involved (default false).
+
+
+ PSU IP address; forwarded as User-IP-Address.
+
+
+
+ Fully resolved set of per-request credentials and PSU context used to call the ČSOB PSD2 API.
+ Built from request headers by ; never logged.
+
+
+
+ OAuth2 Bearer access token (forwarded as Authorization: Bearer).
+
+
+ ČSOB application API key (forwarded as APIKEY).
+
+
+ TPP organisation name (forwarded as TPP-Name).
+
+
+
+ eIDAS client certificate (with private key) for mutual TLS. Optional at the type level so
+ metadata endpoints can resolve context, but required for any real upstream call.
+
+
+
+ Whether the PSU is online for this request (User-Involved); defaults to false.
+
+
+ Optional PSU IP address (User-IP-Address).
+
+
+
+ Raised when a request does not provide the credential headers required to call ČSOB.
+ Translated to HTTP 401 by the exception-handling middleware.
+
+
+
+
+ Resolves the ČSOB credentials and PSU context for the current request, exclusively from HTTP
+ headers (this service stores no secrets). Missing required headers produce a 401; a malformed
+ certificate or wrong passphrase produces a 400 (via ).
+
+
+
+ Reads a single request header, returning null when absent or blank.
+
+
+
+ Builds the eIDAS client certificate from the Base64 PFX header (+ optional passphrase).
+ Returns null when no certificate header is present.
+
+ The header is not valid Base64 or the PFX/passphrase is invalid.
+
+
+
+ Resolves the full credential set required for an AISP/PISP/consent call. Throws
+ if any required header is absent.
+
+
+
+
+ Documents the per-request credential headers in Swagger. Metadata endpoints need none; the
+ OAuth helper needs the certificate + client id/secret; every other (AISP/PISP/consent) endpoint
+ needs the certificate + access token + API key + TPP name. Headers are marked optional at the
+ schema level (the service validates them at runtime) but the descriptions state what is required.
+
+
+
+
+ Translates domain exceptions into JSON responses. Credentials are
+ never logged — only upstream status codes and error codes (the upstream's own payload). Errors
+ are always logged (no silent failures).
+
+
+
+ Monetary amount with ISO 4217 currency.
+
+
+ Wrapper matching the amount object whose instructedAmount holds the value/currency.
+
+
+ Account number identification. For domestic/SEPA writes only iban is required.
+
+
+ Account reference with optional currency (debtorAccount/creditorAccount).
+
+
+ Payment scheme: DMCT (domestic), ESCT (SEPA), XBCT (cross-border), EXCT, NXCT.
+
+
+ NORM (default), HIGH (express) or INST (instant).
+
+
+ A party (debtor/creditor/ultimate*). Identification is variable, so kept as raw JSON.
+
+
+
+ Remittance information. unstructured is free text (Czech symbols may be encoded as
+ /VS/.../SS/.../KS/...); structured varies (its reference may be a string or
+ an array) so it is passed through as raw JSON.
+
+
+
+ Body for POST /oauth/token (Authorization Code grant). Secrets travel in headers.
+
+
+ Authorization code returned to the redirect URI after PSU consent.
+
+
+ Redirect URI registered for the TPP app; must match the one used to obtain the code.
+
+
+ Body for POST /oauth/refresh (Refresh Token grant).
+
+
+ Response of GET /oauth/authorization-url.
+
+
+ Fully-built ČSOB authorization URL to which the PSU must be redirected.
+
+
+ The opaque state value echoed back on the redirect (CSRF protection).
+
+
+ Unique instruction id assigned by the TPP (idempotency key). Max 35 chars.
+
+
+
+ Payment initiation request (POST /my/payments). Covers domestic (DMCT) and SEPA (ESCT)
+ payments — SEPA-only fields (creditor address, agent, ultimate parties, purpose) are optional.
+ Any additional COBS field not modelled here is preserved via .
+
+
+
+ Any COBS fields not explicitly modelled are forwarded to ČSOB unchanged.
+
+
+ DAILY, WEEKLY, MONTHLY, BI_MONTHLY, QUARTERLY, HALFYEARLY, YEARLY, SINGLE, IRREGULAR.
+
+
+ Day within the interval (e.g. day-of-month "25").
+
+
+ e.g. MAX_AMOUNT_EXCEEDED, UNTIL_CANCELLATION.
+
+
+ Optional exceptions block (stoppages/breaks); shape varies, kept as raw JSON.
+
+
+ Optional validity block (lastExecutionDate/maxAmount); kept as raw JSON.
+
+
+ Standing-order initiation request (POST /my/standingorders).
+
+
+ AISP – account information (accounts, balance, transactions, standing orders, direct debits).
+
+
+ Optional filters for the transactions listing (forwarded as query parameters).
+
+
+ ISO date (YYYY-MM-DD) lower bound.
+
+
+ ISO date (YYYY-MM-DD) upper bound.
+
+
+
+ Common – PSU consent lifecycle (create / detail / revoke). The consent request body varies
+ across COBS profiles, so it is accepted and forwarded as raw JSON.
+
+
+
+
+ PISP – direct-debit mandate initiation, detail/status, revocation and the sign (SCA) flow.
+ The mandate creation body varies across COBS profiles, so it is accepted and forwarded as raw JSON.
+
+
+
+
+ OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and
+ exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual
+ TLS, so the client certificate header is required for the token/refresh calls. Client id/secret
+ are taken from per-request headers (this service is multi-tenant and stores no app credentials).
+
+
+
+ Builds the authorization URL to which the PSU must be redirected.
+
+
+ PISP – single payment initiation, status/detail, cancellation and the sign (SCA) flow.
+
+
+ Starts transaction authorization (SCA). Returns the redirect details for the PSU.
+
+
+ Gets the current state of an authorization (sign) transaction.
+
+
+ Finalizes an authorization (sign) transaction.
+
+
+ PISP – standing-order initiation, detail/status, cancellation and the sign (SCA) flow.
+
+
+
diff --git a/bin/Release/net8.0/Microsoft.OpenApi.dll b/bin/Release/net8.0/Microsoft.OpenApi.dll
new file mode 100644
index 0000000..aac9a6d
Binary files /dev/null and b/bin/Release/net8.0/Microsoft.OpenApi.dll differ
diff --git a/bin/Release/net8.0/Swashbuckle.AspNetCore.Swagger.dll b/bin/Release/net8.0/Swashbuckle.AspNetCore.Swagger.dll
new file mode 100644
index 0000000..b473263
Binary files /dev/null and b/bin/Release/net8.0/Swashbuckle.AspNetCore.Swagger.dll differ
diff --git a/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll
new file mode 100644
index 0000000..288a90f
Binary files /dev/null and b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll differ
diff --git a/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll
new file mode 100644
index 0000000..71e6db4
Binary files /dev/null and b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll differ
diff --git a/bin/Release/net8.0/appsettings.json b/bin/Release/net8.0/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/bin/Release/net8.0/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/documentation/authentication.md b/documentation/authentication.md
new file mode 100644
index 0000000..02ec4e0
--- /dev/null
+++ b/documentation/authentication.md
@@ -0,0 +1,86 @@
+# Autentizace a OAuth2 / SCA flow
+
+ČSOB PSD2 vyžaduje tři vrstvy:
+
+1. **Mutual TLS** s eIDAS QWAC certifikátem (na transportní vrstvě).
+2. **APIKEY** identifikující registrovanou TPP aplikaci.
+3. **OAuth2 Bearer access token** prokazující souhlas (consent) PSU – získaný Authorization Code flow.
+
+Tato služba je multi-tenant a **neukládá nic** – všechny tři vrstvy dostává per-request v hlavičkách.
+
+## Credential hlavičky
+
+| Hlavička | Povinná pro | Předáno do ČSOB jako |
+|---|---|---|
+| `X-CSOB-Certificate` (Base64 PFX) | vše (mTLS) | TLS klientský certifikát |
+| `X-CSOB-Certificate-Password` | volitelné | – |
+| `X-Access-Token` | AISP/PISP/consents | `Authorization: Bearer` |
+| `X-API-Key` | AISP/PISP/consents | `APIKEY` |
+| `X-TPP-Name` | AISP/PISP/consents | `TPP-Name` |
+| `X-CSOB-Client-Id` | `/oauth/*` | OAuth `client_id` |
+| `X-CSOB-Client-Secret` | `/oauth/token`, `/oauth/refresh` | OAuth `client_secret` |
+
+`X-Request-ID` (UUID) a `Date` (RFC 7231) doplňuje služba sama.
+
+### Příprava Base64 PFX
+
+```bash
+base64 -w0 client_qwac.pfx # Linux/macOS -> hodnota X-CSOB-Certificate
+certutil -encode client_qwac.pfx out.txt # Windows (odstranit hlavičky/řádky)
+```
+
+Heslo k PFX jde do `X-CSOB-Certificate-Password`. Hodnoty posílej **výhradně přes HTTPS**.
+
+## OAuth2 Authorization Code flow
+
+```
+1. GET /oauth/authorization-url?redirect_uri=...&scope=...&state=...
+ hlavičky: X-CSOB-Client-Id
+ -> { "authorizationUrl": "...", "state": "..." }
+
+2. Klient přesměruje PSU na authorizationUrl. PSU se přihlásí a udělí souhlas v ČSOB.
+ ČSOB přesměruje zpět na redirect_uri s parametrem ?code=...&state=...
+
+3. POST /oauth/token { "code": "...", "redirectUri": "..." }
+ hlavičky: X-CSOB-Certificate (+password), X-CSOB-Client-Id, X-CSOB-Client-Secret
+ -> { "access_token": "...", "refresh_token": "...", "expires_in": ... }
+
+4. AISP/PISP volání s hlavičkami:
+ X-CSOB-Certificate, X-Access-Token, X-API-Key, X-TPP-Name
+
+5. Obnova: POST /oauth/refresh { "refreshToken": "..." } (stejné hlavičky jako krok 3)
+```
+
+### Scopes (COBS)
+
+`aisp.accounts`, `aisp.balances`, `aisp.transactions`, `aisp.standingorders`, `aisp.directdebits`,
+`aisp.notifications`, `pisp.payments`, `pisp.standingorders`, `pisp.directdebits`, `pisp.accounts`.
+
+## PISP autorizace platby (SCA / sign flow)
+
+```
+1. POST /payments (body s platbou)
+ -> odpověď obsahuje signInfo.signId a instructionStatus
+
+2. POST /payments/{id}/sign/{signId}
+ -> autorizační detaily pro PSU (např. authorizationType, href.url pro redirect SCA)
+
+3. (volitelně) GET /payments/{id}/sign/{signId} – stav autorizace
+ (volitelně) PUT /payments/{id}/sign/{signId} – finalizace
+
+4. GET /payments/{id}/status – konečný stav (ACTC, ACSC, RJCT, …)
+```
+
+Stejný sign flow platí pro `/standing-orders` a `/direct-debits`.
+
+## Chybové odpovědi
+
+Služba vrací `application/problem+json`:
+
+- `401` – chybí povinná credential hlavička (`missingHeaders`).
+- `400` – neplatný Base64 PFX nebo špatné heslo certifikátu.
+- upstream status (4xx/5xx) – chyba z ČSOB; pole `csobErrorCodes` a `upstreamBody` nesou originální payload.
+- `502` – ČSOB nedostupné nebo selhal mTLS handshake (např. neplatný/expirovaný certifikát).
+- `504` – timeout.
+
+Secrets se nikdy nelogují ani nevracejí.
diff --git a/documentation/overview.md b/documentation/overview.md
new file mode 100644
index 0000000..31b2786
--- /dev/null
+++ b/documentation/overview.md
@@ -0,0 +1,54 @@
+# ČSOB PSD2 služba – přehled
+
+Tato služba je tenký, **bezstavový multi-tenant proxy** mezi klientskými aplikacemi a produkčním
+**ČSOB PSD2 (Open Banking) API** (Czech Open Banking Standard – COBS). Vystavuje vlastní REST endpointy
+a každý request 1:1 přeloží na odpovídající ČSOB volání, přičemž doplní povinné COBS hlavičky a naváže
+mutual TLS pomocí certifikátu předaného v hlavičce.
+
+## Klíčové principy
+
+- **Žádné uložené secrets.** Certifikát, OAuth client id/secret, access token, APIKEY i TPP name přicházejí
+ per-request v hlavičkách (viz [authentication.md](authentication.md)). V env jsou jen ne-secret URL a metadata.
+- **Věrný průchod dat.** Čtecí (GET) odpovědi a méně stabilní těla (consent, direct-debit, sign) se předávají
+ jako surové JSON (`JsonNode`), takže se neztrácí žádné pole. Typovaná těla mají jen platba a trvalý příkaz
+ (pro kvalitní Swagger), s `additionalData` pro forward-kompatibilitu.
+- **mTLS per-request.** Certifikát z `X-CSOB-Certificate` (Base64 PFX) se načte do paměti (nikdy na disk)
+ a použije pro TLS handshake. `HttpClient` se cachuje podle thumbprintu certifikátu kvůli znovupoužití spojení.
+- **Reverse proxy.** OpenAPI `servers` se nastavuje z `ROOT_PATH`, takže Swagger „Try it out“ volá přes
+ `/apps/csob/...` (Caddy `handle_path` prefix předtím odstraní).
+
+## Mapování na ČSOB
+
+| Tato služba | ČSOB PSD2 (relativně k `CSOB_API_BASE_URL`) |
+|---|---|
+| `GET /accounts` | `GET /my/accounts` |
+| `GET /accounts/{id}/balance` | `GET /my/accounts/{id}/balance` |
+| `GET /accounts/{id}/transactions` | `GET /my/accounts/{id}/transactions` |
+| `GET /accounts/{id}/transactions/awaiting` | `GET /my/accounts/{id}/transactions/awaiting` |
+| `GET /accounts/{id}/standing-orders` | `GET /my/accounts/{id}/standingorders` |
+| `GET /accounts/{id}/direct-debits` | `GET /my/accounts/{id}/directdebits` |
+| `POST /payments` | `POST /my/payments` |
+| `GET /payments/{id}` / `/status` | `GET /my/payments/{id}` / `/status` |
+| `POST|GET|PUT /payments/{id}/sign/{signId}` | `…/my/payments/{id}/sign/{signId}` |
+| `…/standing-orders…` | `…/my/standingorders…` |
+| `…/direct-debits…` | `…/my/directdebits…` |
+| `POST|GET|DELETE /consents…` | `…/consents…` |
+
+Cesty jsou centralizované v `Client/CsobApiPaths.cs`.
+
+## Co je vědomě mimo rozsah (zatím nenapojeno)
+
+V souladu s philosophy sesterské služby `idoklad` (dokumentovat, co není napojeno):
+
+- **Batch payments** (`/batchpayments`) a **co-signing list** (`/authorizations`) z COBS PISP.
+- **PIISP** balance check (`/balanceCheck`).
+- **Šifrování hlavičkových credentials** (jako v microsoft-365-service) – odloženo, spoléháme na TLS.
+
+## Ověření a rizika
+
+- Build je čistý (0 warnings). Lokálně ověřeno: `/health`, `/docs`, OpenAPI `servers`=`/apps/csob`,
+ 401 bez credentials, 400 při neplatném certifikátu, dokumentace credential hlaviček per operace.
+- **Reálné ČSOB volání** vyžaduje platnou PSD2 licenci + eIDAS QWAC certifikát, takže ho nelze otestovat bez
+ produkčních údajů klienta.
+- **OAuth authorize/token URL** a přesný tvar `sign` sub-cest pocházejí z výzkumu COBS + ČSOB portálu;
+ URL jsou env-konfigurovatelné a cesty centralizované – při odchylce ČSOB portálu je oprava jednoho místa.
diff --git a/obj/Csob.csproj.nuget.dgspec.json b/obj/Csob.csproj.nuget.dgspec.json
new file mode 100644
index 0000000..f6e0ade
--- /dev/null
+++ b/obj/Csob.csproj.nuget.dgspec.json
@@ -0,0 +1,84 @@
+{
+ "format": 1,
+ "restore": {
+ "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj": {}
+ },
+ "projects": {
+ "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj": {
+ "version": "1.0.0",
+ "restore": {
+ "projectUniqueName": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj",
+ "projectName": "Csob",
+ "projectPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj",
+ "packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\",
+ "outputPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\obj\\",
+ "projectStyle": "PackageReference",
+ "fallbackFolders": [
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
+ ],
+ "configFilePaths": [
+ "C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config",
+ "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
+ "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
+ ],
+ "originalTargetFrameworks": [
+ "net8.0"
+ ],
+ "sources": {
+ "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
+ "C:\\Program Files\\dotnet\\library-packs": {},
+ "D:\\CustomNuGetPackages": {},
+ "https://api.nuget.org/v3/index.json": {}
+ },
+ "frameworks": {
+ "net8.0": {
+ "targetAlias": "net8.0",
+ "projectReferences": {}
+ }
+ },
+ "warningProperties": {
+ "warnAsError": [
+ "NU1605"
+ ]
+ },
+ "restoreAuditProperties": {
+ "enableAudit": "true",
+ "auditLevel": "low",
+ "auditMode": "direct"
+ },
+ "SdkAnalysisLevel": "10.0.200"
+ },
+ "frameworks": {
+ "net8.0": {
+ "targetAlias": "net8.0",
+ "dependencies": {
+ "Swashbuckle.AspNetCore": {
+ "target": "Package",
+ "version": "[6.9.0, )"
+ }
+ },
+ "imports": [
+ "net461",
+ "net462",
+ "net47",
+ "net471",
+ "net472",
+ "net48",
+ "net481"
+ ],
+ "assetTargetFallback": true,
+ "warn": true,
+ "frameworkReferences": {
+ "Microsoft.AspNetCore.App": {
+ "privateAssets": "none"
+ },
+ "Microsoft.NETCore.App": {
+ "privateAssets": "all"
+ }
+ },
+ "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.204/PortableRuntimeIdentifierGraph.json"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/obj/Csob.csproj.nuget.g.props b/obj/Csob.csproj.nuget.g.props
new file mode 100644
index 0000000..e76774a
--- /dev/null
+++ b/obj/Csob.csproj.nuget.g.props
@@ -0,0 +1,23 @@
+
+
+
+ True
+ NuGet
+ $(MSBuildThisFileDirectory)project.assets.json
+ $(UserProfile)\.nuget\packages\
+ C:\Users\GamingPC\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages
+ PackageReference
+ 7.0.0
+
+
+
+
+
+
+
+
+
+
+ C:\Users\GamingPC\.nuget\packages\microsoft.extensions.apidescription.server\6.0.5
+
+
\ No newline at end of file
diff --git a/obj/Csob.csproj.nuget.g.targets b/obj/Csob.csproj.nuget.g.targets
new file mode 100644
index 0000000..eea8d76
--- /dev/null
+++ b/obj/Csob.csproj.nuget.g.targets
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs
new file mode 100644
index 0000000..2217181
--- /dev/null
+++ b/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs
@@ -0,0 +1,4 @@
+//
+using System;
+using System.Reflection;
+[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]
diff --git a/obj/Debug/net8.0/Csob.AssemblyInfo.cs b/obj/Debug/net8.0/Csob.AssemblyInfo.cs
new file mode 100644
index 0000000..cd1ad5a
--- /dev/null
+++ b/obj/Debug/net8.0/Csob.AssemblyInfo.cs
@@ -0,0 +1,22 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using System.Reflection;
+
+[assembly: System.Reflection.AssemblyCompanyAttribute("Csob")]
+[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
+[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f44ffae9fc38f3033750d626a06752e62097876f")]
+[assembly: System.Reflection.AssemblyProductAttribute("Csob")]
+[assembly: System.Reflection.AssemblyTitleAttribute("Csob")]
+[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
+
+// Generated by the MSBuild WriteCodeFragment class.
+
diff --git a/obj/Debug/net8.0/Csob.AssemblyInfoInputs.cache b/obj/Debug/net8.0/Csob.AssemblyInfoInputs.cache
new file mode 100644
index 0000000..62a3e1b
--- /dev/null
+++ b/obj/Debug/net8.0/Csob.AssemblyInfoInputs.cache
@@ -0,0 +1 @@
+0991943a86580b954544318f8a8536f244a189324540e319d6b1d62a8de99070
diff --git a/obj/Debug/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig b/obj/Debug/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig
new file mode 100644
index 0000000..fd0dccc
--- /dev/null
+++ b/obj/Debug/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig
@@ -0,0 +1,23 @@
+is_global = true
+build_property.TargetFramework = net8.0
+build_property.TargetFrameworkIdentifier = .NETCoreApp
+build_property.TargetFrameworkVersion = v8.0
+build_property.TargetPlatformMinVersion =
+build_property.UsingMicrosoftNETSdkWeb = true
+build_property.ProjectTypeGuids =
+build_property.InvariantGlobalization =
+build_property.PlatformNeutralAssembly =
+build_property.EnforceExtendedAnalyzerRules =
+build_property._SupportedPlatformList = Linux,macOS,Windows
+build_property.RootNamespace = Csob
+build_property.RootNamespace = Csob
+build_property.ProjectDir = d:\GitHubRepository\Hracicky\x\csob\
+build_property.EnableComHosting =
+build_property.EnableGeneratedComInterfaceComImportInterop =
+build_property.RazorLangVersion = 8.0
+build_property.SupportLocalizedComponentNames =
+build_property.GenerateRazorMetadataSourceChecksumAttributes =
+build_property.MSBuildProjectDirectory = d:\GitHubRepository\Hracicky\x\csob
+build_property._RazorSourceGeneratorDebug =
+build_property.EffectiveAnalysisLevelStyle = 8.0
+build_property.EnableCodeStyleSeverity =
diff --git a/obj/Debug/net8.0/Csob.GlobalUsings.g.cs b/obj/Debug/net8.0/Csob.GlobalUsings.g.cs
new file mode 100644
index 0000000..5e6145d
--- /dev/null
+++ b/obj/Debug/net8.0/Csob.GlobalUsings.g.cs
@@ -0,0 +1,17 @@
+//
+global using Microsoft.AspNetCore.Builder;
+global using Microsoft.AspNetCore.Hosting;
+global using Microsoft.AspNetCore.Http;
+global using Microsoft.AspNetCore.Routing;
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Linq;
+global using System.Net.Http;
+global using System.Net.Http.Json;
+global using System.Threading;
+global using System.Threading.Tasks;
diff --git a/obj/Debug/net8.0/Csob.assets.cache b/obj/Debug/net8.0/Csob.assets.cache
new file mode 100644
index 0000000..991f750
Binary files /dev/null and b/obj/Debug/net8.0/Csob.assets.cache differ
diff --git a/obj/Debug/net8.0/Csob.csproj.AssemblyReference.cache b/obj/Debug/net8.0/Csob.csproj.AssemblyReference.cache
new file mode 100644
index 0000000..ccf1f85
Binary files /dev/null and b/obj/Debug/net8.0/Csob.csproj.AssemblyReference.cache differ
diff --git a/obj/Release/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/obj/Release/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs
new file mode 100644
index 0000000..2217181
--- /dev/null
+++ b/obj/Release/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs
@@ -0,0 +1,4 @@
+//
+using System;
+using System.Reflection;
+[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]
diff --git a/obj/Release/net8.0/Csob.AssemblyInfo.cs b/obj/Release/net8.0/Csob.AssemblyInfo.cs
new file mode 100644
index 0000000..7ee9422
--- /dev/null
+++ b/obj/Release/net8.0/Csob.AssemblyInfo.cs
@@ -0,0 +1,22 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using System.Reflection;
+
+[assembly: System.Reflection.AssemblyCompanyAttribute("Csob")]
+[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
+[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f44ffae9fc38f3033750d626a06752e62097876f")]
+[assembly: System.Reflection.AssemblyProductAttribute("Csob")]
+[assembly: System.Reflection.AssemblyTitleAttribute("Csob")]
+[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
+
+// Generated by the MSBuild WriteCodeFragment class.
+
diff --git a/obj/Release/net8.0/Csob.AssemblyInfoInputs.cache b/obj/Release/net8.0/Csob.AssemblyInfoInputs.cache
new file mode 100644
index 0000000..38a8fe8
--- /dev/null
+++ b/obj/Release/net8.0/Csob.AssemblyInfoInputs.cache
@@ -0,0 +1 @@
+60c4996bbd19af9863eeaf63653e6911961f28af17e965a302a0cd7e40f09823
diff --git a/obj/Release/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig b/obj/Release/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig
new file mode 100644
index 0000000..fd0dccc
--- /dev/null
+++ b/obj/Release/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig
@@ -0,0 +1,23 @@
+is_global = true
+build_property.TargetFramework = net8.0
+build_property.TargetFrameworkIdentifier = .NETCoreApp
+build_property.TargetFrameworkVersion = v8.0
+build_property.TargetPlatformMinVersion =
+build_property.UsingMicrosoftNETSdkWeb = true
+build_property.ProjectTypeGuids =
+build_property.InvariantGlobalization =
+build_property.PlatformNeutralAssembly =
+build_property.EnforceExtendedAnalyzerRules =
+build_property._SupportedPlatformList = Linux,macOS,Windows
+build_property.RootNamespace = Csob
+build_property.RootNamespace = Csob
+build_property.ProjectDir = d:\GitHubRepository\Hracicky\x\csob\
+build_property.EnableComHosting =
+build_property.EnableGeneratedComInterfaceComImportInterop =
+build_property.RazorLangVersion = 8.0
+build_property.SupportLocalizedComponentNames =
+build_property.GenerateRazorMetadataSourceChecksumAttributes =
+build_property.MSBuildProjectDirectory = d:\GitHubRepository\Hracicky\x\csob
+build_property._RazorSourceGeneratorDebug =
+build_property.EffectiveAnalysisLevelStyle = 8.0
+build_property.EnableCodeStyleSeverity =
diff --git a/obj/Release/net8.0/Csob.GlobalUsings.g.cs b/obj/Release/net8.0/Csob.GlobalUsings.g.cs
new file mode 100644
index 0000000..5e6145d
--- /dev/null
+++ b/obj/Release/net8.0/Csob.GlobalUsings.g.cs
@@ -0,0 +1,17 @@
+//
+global using Microsoft.AspNetCore.Builder;
+global using Microsoft.AspNetCore.Hosting;
+global using Microsoft.AspNetCore.Http;
+global using Microsoft.AspNetCore.Routing;
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Linq;
+global using System.Net.Http;
+global using System.Net.Http.Json;
+global using System.Threading;
+global using System.Threading.Tasks;
diff --git a/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cache b/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cache
new file mode 100644
index 0000000..e69de29
diff --git a/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cs b/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cs
new file mode 100644
index 0000000..5c337f8
--- /dev/null
+++ b/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cs
@@ -0,0 +1,16 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using System.Reflection;
+
+[assembly: Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartAttribute("Swashbuckle.AspNetCore.SwaggerGen")]
+
+// Generated by the MSBuild WriteCodeFragment class.
+
diff --git a/obj/Release/net8.0/Csob.assets.cache b/obj/Release/net8.0/Csob.assets.cache
new file mode 100644
index 0000000..62d2b5d
Binary files /dev/null and b/obj/Release/net8.0/Csob.assets.cache differ
diff --git a/obj/Release/net8.0/Csob.csproj.AssemblyReference.cache b/obj/Release/net8.0/Csob.csproj.AssemblyReference.cache
new file mode 100644
index 0000000..ccf1f85
Binary files /dev/null and b/obj/Release/net8.0/Csob.csproj.AssemblyReference.cache differ
diff --git a/obj/Release/net8.0/Csob.csproj.CoreCompileInputs.cache b/obj/Release/net8.0/Csob.csproj.CoreCompileInputs.cache
new file mode 100644
index 0000000..7816add
--- /dev/null
+++ b/obj/Release/net8.0/Csob.csproj.CoreCompileInputs.cache
@@ -0,0 +1 @@
+4fe5ffce92f1961f66e6e5263d5719ffa1de25f67d0273a38e9aa8b4e6cbe7ab
diff --git a/obj/Release/net8.0/Csob.csproj.FileListAbsolute.txt b/obj/Release/net8.0/Csob.csproj.FileListAbsolute.txt
new file mode 100644
index 0000000..95b7d87
--- /dev/null
+++ b/obj/Release/net8.0/Csob.csproj.FileListAbsolute.txt
@@ -0,0 +1,36 @@
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\appsettings.json
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.staticwebassets.endpoints.json
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.exe
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.deps.json
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.runtimeconfig.json
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.dll
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.pdb
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.xml
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Microsoft.OpenApi.dll
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Swashbuckle.AspNetCore.Swagger.dll
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Swashbuckle.AspNetCore.SwaggerGen.dll
+d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Swashbuckle.AspNetCore.SwaggerUI.dll
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.csproj.AssemblyReference.cache
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rpswa.dswa.cache.json
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.GeneratedMSBuildEditorConfig.editorconfig
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.AssemblyInfoInputs.cache
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.AssemblyInfo.cs
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.csproj.CoreCompileInputs.cache
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.MvcApplicationPartsAssemblyInfo.cs
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.MvcApplicationPartsAssemblyInfo.cache
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rjimswa.dswa.cache.json
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rjsmrazor.dswa.cache.json
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rjsmcshtml.dswa.cache.json
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\scopedcss\bundle\Csob.styles.css
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.build.json
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.build.json.cache
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.development.json
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.build.endpoints.json
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\swae.build.ex.cache
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.csproj.Up2Date
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.dll
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\refint\Csob.dll
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.xml
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.pdb
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.genruntimeconfig.cache
+d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\ref\Csob.dll
diff --git a/obj/Release/net8.0/Csob.csproj.Up2Date b/obj/Release/net8.0/Csob.csproj.Up2Date
new file mode 100644
index 0000000..e69de29
diff --git a/obj/Release/net8.0/Csob.dll b/obj/Release/net8.0/Csob.dll
new file mode 100644
index 0000000..f133367
Binary files /dev/null and b/obj/Release/net8.0/Csob.dll differ
diff --git a/obj/Release/net8.0/Csob.genruntimeconfig.cache b/obj/Release/net8.0/Csob.genruntimeconfig.cache
new file mode 100644
index 0000000..5e561ad
--- /dev/null
+++ b/obj/Release/net8.0/Csob.genruntimeconfig.cache
@@ -0,0 +1 @@
+aa0d3e069791e9af17dc7d6aa9ccb6ac770b84a356fe5664aef85ad9983cc8d5
diff --git a/obj/Release/net8.0/Csob.pdb b/obj/Release/net8.0/Csob.pdb
new file mode 100644
index 0000000..fab13fe
Binary files /dev/null and b/obj/Release/net8.0/Csob.pdb differ
diff --git a/obj/Release/net8.0/Csob.xml b/obj/Release/net8.0/Csob.xml
new file mode 100644
index 0000000..3c80cbb
--- /dev/null
+++ b/obj/Release/net8.0/Csob.xml
@@ -0,0 +1,502 @@
+
+
+
+ Csob
+
+
+
+
+ Scoped accessor that resolves the credentials for the current request and lazily builds a
+ single (bound to the request's mutual-TLS client) shared by all
+ services handling that request.
+
+
+
+
+ Thin HTTP wrapper around the ČSOB PSD2 resource API for a single request. It attaches the
+ mandatory COBS headers (Authorization, APIKEY, TPP-Name, X-Request-ID,
+ Date, User-Involved), sends the call over the per-request mutual-TLS client and
+ returns the response JSON verbatim () so no field is lost in translation.
+ Non-success responses become a .
+
+
+
+ Web defaults (camelCase) match the COBS JSON contract; null properties are omitted on write.
+
+
+ Best-effort parse of the COBS error shape { "errors": [ { "error": "CODE" } ] }.
+
+
+
+ Raised when the ČSOB PSD2 API returns a non-success HTTP status. Carries the upstream status
+ and the raw error body so the middleware can surface it without leaking credentials.
+
+
+
+ Machine-readable error codes parsed from the ČSOB errors[].error array (best effort).
+
+
+ Raw response body (already credential-free — it is the upstream's own error payload).
+
+
+
+ Central registry of ČSOB PSD2 resource path templates (relative to CSOB_API_BASE_URL).
+
+ Paths follow the Czech Open Banking Standard (COBS) as implemented by ČSOB. The account-scoped
+ AISP paths and the PISP /my/payments/.../sign/{signId} authorization flow are confirmed
+ against the ČSOB developer portal; the remaining COBS resources use the same /my/ prefix.
+ Keep every path here so a portal-specific correction is a single-file change.
+
+
+
+
+ Provides instances configured for mutual TLS with a per-request eIDAS
+ client certificate. ČSOB requires the client certificate at the transport layer, so a single
+ shared client cannot be used across tenants.
+
+ Clients are cached by certificate thumbprint and reused for connection pooling. In practice the
+ certificate identifies the TPP application (not the PSU), so the number of distinct certificates
+ is small and bounded by the set of calling tenants. Certificates and clients live in memory only
+ and never touch disk.
+
+
+
+
+ Returns a (cached) mutual-TLS presenting .
+ A fresh certificate instance is built per request; if an equivalent one (same thumbprint) is
+ already cached, the redundant instance is disposed so it does not leak.
+
+
+
+
+ Service configuration resolved from environment variables.
+
+ This service is a stateless, multi-tenant proxy in front of the ČSOB PSD2 (Open Banking)
+ API. Unlike the sibling iDoklad service, it holds no per-client secrets: the eIDAS
+ client certificate, OAuth client id/secret, access token, API key and TPP name are all
+ supplied per request as HTTP headers by the calling client service
+ (see ). Only non-secret infrastructure
+ configuration (API/OAuth base URLs, app metadata, reverse-proxy prefix, timeout) lives here.
+
+
+
+ Public reverse-proxy prefix (e.g. /apps/csob) injected by AppFactory.
+
+
+
+ Base URL of the ČSOB PSD2 resource API. Production default; override for any other
+ environment. All AISP/PISP/consent path templates are appended to this base.
+
+
+
+
+ OAuth2 authorization endpoint (Authorization Code flow, PSU redirect). Production default;
+ verify against the current ČSOB developer portal as the host may change.
+
+
+
+
+ OAuth2 token endpoint (code->token and refresh). Production default; verify against the
+ current ČSOB developer portal.
+
+
+
+ Upstream HTTP request timeout in seconds.
+
+
+
+ AISP – account information. All endpoints require the ČSOB credential headers
+ (eIDAS certificate, access token, API key, TPP name). See the Swagger description for details.
+
+
+
+ List the PSU's payment accounts (paged).
+
+
+ Get the balance(s) of an account.
+
+
+ List booked transactions of an account (paged, optional date range).
+
+
+ List awaiting (pending) transactions of an account.
+
+
+ List the account's existing standing orders.
+
+
+ Get a standing-order detail for the account.
+
+
+ List the account's direct-debit mandates.
+
+
+
+ Common – PSU consent lifecycle. Requires the ČSOB credential headers. The consent body is
+ forwarded to ČSOB as raw JSON (COBS consent shape).
+
+
+
+ Create a consent.
+
+
+ Get a consent detail.
+
+
+ Revoke a consent.
+
+
+
+ PISP – direct-debit mandate initiation and its authorization (sign) flow. Requires the ČSOB
+ credential headers. The mandate body is forwarded to ČSOB as raw JSON (COBS direct-debit shape).
+
+
+
+ Initiate a direct-debit mandate.
+
+
+ Get the direct-debit mandate detail.
+
+
+ Get the direct-debit instruction status.
+
+
+ Revoke a direct-debit mandate.
+
+
+ Start the authorization (SCA) of a direct-debit mandate. Returns the PSU redirect details.
+
+
+ Get the current state of an authorization (sign) transaction.
+
+
+ Finalize an authorization (sign) transaction.
+
+
+ Service metadata endpoints. These do not require ČSOB credentials.
+
+
+ Liveness probe.
+
+
+ Service name, version and the configured upstream endpoints (no secrets).
+
+
+
+ Reports the non-secret configuration. This service is stateless and multi-tenant: it holds
+ no credentials, so there is nothing per-client to report — every secret is supplied per
+ request via headers.
+
+
+
+
+ OAuth2 Authorization Code helper for the ČSOB PSD2 PSU consent flow. Build the authorization URL,
+ redirect the PSU to it, then exchange the returned code for tokens. The token/refresh calls run
+ over mutual TLS, so they require the X-CSOB-Certificate header; client id/secret are also
+ supplied per request (this service is multi-tenant).
+
+
+
+ Build the ČSOB authorization URL to which the PSU must be redirected.
+
+
+ Exchange an authorization code for an access/refresh token (mutual TLS).
+
+
+ Refresh an access token using a refresh token (mutual TLS).
+
+
+
+ PISP – single payment initiation and its authorization (sign) flow. Requires the ČSOB credential
+ headers. After POST /payments the response carries signInfo.signId; use it with the
+ sign endpoints to drive Strong Customer Authentication (SCA).
+
+
+
+ Initiate a domestic (DMCT) or SEPA (ESCT) payment.
+
+
+ Get the full payment detail.
+
+
+ Get the payment instruction status.
+
+
+ Cancel a not-yet-authorized payment.
+
+
+ Start the authorization (SCA) of a payment. Returns the PSU redirect details.
+
+
+ Get the current state of an authorization (sign) transaction.
+
+
+ Finalize an authorization (sign) transaction.
+
+
+ PISP – standing-order initiation and its authorization (sign) flow. Requires the ČSOB credential headers.
+
+
+ Initiate a standing order.
+
+
+ Get the standing-order detail.
+
+
+ Get the standing-order instruction status.
+
+
+ Cancel a standing order.
+
+
+ Start the authorization (SCA) of a standing order. Returns the PSU redirect details.
+
+
+ Get the current state of an authorization (sign) transaction.
+
+
+ Finalize an authorization (sign) transaction.
+
+
+
+ Names of the HTTP headers that carry per-request ČSOB credentials and context.
+
+ This service is multi-tenant: it stores no credentials itself. Every sensitive value is
+ supplied per request in a header (never the query string or body) and is forwarded to ČSOB.
+ Headers must therefore only be sent over TLS. Nothing here is logged or persisted.
+
+
+
+
+ eIDAS client certificate (QWAC) as a Base64-encoded PKCS#12 / PFX bundle, including the
+ private key and the full chain. Used to establish the mutual-TLS connection to ČSOB.
+ Analogous to Node's https.Agent({ pfx, passphrase }).
+
+
+
+ Optional passphrase protecting the PFX in .
+
+
+ OAuth2 Bearer access token obtained for the PSU; forwarded as Authorization: Bearer.
+
+
+ ČSOB application API key; forwarded as the APIKEY header.
+
+
+ TPP (third-party provider) organisation name; forwarded as the TPP-Name header.
+
+
+ OAuth2 client id of the registered TPP application (used by the OAuth helper endpoints).
+
+
+ OAuth2 client secret of the registered TPP application (used by the OAuth helper endpoints).
+
+
+ Whether the PSU is online/involved in the request; forwarded as User-Involved (default false).
+
+
+ PSU IP address; forwarded as User-IP-Address.
+
+
+
+ Fully resolved set of per-request credentials and PSU context used to call the ČSOB PSD2 API.
+ Built from request headers by ; never logged.
+
+
+
+ OAuth2 Bearer access token (forwarded as Authorization: Bearer).
+
+
+ ČSOB application API key (forwarded as APIKEY).
+
+
+ TPP organisation name (forwarded as TPP-Name).
+
+
+
+ eIDAS client certificate (with private key) for mutual TLS. Optional at the type level so
+ metadata endpoints can resolve context, but required for any real upstream call.
+
+
+
+ Whether the PSU is online for this request (User-Involved); defaults to false.
+
+
+ Optional PSU IP address (User-IP-Address).
+
+
+
+ Raised when a request does not provide the credential headers required to call ČSOB.
+ Translated to HTTP 401 by the exception-handling middleware.
+
+
+
+
+ Resolves the ČSOB credentials and PSU context for the current request, exclusively from HTTP
+ headers (this service stores no secrets). Missing required headers produce a 401; a malformed
+ certificate or wrong passphrase produces a 400 (via ).
+
+
+
+ Reads a single request header, returning null when absent or blank.
+
+
+
+ Builds the eIDAS client certificate from the Base64 PFX header (+ optional passphrase).
+ Returns null when no certificate header is present.
+
+ The header is not valid Base64 or the PFX/passphrase is invalid.
+
+
+
+ Resolves the full credential set required for an AISP/PISP/consent call. Throws
+ if any required header is absent.
+
+
+
+
+ Documents the per-request credential headers in Swagger. Metadata endpoints need none; the
+ OAuth helper needs the certificate + client id/secret; every other (AISP/PISP/consent) endpoint
+ needs the certificate + access token + API key + TPP name. Headers are marked optional at the
+ schema level (the service validates them at runtime) but the descriptions state what is required.
+
+
+
+
+ Translates domain exceptions into JSON responses. Credentials are
+ never logged — only upstream status codes and error codes (the upstream's own payload). Errors
+ are always logged (no silent failures).
+
+
+
+ Monetary amount with ISO 4217 currency.
+
+
+ Wrapper matching the amount object whose instructedAmount holds the value/currency.
+
+
+ Account number identification. For domestic/SEPA writes only iban is required.
+
+
+ Account reference with optional currency (debtorAccount/creditorAccount).
+
+
+ Payment scheme: DMCT (domestic), ESCT (SEPA), XBCT (cross-border), EXCT, NXCT.
+
+
+ NORM (default), HIGH (express) or INST (instant).
+
+
+ A party (debtor/creditor/ultimate*). Identification is variable, so kept as raw JSON.
+
+
+
+ Remittance information. unstructured is free text (Czech symbols may be encoded as
+ /VS/.../SS/.../KS/...); structured varies (its reference may be a string or
+ an array) so it is passed through as raw JSON.
+
+
+
+ Body for POST /oauth/token (Authorization Code grant). Secrets travel in headers.
+
+
+ Authorization code returned to the redirect URI after PSU consent.
+
+
+ Redirect URI registered for the TPP app; must match the one used to obtain the code.
+
+
+ Body for POST /oauth/refresh (Refresh Token grant).
+
+
+ Response of GET /oauth/authorization-url.
+
+
+ Fully-built ČSOB authorization URL to which the PSU must be redirected.
+
+
+ The opaque state value echoed back on the redirect (CSRF protection).
+
+
+ Unique instruction id assigned by the TPP (idempotency key). Max 35 chars.
+
+
+
+ Payment initiation request (POST /my/payments). Covers domestic (DMCT) and SEPA (ESCT)
+ payments — SEPA-only fields (creditor address, agent, ultimate parties, purpose) are optional.
+ Any additional COBS field not modelled here is preserved via .
+
+
+
+ Any COBS fields not explicitly modelled are forwarded to ČSOB unchanged.
+
+
+ DAILY, WEEKLY, MONTHLY, BI_MONTHLY, QUARTERLY, HALFYEARLY, YEARLY, SINGLE, IRREGULAR.
+
+
+ Day within the interval (e.g. day-of-month "25").
+
+
+ e.g. MAX_AMOUNT_EXCEEDED, UNTIL_CANCELLATION.
+
+
+ Optional exceptions block (stoppages/breaks); shape varies, kept as raw JSON.
+
+
+ Optional validity block (lastExecutionDate/maxAmount); kept as raw JSON.
+
+
+ Standing-order initiation request (POST /my/standingorders).
+
+
+ AISP – account information (accounts, balance, transactions, standing orders, direct debits).
+
+
+ Optional filters for the transactions listing (forwarded as query parameters).
+
+
+ ISO date (YYYY-MM-DD) lower bound.
+
+
+ ISO date (YYYY-MM-DD) upper bound.
+
+
+
+ Common – PSU consent lifecycle (create / detail / revoke). The consent request body varies
+ across COBS profiles, so it is accepted and forwarded as raw JSON.
+
+
+
+
+ PISP – direct-debit mandate initiation, detail/status, revocation and the sign (SCA) flow.
+ The mandate creation body varies across COBS profiles, so it is accepted and forwarded as raw JSON.
+
+
+
+
+ OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and
+ exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual
+ TLS, so the client certificate header is required for the token/refresh calls. Client id/secret
+ are taken from per-request headers (this service is multi-tenant and stores no app credentials).
+
+
+
+ Builds the authorization URL to which the PSU must be redirected.
+
+
+ PISP – single payment initiation, status/detail, cancellation and the sign (SCA) flow.
+
+
+ Starts transaction authorization (SCA). Returns the redirect details for the PSU.
+
+
+ Gets the current state of an authorization (sign) transaction.
+
+
+ Finalizes an authorization (sign) transaction.
+
+
+ PISP – standing-order initiation, detail/status, cancellation and the sign (SCA) flow.
+
+
+
diff --git a/obj/Release/net8.0/apphost.exe b/obj/Release/net8.0/apphost.exe
new file mode 100644
index 0000000..b47b671
Binary files /dev/null and b/obj/Release/net8.0/apphost.exe differ
diff --git a/obj/Release/net8.0/ref/Csob.dll b/obj/Release/net8.0/ref/Csob.dll
new file mode 100644
index 0000000..a029dbb
Binary files /dev/null and b/obj/Release/net8.0/ref/Csob.dll differ
diff --git a/obj/Release/net8.0/refint/Csob.dll b/obj/Release/net8.0/refint/Csob.dll
new file mode 100644
index 0000000..a029dbb
Binary files /dev/null and b/obj/Release/net8.0/refint/Csob.dll differ
diff --git a/obj/Release/net8.0/rjsmcshtml.dswa.cache.json b/obj/Release/net8.0/rjsmcshtml.dswa.cache.json
new file mode 100644
index 0000000..01f6eda
--- /dev/null
+++ b/obj/Release/net8.0/rjsmcshtml.dswa.cache.json
@@ -0,0 +1 @@
+{"GlobalPropertiesHash":"cmk/g0ZNXu9gJ27DjjLcpkDBlTy8RbJJZDNXpi5zobE=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["RIECXlOw3nHsuvqvxZXaPj/jy\u002BfI4EndOE5nviSURLE=","7d9SLaipaa/OPEXpY1q2f1TJiMYg8zqpA6Pih/R8Tcs=","lyWTB1SAn8j26WrlgTpjAfgPiRk8CHzueKPU4K7RU/w=","AqlAhVdvD3E/meEAwnjKEEXVbI0nCoFmuUVcx4RWscw=","9bwgN9Y/AEO67vqkjG6ZJRLuRMuKd2T8iH5YYodZ8Qo=","qnfluIVdGAut31To7iOrysqO/P2XiKnuymbhW00B3F4=","lF1S3jjLqtF4rW5S0esV3nAvTIcaMB/6tBlOniDiUvI=","pHVlcHSyz8AmNqJCN6sger3xiF6u4d2yhyHAaRW4PsM="],"CachedAssets":{},"CachedCopyCandidates":{}}
\ No newline at end of file
diff --git a/obj/Release/net8.0/rjsmrazor.dswa.cache.json b/obj/Release/net8.0/rjsmrazor.dswa.cache.json
new file mode 100644
index 0000000..b5be5d0
--- /dev/null
+++ b/obj/Release/net8.0/rjsmrazor.dswa.cache.json
@@ -0,0 +1 @@
+{"GlobalPropertiesHash":"g3hu5zSe6T51z8fRKoyDb/XOC4JY4b05h9YvX9zmd1I=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["RIECXlOw3nHsuvqvxZXaPj/jy\u002BfI4EndOE5nviSURLE=","7d9SLaipaa/OPEXpY1q2f1TJiMYg8zqpA6Pih/R8Tcs=","lyWTB1SAn8j26WrlgTpjAfgPiRk8CHzueKPU4K7RU/w=","AqlAhVdvD3E/meEAwnjKEEXVbI0nCoFmuUVcx4RWscw=","9bwgN9Y/AEO67vqkjG6ZJRLuRMuKd2T8iH5YYodZ8Qo=","qnfluIVdGAut31To7iOrysqO/P2XiKnuymbhW00B3F4=","lF1S3jjLqtF4rW5S0esV3nAvTIcaMB/6tBlOniDiUvI=","pHVlcHSyz8AmNqJCN6sger3xiF6u4d2yhyHAaRW4PsM="],"CachedAssets":{},"CachedCopyCandidates":{}}
\ No newline at end of file
diff --git a/obj/Release/net8.0/rpswa.dswa.cache.json b/obj/Release/net8.0/rpswa.dswa.cache.json
new file mode 100644
index 0000000..5a0d787
--- /dev/null
+++ b/obj/Release/net8.0/rpswa.dswa.cache.json
@@ -0,0 +1 @@
+{"GlobalPropertiesHash":"s18Zph11o4XKbouZq9V6XR416tqQ2cEqoV1zo+Pq7KQ=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["RIECXlOw3nHsuvqvxZXaPj/jy\u002BfI4EndOE5nviSURLE="],"CachedAssets":{},"CachedCopyCandidates":{}}
\ No newline at end of file
diff --git a/obj/Release/net8.0/staticwebassets.build.endpoints.json b/obj/Release/net8.0/staticwebassets.build.endpoints.json
new file mode 100644
index 0000000..5576e88
--- /dev/null
+++ b/obj/Release/net8.0/staticwebassets.build.endpoints.json
@@ -0,0 +1 @@
+{"Version":1,"ManifestType":"Build","Endpoints":[]}
\ No newline at end of file
diff --git a/obj/Release/net8.0/staticwebassets.build.json b/obj/Release/net8.0/staticwebassets.build.json
new file mode 100644
index 0000000..107d3af
--- /dev/null
+++ b/obj/Release/net8.0/staticwebassets.build.json
@@ -0,0 +1 @@
+{"Version":1,"Hash":"BhivtHlvAyC9wdGc/wHwm/2EEkmCBBuuzAERe0/lTAw=","Source":"Csob","BasePath":"/","Mode":"Root","ManifestType":"Build","ReferencedProjectsConfiguration":[],"DiscoveryPatterns":[],"Assets":[],"Endpoints":[]}
\ No newline at end of file
diff --git a/obj/Release/net8.0/staticwebassets.build.json.cache b/obj/Release/net8.0/staticwebassets.build.json.cache
new file mode 100644
index 0000000..72741cd
--- /dev/null
+++ b/obj/Release/net8.0/staticwebassets.build.json.cache
@@ -0,0 +1 @@
+BhivtHlvAyC9wdGc/wHwm/2EEkmCBBuuzAERe0/lTAw=
\ No newline at end of file
diff --git a/obj/Release/net8.0/swae.build.ex.cache b/obj/Release/net8.0/swae.build.ex.cache
new file mode 100644
index 0000000..e69de29
diff --git a/obj/project.assets.json b/obj/project.assets.json
new file mode 100644
index 0000000..42401c2
--- /dev/null
+++ b/obj/project.assets.json
@@ -0,0 +1,533 @@
+{
+ "version": 3,
+ "targets": {
+ "net8.0": {
+ "Microsoft.Extensions.ApiDescription.Server/6.0.5": {
+ "type": "package",
+ "build": {
+ "build/Microsoft.Extensions.ApiDescription.Server.props": {},
+ "build/Microsoft.Extensions.ApiDescription.Server.targets": {}
+ },
+ "buildMultiTargeting": {
+ "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.props": {},
+ "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.targets": {}
+ }
+ },
+ "Microsoft.OpenApi/1.6.14": {
+ "type": "package",
+ "compile": {
+ "lib/netstandard2.0/Microsoft.OpenApi.dll": {
+ "related": ".pdb;.xml"
+ }
+ },
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.OpenApi.dll": {
+ "related": ".pdb;.xml"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore/6.9.0": {
+ "type": "package",
+ "dependencies": {
+ "Microsoft.Extensions.ApiDescription.Server": "6.0.5",
+ "Swashbuckle.AspNetCore.Swagger": "6.9.0",
+ "Swashbuckle.AspNetCore.SwaggerGen": "6.9.0",
+ "Swashbuckle.AspNetCore.SwaggerUI": "6.9.0"
+ },
+ "build": {
+ "build/Swashbuckle.AspNetCore.props": {}
+ },
+ "buildMultiTargeting": {
+ "buildMultiTargeting/Swashbuckle.AspNetCore.props": {}
+ }
+ },
+ "Swashbuckle.AspNetCore.Swagger/6.9.0": {
+ "type": "package",
+ "dependencies": {
+ "Microsoft.OpenApi": "1.6.14"
+ },
+ "compile": {
+ "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
+ "related": ".pdb;.xml"
+ }
+ },
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
+ "related": ".pdb;.xml"
+ }
+ },
+ "frameworkReferences": [
+ "Microsoft.AspNetCore.App"
+ ]
+ },
+ "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": {
+ "type": "package",
+ "dependencies": {
+ "Swashbuckle.AspNetCore.Swagger": "6.9.0"
+ },
+ "compile": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
+ "related": ".pdb;.xml"
+ }
+ },
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
+ "related": ".pdb;.xml"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": {
+ "type": "package",
+ "compile": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
+ "related": ".pdb;.xml"
+ }
+ },
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
+ "related": ".pdb;.xml"
+ }
+ },
+ "frameworkReferences": [
+ "Microsoft.AspNetCore.App"
+ ]
+ }
+ }
+ },
+ "libraries": {
+ "Microsoft.Extensions.ApiDescription.Server/6.0.5": {
+ "sha512": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==",
+ "type": "package",
+ "path": "microsoft.extensions.apidescription.server/6.0.5",
+ "hasTools": true,
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "Icon.png",
+ "build/Microsoft.Extensions.ApiDescription.Server.props",
+ "build/Microsoft.Extensions.ApiDescription.Server.targets",
+ "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.props",
+ "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.targets",
+ "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512",
+ "microsoft.extensions.apidescription.server.nuspec",
+ "tools/Newtonsoft.Json.dll",
+ "tools/dotnet-getdocument.deps.json",
+ "tools/dotnet-getdocument.dll",
+ "tools/dotnet-getdocument.runtimeconfig.json",
+ "tools/net461-x86/GetDocument.Insider.exe",
+ "tools/net461-x86/GetDocument.Insider.exe.config",
+ "tools/net461-x86/Microsoft.Win32.Primitives.dll",
+ "tools/net461-x86/System.AppContext.dll",
+ "tools/net461-x86/System.Buffers.dll",
+ "tools/net461-x86/System.Collections.Concurrent.dll",
+ "tools/net461-x86/System.Collections.NonGeneric.dll",
+ "tools/net461-x86/System.Collections.Specialized.dll",
+ "tools/net461-x86/System.Collections.dll",
+ "tools/net461-x86/System.ComponentModel.EventBasedAsync.dll",
+ "tools/net461-x86/System.ComponentModel.Primitives.dll",
+ "tools/net461-x86/System.ComponentModel.TypeConverter.dll",
+ "tools/net461-x86/System.ComponentModel.dll",
+ "tools/net461-x86/System.Console.dll",
+ "tools/net461-x86/System.Data.Common.dll",
+ "tools/net461-x86/System.Diagnostics.Contracts.dll",
+ "tools/net461-x86/System.Diagnostics.Debug.dll",
+ "tools/net461-x86/System.Diagnostics.DiagnosticSource.dll",
+ "tools/net461-x86/System.Diagnostics.FileVersionInfo.dll",
+ "tools/net461-x86/System.Diagnostics.Process.dll",
+ "tools/net461-x86/System.Diagnostics.StackTrace.dll",
+ "tools/net461-x86/System.Diagnostics.TextWriterTraceListener.dll",
+ "tools/net461-x86/System.Diagnostics.Tools.dll",
+ "tools/net461-x86/System.Diagnostics.TraceSource.dll",
+ "tools/net461-x86/System.Diagnostics.Tracing.dll",
+ "tools/net461-x86/System.Drawing.Primitives.dll",
+ "tools/net461-x86/System.Dynamic.Runtime.dll",
+ "tools/net461-x86/System.Globalization.Calendars.dll",
+ "tools/net461-x86/System.Globalization.Extensions.dll",
+ "tools/net461-x86/System.Globalization.dll",
+ "tools/net461-x86/System.IO.Compression.ZipFile.dll",
+ "tools/net461-x86/System.IO.Compression.dll",
+ "tools/net461-x86/System.IO.FileSystem.DriveInfo.dll",
+ "tools/net461-x86/System.IO.FileSystem.Primitives.dll",
+ "tools/net461-x86/System.IO.FileSystem.Watcher.dll",
+ "tools/net461-x86/System.IO.FileSystem.dll",
+ "tools/net461-x86/System.IO.IsolatedStorage.dll",
+ "tools/net461-x86/System.IO.MemoryMappedFiles.dll",
+ "tools/net461-x86/System.IO.Pipes.dll",
+ "tools/net461-x86/System.IO.UnmanagedMemoryStream.dll",
+ "tools/net461-x86/System.IO.dll",
+ "tools/net461-x86/System.Linq.Expressions.dll",
+ "tools/net461-x86/System.Linq.Parallel.dll",
+ "tools/net461-x86/System.Linq.Queryable.dll",
+ "tools/net461-x86/System.Linq.dll",
+ "tools/net461-x86/System.Memory.dll",
+ "tools/net461-x86/System.Net.Http.dll",
+ "tools/net461-x86/System.Net.NameResolution.dll",
+ "tools/net461-x86/System.Net.NetworkInformation.dll",
+ "tools/net461-x86/System.Net.Ping.dll",
+ "tools/net461-x86/System.Net.Primitives.dll",
+ "tools/net461-x86/System.Net.Requests.dll",
+ "tools/net461-x86/System.Net.Security.dll",
+ "tools/net461-x86/System.Net.Sockets.dll",
+ "tools/net461-x86/System.Net.WebHeaderCollection.dll",
+ "tools/net461-x86/System.Net.WebSockets.Client.dll",
+ "tools/net461-x86/System.Net.WebSockets.dll",
+ "tools/net461-x86/System.Numerics.Vectors.dll",
+ "tools/net461-x86/System.ObjectModel.dll",
+ "tools/net461-x86/System.Reflection.Extensions.dll",
+ "tools/net461-x86/System.Reflection.Primitives.dll",
+ "tools/net461-x86/System.Reflection.dll",
+ "tools/net461-x86/System.Resources.Reader.dll",
+ "tools/net461-x86/System.Resources.ResourceManager.dll",
+ "tools/net461-x86/System.Resources.Writer.dll",
+ "tools/net461-x86/System.Runtime.CompilerServices.Unsafe.dll",
+ "tools/net461-x86/System.Runtime.CompilerServices.VisualC.dll",
+ "tools/net461-x86/System.Runtime.Extensions.dll",
+ "tools/net461-x86/System.Runtime.Handles.dll",
+ "tools/net461-x86/System.Runtime.InteropServices.RuntimeInformation.dll",
+ "tools/net461-x86/System.Runtime.InteropServices.dll",
+ "tools/net461-x86/System.Runtime.Numerics.dll",
+ "tools/net461-x86/System.Runtime.Serialization.Formatters.dll",
+ "tools/net461-x86/System.Runtime.Serialization.Json.dll",
+ "tools/net461-x86/System.Runtime.Serialization.Primitives.dll",
+ "tools/net461-x86/System.Runtime.Serialization.Xml.dll",
+ "tools/net461-x86/System.Runtime.dll",
+ "tools/net461-x86/System.Security.Claims.dll",
+ "tools/net461-x86/System.Security.Cryptography.Algorithms.dll",
+ "tools/net461-x86/System.Security.Cryptography.Csp.dll",
+ "tools/net461-x86/System.Security.Cryptography.Encoding.dll",
+ "tools/net461-x86/System.Security.Cryptography.Primitives.dll",
+ "tools/net461-x86/System.Security.Cryptography.X509Certificates.dll",
+ "tools/net461-x86/System.Security.Principal.dll",
+ "tools/net461-x86/System.Security.SecureString.dll",
+ "tools/net461-x86/System.Text.Encoding.Extensions.dll",
+ "tools/net461-x86/System.Text.Encoding.dll",
+ "tools/net461-x86/System.Text.RegularExpressions.dll",
+ "tools/net461-x86/System.Threading.Overlapped.dll",
+ "tools/net461-x86/System.Threading.Tasks.Parallel.dll",
+ "tools/net461-x86/System.Threading.Tasks.dll",
+ "tools/net461-x86/System.Threading.Thread.dll",
+ "tools/net461-x86/System.Threading.ThreadPool.dll",
+ "tools/net461-x86/System.Threading.Timer.dll",
+ "tools/net461-x86/System.Threading.dll",
+ "tools/net461-x86/System.ValueTuple.dll",
+ "tools/net461-x86/System.Xml.ReaderWriter.dll",
+ "tools/net461-x86/System.Xml.XDocument.dll",
+ "tools/net461-x86/System.Xml.XPath.XDocument.dll",
+ "tools/net461-x86/System.Xml.XPath.dll",
+ "tools/net461-x86/System.Xml.XmlDocument.dll",
+ "tools/net461-x86/System.Xml.XmlSerializer.dll",
+ "tools/net461-x86/netstandard.dll",
+ "tools/net461/GetDocument.Insider.exe",
+ "tools/net461/GetDocument.Insider.exe.config",
+ "tools/net461/Microsoft.Win32.Primitives.dll",
+ "tools/net461/System.AppContext.dll",
+ "tools/net461/System.Buffers.dll",
+ "tools/net461/System.Collections.Concurrent.dll",
+ "tools/net461/System.Collections.NonGeneric.dll",
+ "tools/net461/System.Collections.Specialized.dll",
+ "tools/net461/System.Collections.dll",
+ "tools/net461/System.ComponentModel.EventBasedAsync.dll",
+ "tools/net461/System.ComponentModel.Primitives.dll",
+ "tools/net461/System.ComponentModel.TypeConverter.dll",
+ "tools/net461/System.ComponentModel.dll",
+ "tools/net461/System.Console.dll",
+ "tools/net461/System.Data.Common.dll",
+ "tools/net461/System.Diagnostics.Contracts.dll",
+ "tools/net461/System.Diagnostics.Debug.dll",
+ "tools/net461/System.Diagnostics.DiagnosticSource.dll",
+ "tools/net461/System.Diagnostics.FileVersionInfo.dll",
+ "tools/net461/System.Diagnostics.Process.dll",
+ "tools/net461/System.Diagnostics.StackTrace.dll",
+ "tools/net461/System.Diagnostics.TextWriterTraceListener.dll",
+ "tools/net461/System.Diagnostics.Tools.dll",
+ "tools/net461/System.Diagnostics.TraceSource.dll",
+ "tools/net461/System.Diagnostics.Tracing.dll",
+ "tools/net461/System.Drawing.Primitives.dll",
+ "tools/net461/System.Dynamic.Runtime.dll",
+ "tools/net461/System.Globalization.Calendars.dll",
+ "tools/net461/System.Globalization.Extensions.dll",
+ "tools/net461/System.Globalization.dll",
+ "tools/net461/System.IO.Compression.ZipFile.dll",
+ "tools/net461/System.IO.Compression.dll",
+ "tools/net461/System.IO.FileSystem.DriveInfo.dll",
+ "tools/net461/System.IO.FileSystem.Primitives.dll",
+ "tools/net461/System.IO.FileSystem.Watcher.dll",
+ "tools/net461/System.IO.FileSystem.dll",
+ "tools/net461/System.IO.IsolatedStorage.dll",
+ "tools/net461/System.IO.MemoryMappedFiles.dll",
+ "tools/net461/System.IO.Pipes.dll",
+ "tools/net461/System.IO.UnmanagedMemoryStream.dll",
+ "tools/net461/System.IO.dll",
+ "tools/net461/System.Linq.Expressions.dll",
+ "tools/net461/System.Linq.Parallel.dll",
+ "tools/net461/System.Linq.Queryable.dll",
+ "tools/net461/System.Linq.dll",
+ "tools/net461/System.Memory.dll",
+ "tools/net461/System.Net.Http.dll",
+ "tools/net461/System.Net.NameResolution.dll",
+ "tools/net461/System.Net.NetworkInformation.dll",
+ "tools/net461/System.Net.Ping.dll",
+ "tools/net461/System.Net.Primitives.dll",
+ "tools/net461/System.Net.Requests.dll",
+ "tools/net461/System.Net.Security.dll",
+ "tools/net461/System.Net.Sockets.dll",
+ "tools/net461/System.Net.WebHeaderCollection.dll",
+ "tools/net461/System.Net.WebSockets.Client.dll",
+ "tools/net461/System.Net.WebSockets.dll",
+ "tools/net461/System.Numerics.Vectors.dll",
+ "tools/net461/System.ObjectModel.dll",
+ "tools/net461/System.Reflection.Extensions.dll",
+ "tools/net461/System.Reflection.Primitives.dll",
+ "tools/net461/System.Reflection.dll",
+ "tools/net461/System.Resources.Reader.dll",
+ "tools/net461/System.Resources.ResourceManager.dll",
+ "tools/net461/System.Resources.Writer.dll",
+ "tools/net461/System.Runtime.CompilerServices.Unsafe.dll",
+ "tools/net461/System.Runtime.CompilerServices.VisualC.dll",
+ "tools/net461/System.Runtime.Extensions.dll",
+ "tools/net461/System.Runtime.Handles.dll",
+ "tools/net461/System.Runtime.InteropServices.RuntimeInformation.dll",
+ "tools/net461/System.Runtime.InteropServices.dll",
+ "tools/net461/System.Runtime.Numerics.dll",
+ "tools/net461/System.Runtime.Serialization.Formatters.dll",
+ "tools/net461/System.Runtime.Serialization.Json.dll",
+ "tools/net461/System.Runtime.Serialization.Primitives.dll",
+ "tools/net461/System.Runtime.Serialization.Xml.dll",
+ "tools/net461/System.Runtime.dll",
+ "tools/net461/System.Security.Claims.dll",
+ "tools/net461/System.Security.Cryptography.Algorithms.dll",
+ "tools/net461/System.Security.Cryptography.Csp.dll",
+ "tools/net461/System.Security.Cryptography.Encoding.dll",
+ "tools/net461/System.Security.Cryptography.Primitives.dll",
+ "tools/net461/System.Security.Cryptography.X509Certificates.dll",
+ "tools/net461/System.Security.Principal.dll",
+ "tools/net461/System.Security.SecureString.dll",
+ "tools/net461/System.Text.Encoding.Extensions.dll",
+ "tools/net461/System.Text.Encoding.dll",
+ "tools/net461/System.Text.RegularExpressions.dll",
+ "tools/net461/System.Threading.Overlapped.dll",
+ "tools/net461/System.Threading.Tasks.Parallel.dll",
+ "tools/net461/System.Threading.Tasks.dll",
+ "tools/net461/System.Threading.Thread.dll",
+ "tools/net461/System.Threading.ThreadPool.dll",
+ "tools/net461/System.Threading.Timer.dll",
+ "tools/net461/System.Threading.dll",
+ "tools/net461/System.ValueTuple.dll",
+ "tools/net461/System.Xml.ReaderWriter.dll",
+ "tools/net461/System.Xml.XDocument.dll",
+ "tools/net461/System.Xml.XPath.XDocument.dll",
+ "tools/net461/System.Xml.XPath.dll",
+ "tools/net461/System.Xml.XmlDocument.dll",
+ "tools/net461/System.Xml.XmlSerializer.dll",
+ "tools/net461/netstandard.dll",
+ "tools/netcoreapp2.1/GetDocument.Insider.deps.json",
+ "tools/netcoreapp2.1/GetDocument.Insider.dll",
+ "tools/netcoreapp2.1/GetDocument.Insider.runtimeconfig.json",
+ "tools/netcoreapp2.1/System.Diagnostics.DiagnosticSource.dll"
+ ]
+ },
+ "Microsoft.OpenApi/1.6.14": {
+ "sha512": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==",
+ "type": "package",
+ "path": "microsoft.openapi/1.6.14",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "README.md",
+ "lib/netstandard2.0/Microsoft.OpenApi.dll",
+ "lib/netstandard2.0/Microsoft.OpenApi.pdb",
+ "lib/netstandard2.0/Microsoft.OpenApi.xml",
+ "microsoft.openapi.1.6.14.nupkg.sha512",
+ "microsoft.openapi.nuspec"
+ ]
+ },
+ "Swashbuckle.AspNetCore/6.9.0": {
+ "sha512": "lvI+XHF21tkwXd2nDCLGJsdhdUYsY3Ax2fWUlvw81Oa6EedtnIAf5tThy8ZnPcz/9/TwsLgjgtX9ifOCIjbEPA==",
+ "type": "package",
+ "path": "swashbuckle.aspnetcore/6.9.0",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "build/Swashbuckle.AspNetCore.props",
+ "buildMultiTargeting/Swashbuckle.AspNetCore.props",
+ "swashbuckle.aspnetcore.6.9.0.nupkg.sha512",
+ "swashbuckle.aspnetcore.nuspec"
+ ]
+ },
+ "Swashbuckle.AspNetCore.Swagger/6.9.0": {
+ "sha512": "P316kpxx5DnDvJwNWW8iTAXkh9DVenAxFGe9v4OUS0gil+vitH7F1feXhCtVeHN/616EFNTMh4pV2lcr9kkw/w==",
+ "type": "package",
+ "path": "swashbuckle.aspnetcore.swagger/6.9.0",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "lib/net5.0/Swashbuckle.AspNetCore.Swagger.dll",
+ "lib/net5.0/Swashbuckle.AspNetCore.Swagger.pdb",
+ "lib/net5.0/Swashbuckle.AspNetCore.Swagger.xml",
+ "lib/net6.0/Swashbuckle.AspNetCore.Swagger.dll",
+ "lib/net6.0/Swashbuckle.AspNetCore.Swagger.pdb",
+ "lib/net6.0/Swashbuckle.AspNetCore.Swagger.xml",
+ "lib/net7.0/Swashbuckle.AspNetCore.Swagger.dll",
+ "lib/net7.0/Swashbuckle.AspNetCore.Swagger.pdb",
+ "lib/net7.0/Swashbuckle.AspNetCore.Swagger.xml",
+ "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll",
+ "lib/net8.0/Swashbuckle.AspNetCore.Swagger.pdb",
+ "lib/net8.0/Swashbuckle.AspNetCore.Swagger.xml",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.Swagger.dll",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.Swagger.pdb",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.Swagger.xml",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.Swagger.dll",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.Swagger.pdb",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.Swagger.xml",
+ "package-readme.md",
+ "swashbuckle.aspnetcore.swagger.6.9.0.nupkg.sha512",
+ "swashbuckle.aspnetcore.swagger.nuspec"
+ ]
+ },
+ "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": {
+ "sha512": "FjeMR3fBzwVc5plfYjoHw9ptf8SOWMupvO9X35J5EgzT3L9dRqSxa+cBKzL8PwCyemY0xNrggQSB5+MFWx1axg==",
+ "type": "package",
+ "path": "swashbuckle.aspnetcore.swaggergen/6.9.0",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "lib/net5.0/Swashbuckle.AspNetCore.SwaggerGen.dll",
+ "lib/net5.0/Swashbuckle.AspNetCore.SwaggerGen.pdb",
+ "lib/net5.0/Swashbuckle.AspNetCore.SwaggerGen.xml",
+ "lib/net6.0/Swashbuckle.AspNetCore.SwaggerGen.dll",
+ "lib/net6.0/Swashbuckle.AspNetCore.SwaggerGen.pdb",
+ "lib/net6.0/Swashbuckle.AspNetCore.SwaggerGen.xml",
+ "lib/net7.0/Swashbuckle.AspNetCore.SwaggerGen.dll",
+ "lib/net7.0/Swashbuckle.AspNetCore.SwaggerGen.pdb",
+ "lib/net7.0/Swashbuckle.AspNetCore.SwaggerGen.xml",
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll",
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.pdb",
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.xml",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerGen.dll",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerGen.pdb",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerGen.xml",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerGen.dll",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerGen.pdb",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerGen.xml",
+ "package-readme.md",
+ "swashbuckle.aspnetcore.swaggergen.6.9.0.nupkg.sha512",
+ "swashbuckle.aspnetcore.swaggergen.nuspec"
+ ]
+ },
+ "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": {
+ "sha512": "0OxlWBFLl2gUESZX/K7QCTz9KctKy0VxHTvLIBcyWGD4z/fv5MCMW02qzYGcReLJr4yBnNDRzApKtLh6oBpe9A==",
+ "type": "package",
+ "path": "swashbuckle.aspnetcore.swaggerui/6.9.0",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "lib/net5.0/Swashbuckle.AspNetCore.SwaggerUI.dll",
+ "lib/net5.0/Swashbuckle.AspNetCore.SwaggerUI.pdb",
+ "lib/net5.0/Swashbuckle.AspNetCore.SwaggerUI.xml",
+ "lib/net6.0/Swashbuckle.AspNetCore.SwaggerUI.dll",
+ "lib/net6.0/Swashbuckle.AspNetCore.SwaggerUI.pdb",
+ "lib/net6.0/Swashbuckle.AspNetCore.SwaggerUI.xml",
+ "lib/net7.0/Swashbuckle.AspNetCore.SwaggerUI.dll",
+ "lib/net7.0/Swashbuckle.AspNetCore.SwaggerUI.pdb",
+ "lib/net7.0/Swashbuckle.AspNetCore.SwaggerUI.xml",
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll",
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.pdb",
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.xml",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerUI.dll",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerUI.pdb",
+ "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerUI.xml",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerUI.dll",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerUI.pdb",
+ "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerUI.xml",
+ "package-readme.md",
+ "swashbuckle.aspnetcore.swaggerui.6.9.0.nupkg.sha512",
+ "swashbuckle.aspnetcore.swaggerui.nuspec"
+ ]
+ }
+ },
+ "projectFileDependencyGroups": {
+ "net8.0": [
+ "Swashbuckle.AspNetCore >= 6.9.0"
+ ]
+ },
+ "packageFolders": {
+ "C:\\Users\\GamingPC\\.nuget\\packages\\": {},
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
+ },
+ "project": {
+ "version": "1.0.0",
+ "restore": {
+ "projectUniqueName": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj",
+ "projectName": "Csob",
+ "projectPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj",
+ "packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\",
+ "outputPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\obj\\",
+ "projectStyle": "PackageReference",
+ "fallbackFolders": [
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
+ ],
+ "configFilePaths": [
+ "C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config",
+ "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
+ "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
+ ],
+ "originalTargetFrameworks": [
+ "net8.0"
+ ],
+ "sources": {
+ "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
+ "C:\\Program Files\\dotnet\\library-packs": {},
+ "D:\\CustomNuGetPackages": {},
+ "https://api.nuget.org/v3/index.json": {}
+ },
+ "frameworks": {
+ "net8.0": {
+ "targetAlias": "net8.0",
+ "projectReferences": {}
+ }
+ },
+ "warningProperties": {
+ "warnAsError": [
+ "NU1605"
+ ]
+ },
+ "restoreAuditProperties": {
+ "enableAudit": "true",
+ "auditLevel": "low",
+ "auditMode": "direct"
+ },
+ "SdkAnalysisLevel": "10.0.200"
+ },
+ "frameworks": {
+ "net8.0": {
+ "targetAlias": "net8.0",
+ "dependencies": {
+ "Swashbuckle.AspNetCore": {
+ "target": "Package",
+ "version": "[6.9.0, )"
+ }
+ },
+ "imports": [
+ "net461",
+ "net462",
+ "net47",
+ "net471",
+ "net472",
+ "net48",
+ "net481"
+ ],
+ "assetTargetFallback": true,
+ "warn": true,
+ "frameworkReferences": {
+ "Microsoft.AspNetCore.App": {
+ "privateAssets": "none"
+ },
+ "Microsoft.NETCore.App": {
+ "privateAssets": "all"
+ }
+ },
+ "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.204/PortableRuntimeIdentifierGraph.json"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/obj/project.nuget.cache b/obj/project.nuget.cache
new file mode 100644
index 0000000..57542e8
--- /dev/null
+++ b/obj/project.nuget.cache
@@ -0,0 +1,15 @@
+{
+ "version": 2,
+ "dgSpecHash": "oxCvoNmhuPk=",
+ "success": true,
+ "projectFilePath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj",
+ "expectedPackageFiles": [
+ "C:\\Users\\GamingPC\\.nuget\\packages\\microsoft.extensions.apidescription.server\\6.0.5\\microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512",
+ "C:\\Users\\GamingPC\\.nuget\\packages\\microsoft.openapi\\1.6.14\\microsoft.openapi.1.6.14.nupkg.sha512",
+ "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore\\6.9.0\\swashbuckle.aspnetcore.6.9.0.nupkg.sha512",
+ "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore.swagger\\6.9.0\\swashbuckle.aspnetcore.swagger.6.9.0.nupkg.sha512",
+ "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore.swaggergen\\6.9.0\\swashbuckle.aspnetcore.swaggergen.6.9.0.nupkg.sha512",
+ "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore.swaggerui\\6.9.0\\swashbuckle.aspnetcore.swaggerui.6.9.0.nupkg.sha512"
+ ],
+ "logs": []
+}
\ No newline at end of file