Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions brand-assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Open Bank Project Brand Assets

Official Open Bank Project logos, kept in this repository so that documentation,
diagrams and glossary items can reference them from a stable, version-controlled
location instead of hotlinking the website.

Source: https://www.openbankproject.com/brand-assets/ (downloaded 2026-08-25).
These assets are designated for **press and official partner use only**.

## Files

| File | Size | Use |
|---|---|---|
| `horizontal/obp-logo-horizontal.svg` | vector | Preferred where SVG is supported |
| `horizontal/obp-logo-horizontal-colour.png` | 1000×127 | Full logo, light backgrounds |
| `horizontal/obp-logo-horizontal-white.png` | 1000×127 | Full logo, dark backgrounds |
| `horizontal/obp-logo-horizontal-green-white.png` | 1000×127 | Green/white variant |
| `horizontal/obp-logo-horizontal-colour-small.png` | 206×28 | Small inline use |
| `vertical/obp-logo-vertical-*.png` | 421×202 / 154×78 | Stacked logo variants |
| `icon/obp-logo-icon-colour.png` | 79×78 | Icon only, light backgrounds |
| `icon/obp-logo-icon-dark.png` | 79×78 | Icon only, dark variant |
| `icon/obp-logo-icon-white.png` | 79×78 | Icon only, dark backgrounds |

## Hotlinking

Once merged to the `develop` branch of `OpenBankProject/OBP-API`, files can be
referenced from markdown and diagrams via, e.g.:

```
https://raw.githubusercontent.com/OpenBankProject/OBP-API/develop/brand-assets/horizontal/obp-logo-horizontal-colour.png
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions brand-assets/horizontal/obp-logo-horizontal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand-assets/icon/obp-logo-icon-colour.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand-assets/icon/obp-logo-icon-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand-assets/icon/obp-logo-icon-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand-assets/vertical/obp-logo-vertical-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,24 @@ object DynamicEndpointCodeGenerator {
| // if the requestUrl of resourceDoc is /hello/banks/BANK_ID/world
| // the request path is /hello/banks/bank_x/world
| //pathParams.get("BANK_ID") will get Option("bank_x") value
| val pathParams = getPathParams(callContext, request)
| $variables
|""".stripMargin
} else ""

val (requestBodyCaseClasses, responseBodyCaseClasses) = buildCaseClasses(fragment.exampleRequestBody, fragment.successResponseBody)

def requestEntityExp(str:String) =
s""" val requestEntity = request.json match {
| case Full(zson) =>
| try {
| zson.extract[$str]
| } catch {
| case e: MappingException =>
| return Full(errorJsonResponse(s"$$InvalidJsonFormat $${e.msg}"))
| }
| case _: EmptyBox =>
| return Full(errorJsonResponse(s"$$InvalidRequestPayload Current request has no payload"))
| }
s""" val requestEntity = callContext.httpBody.filter(_.nonEmpty) match {
| case Some(rawBody) =>
| try {
| com.openbankproject.commons.util.JsonAliases.parse(rawBody).extract[$str]
| } catch {
| case e: MappingException =>
| return errorResponse(s"$$InvalidJsonFormat $${e.msg}")
| }
| case None =>
| return errorResponse(s"$$InvalidRequestPayload Current request has no payload")
| }
|""".stripMargin

val requestEntity = fragment.exampleRequestBody match {
Expand Down Expand Up @@ -71,11 +70,12 @@ object DynamicEndpointCodeGenerator {
| val requestUrl = "${fragment.requestUrl}"
|
| // copy the whole method body as "dynamicResourceDoc" method body
| override protected def process(callContext: CallContext, request: Req): Box[JsonResponse] = {
| override protected def process(callContext: CallContext, request: Request[IO], pathParams: Map[String, String]): IO[Response[IO]] = {
| // please add import sentences here, those used by this method
|
| val Some(resourceDoc) = callContext.resourceDocument
| val hasRequestBody = request.body.isDefined
| // the request body is available as a String on the CallContext (read by Http4sCallContextBuilder)
| val hasRequestBody = callContext.httpBody.exists(_.nonEmpty)
|
|$pathVariables
|
Expand Down
12 changes: 12 additions & 0 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3254,6 +3254,18 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
base64EncodedSha256(in)
}

/**
* Lower-case hex SHA-256 of the given string (UTF-8). Used to fingerprint the source of
* runtime-compiled dynamic code (e.g. a Dynamic Resource Doc's method body) so that a stored
* record carries an integrity hash: it lets an operator answer "has this code changed since it
* was created?" without diffing the raw body, and is the value a future code-signing / approval
* step signs over.
*/
def sha256Hex(in: String): String = {
val digest = java.security.MessageDigest.getInstance("SHA-256").digest(in.getBytes("UTF-8"))
digest.map(b => f"$b%02x").mkString
}

/**
* Create the explicit CounterpartyId, (Used in `Create counterparty for an account` endpoint ).
* This is just a UUID, use both in Counterparty.counterpartyId and CounterpartyMetadata.counterpartyId
Expand Down
97 changes: 95 additions & 2 deletions obp-api/src/main/scala/code/api/util/Glossary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3535,7 +3535,7 @@
""".stripMargin)

glossaryItems += GlossaryItem(
title = "Dynamic Endpoint Manage",

Check failure on line 3538 in obp-api/src/main/scala/code/api/util/Glossary.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Dynamic Endpoint Manage" 3 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AaA3C1KuhKBcFfomRL4q&open=AaA3C1KuhKBcFfomRL4q&pullRequest=2893
description =
s"""
|
Expand Down Expand Up @@ -3589,6 +3589,89 @@
| * [Introduction to Dynamic Endpoints](https://vimeo.com/426235612)
| * [Features of Dynamic Endpoints](https://vimeo.com/444133309)
|
""".stripMargin)

glossaryItems += GlossaryItem(
title = "Dynamic Resource Doc",
description =
s"""
|A Dynamic Resource Doc defines a *single* Endpoint at runtime: its verb, URL path, summary, description, example request and response bodies, error list, tags and Roles - plus a *method body* written in Scala which is compiled at runtime and becomes the handler of the Endpoint.
|
|Whereas a Dynamic Endpoint (see ${getGlossaryItemLink("Dynamic Endpoint Manage")}) is created from a Swagger / OpenAPI file and contains *no code* (its behaviour is selected by the swagger `host` field), a Dynamic Resource Doc *is* code: the method body has access to the full CallContext and can transform payloads, call Connector methods and NewStyle functions, or invoke Dynamic Message Docs.
|
|Like all Resource Docs, Dynamic Resource Docs are part of the server registry of the API (see ${getGlossaryItemLink("Resource Doc")}), so they appear in the API Explorer and resource-docs endpoints like any Static endpoint.
|
|Dynamic Resource Docs can be created at System level or Bank / Space level, and are served under the `/obp/dynamic-endpoint/dynamic-resource-doc` path prefix (configurable via the `url.prefix.dynamic.resourceDoc` prop).
|
|Authentication and Role checks are applied to the compiled endpoint exactly as for Static endpoints - including the checks that run inside the shared authentication step: Consumer disabled, User locked / deleted, Consent processing and Rate Limiting.
|
|Some cross-cutting features of the Static pipeline do *not* currently apply to runtime-compiled Dynamic Resource Doc endpoints: API Metrics are not recorded, the JSON Schema Validation and Force-Error interceptors are not run, the Idempotency-Key mechanism is unavailable, and handlers run on auto-commit (no request-scoped database transaction). Dynamic Endpoints created from Swagger (the proxy path) *do* record Metrics and *do* run the JSON Schema Validation interceptors.
|
|Because the method body is user-supplied code compiled at runtime, this feature is guarded by the `allow_user_generated_scala_code` prop (default: false) and the Roles CanCreateDynamicResourceDoc / CanCreateBankLevelDynamicResourceDoc etc.
|
|A helper endpoint (`POST /management/dynamic-resource-docs/endpoint-code`) can generate a method-body template from example request / response bodies.
|
|See ${getGlossaryItemLink("Dynamic Code Paths")} for how Dynamic Resource Docs relate to the other runtime-defined building blocks.
|
""".stripMargin)

glossaryItems += GlossaryItem(
title = "Dynamic Code Paths",
description =
s"""
|OBP offers several building blocks for defining API behaviour at *runtime* - stored in the OBP database as instance configuration rather than compiled into the source code. This item explains how they fit together.
|
|**The building blocks**
|
|At the *API surface* layer (what URL / verb exists, who may call it):
|
|1) **Dynamic Endpoint** (${getGlossaryItemLink("Dynamic Endpoint Manage")}) - created from a Swagger / OpenAPI file. No code. Every operation in the file becomes a live endpoint with an auto-generated Role.
|
|2) **Dynamic Resource Doc** (${getGlossaryItemLink("Dynamic Resource Doc")}) - one endpoint definition *plus* a Scala method body compiled at runtime. The code is the handler.
|
|At the *Connector* layer (how a backend system is reached):
|
|3) **Method Routing** (${getGlossaryItemLink("Method Routing")}) - a routing rule that selects which Connector implementation serves a given Connector method (per bank, per URL pattern etc.). Pure configuration, no code.
|
|4) **Connector Method** (${getGlossaryItemLink("Connector Method")}) - a runtime-compiled body (Scala, Java or JavaScript) for one of the *existing* methods of the Connector trait (e.g. getBanks, makePaymentv210, dynamicEndpointProcess). Executed when a Method Routing rule routes that method to `connector = internal`.
|
|5) **Dynamic Message Doc** (${getGlossaryItemLink("Dynamic Message Doc")}) - a runtime-compiled function keyed by a *process name*, for logic that does not correspond to an existing Connector method. Invoked from other dynamic code (or by Dynamic Entity storage operations).
|
|Related: **Dynamic Entities** (${getGlossaryItemLink("Dynamic-Entities")}) provide runtime-defined data storage, and **Endpoint Mapping** (${getGlossaryItemLink("Endpoint Mapping")}) maps Dynamic Endpoint JSON fields onto Dynamic Entity fields.
|
|**How they compose - the paths**
|
|```
| +--> host=obp_mock ......... returns swagger example (mock)
| |
| Dynamic Endpoint (swagger) --+--> host=dynamic_entity ... Endpoint Mapping
| no code | -> Dynamic Entity storage (data-backed)
| |
| +--> any other host ......... Method Routing:
| connector=rest -> HTTP proxy to backend
| connector=internal -> Connector Method (code)
|
| Dynamic Resource Doc ------------> compiled Scala handler
| code at the endpoint layer |-> Connector methods (routed by Method Routing)
| |-> Dynamic Message Docs (by process name)
| |-> any transformation / orchestration logic
|```
|
|**Choosing a path**
|
|* Need a quick mock of an API from its spec? Dynamic Endpoint with `host = obp_mock`.
|* Need a data-backed CRUD API with no code? Dynamic Endpoint with `host = dynamic_entity` + Endpoint Mapping + a Dynamic Entity.
|* Need to pass requests through to an existing backend *unchanged*? Dynamic Endpoint + Method Routing with a `url` parameter (transparent HTTP proxy - no payload transformation, no credential minting).
|* Need transformation, authentication against the backend, error mapping or orchestration? Use code: either a Dynamic Resource Doc (code at the endpoint layer - one self-contained artifact per endpoint) or a Connector Method (code at the connector seam - keeps backend integration reusable across endpoints and swappable via Method Routing). These combine well: Dynamic Resource Docs for the API surface, Connector Methods / Dynamic Message Docs for the backend calls.
|
|**Static vs Dynamic**
|
|Static endpoints (${getGlossaryItemLink("Static Endpoint")}) are Scala source code in Git, changed via release and restart. All the dynamic building blocks above live in the OBP database of the instance: they can be created and changed in real time over the management API (or via the API Manager UI) with *no code deployment and no restart*, and they never require instance-specific code in the public source repositories.
|
|**Guards**
|
|Runtime-compiled code (Dynamic Resource Docs, Connector Methods, Dynamic Message Docs) is disabled unless the `allow_user_generated_scala_code` prop is set to true, and every creation endpoint requires its corresponding Role. Dynamic Endpoints (swagger, no code) are not affected by that prop; each generated endpoint is protected by its own auto-generated Role.
|
""".stripMargin)

glossaryItems += GlossaryItem(
Expand Down Expand Up @@ -3929,8 +4012,6 @@
|
|You can also use these endpoints to create your own helper methods in OBP code.
|
| This feature is somewhat work in progress (WIP).
|
|The following videos are available:
|* [Introduction to Dynamic Message Doc] (https://vimeo.com/623317747)
|
Expand Down Expand Up @@ -5894,6 +5975,14 @@
|└──────────────────┘ └────────────────────────┘ └──────────────┘
|```
|
|## Architecture diagram
|
|The full picture — Portal/API Explorer, Opey, external MCP clients (Claude Code, Claude Desktop, IDE agents), OBP-OIDC, the numbered consent flow, and OBP-API down to the core banking systems:
|
|![How Opey, Claude Code and OBP-MCP call OBP-API](https://github.com/user-attachments/assets/d3ff5c10-7167-4034-98f7-c53a323bf985)
|
|The editable master is a Lucidchart document linked from the [OBP-MCP README](https://github.com/OpenBankProject/OBP-MCP#architecture).
|
|## Three-step discovery + call (no RAG, no vector DB)
|
|OBP-MCP avoids embedding the 4 MB OpenAPI spec into the LLM's context. Instead it exposes three tools that work together:
Expand Down Expand Up @@ -5954,6 +6043,10 @@
|
|Since [OBP-MCP](/glossary#OBP-MCP) was introduced, Opey has been refactored from a self-contained chatbot (with its own endpoint search, glossary search, and OBP HTTP client baked in) into a focused **agent** that *consumes* OBP-MCP as its primary tool source.
|
|![How Opey, Claude Code and OBP-MCP call OBP-API](https://github.com/user-attachments/assets/d3ff5c10-7167-4034-98f7-c53a323bf985)
|
|Besides the MCP path shown above, Opey makes some direct HTTP calls to OBP-API for its own infrastructure (session validation via `/users/current`, admin DirectLogin operations, persisting LangGraph checkpoints as dynamic entities, and health probes) — see the architecture section of the [Opey README](https://github.com/OpenBankProject/OBP-Opey-II#architecture-how-opey-reaches-the-obp-api) for the detail diagram.
|
|Opey's `mcp_servers.json` typically points at a running OBP-MCP instance:
|
|```json
Expand Down
21 changes: 15 additions & 6 deletions obp-api/src/main/scala/code/api/util/NewStyle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4272,14 +4272,17 @@ object NewStyle extends MdcLoggable{

def createJsonConnectorMethod(connectorMethod: JsonConnectorMethod, callContext: Option[CallContext]): OBPReturnType[JsonConnectorMethod] =
Future {
val newInternalConnector = ConnectorMethodProvider.provider.vend.create(connectorMethod)
// provenance is taken from the authenticated CallContext user, never from the request body
val createdByUserId = callContext.flatMap(_.user).map(_.userId)
val newInternalConnector = ConnectorMethodProvider.provider.vend.create(connectorMethod, createdByUserId)
val errorMsg = s"$UnknownError Can not create Connector Method in the backend. "
(unboxFullOrFail(newInternalConnector, callContext, errorMsg, 400), callContext)
}

def updateJsonConnectorMethod(connectorMethodId: String, connectorMethodBody: String, programmingLang: String, callContext: Option[CallContext]): OBPReturnType[JsonConnectorMethod] =
Future {
val updatedConnectorMethod = ConnectorMethodProvider.provider.vend.update(connectorMethodId, connectorMethodBody, programmingLang)
val updatedByUserId = callContext.flatMap(_.user).map(_.userId)
val updatedConnectorMethod = ConnectorMethodProvider.provider.vend.update(connectorMethodId, connectorMethodBody, programmingLang, updatedByUserId)
val errorMsg = s"$UnknownError Can not update Connector Method in the backend. "
(unboxFullOrFail(updatedConnectorMethod, callContext, errorMsg, 400), callContext)
}
Expand Down Expand Up @@ -4316,14 +4319,17 @@ object NewStyle extends MdcLoggable{

def createJsonDynamicResourceDoc(bankId: Option[String], dynamicResourceDoc: JsonDynamicResourceDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicResourceDoc] =
Future {
val newInternalConnector = DynamicResourceDocProvider.provider.vend.create(bankId, dynamicResourceDoc)
// provenance is taken from the authenticated CallContext user, never from the request body
val createdByUserId = callContext.flatMap(_.user).map(_.userId)
val newInternalConnector = DynamicResourceDocProvider.provider.vend.create(bankId, dynamicResourceDoc, createdByUserId)
val errorMsg = s"$UnknownError Can not create Dynamic Resource Doc in the backend. "
(unboxFullOrFail(newInternalConnector, callContext, errorMsg, 400), callContext)
}

def updateJsonDynamicResourceDoc(bankId: Option[String], entity: JsonDynamicResourceDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicResourceDoc] =
Future {
val updatedConnectorMethod = DynamicResourceDocProvider.provider.vend.update(bankId, entity: JsonDynamicResourceDoc)
val updatedByUserId = callContext.flatMap(_.user).map(_.userId)
val updatedConnectorMethod = DynamicResourceDocProvider.provider.vend.update(bankId, entity, updatedByUserId)
val errorMsg = s"$UnknownError Can not update Dynamic Resource Doc in the backend. "
(unboxFullOrFail(updatedConnectorMethod, callContext, errorMsg, 400), callContext)
}
Expand Down Expand Up @@ -4354,14 +4360,17 @@ object NewStyle extends MdcLoggable{

def createJsonDynamicMessageDoc(bankId: Option[String], dynamicMessageDoc: JsonDynamicMessageDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicMessageDoc] =
Future {
val newInternalConnector = DynamicMessageDocProvider.provider.vend.create(bankId, dynamicMessageDoc)
// provenance is taken from the authenticated CallContext user, never from the request body
val createdByUserId = callContext.flatMap(_.user).map(_.userId)
val newInternalConnector = DynamicMessageDocProvider.provider.vend.create(bankId, dynamicMessageDoc, createdByUserId)
val errorMsg = s"$UnknownError Can not create Dynamic Message Doc in the backend. "
(unboxFullOrFail(newInternalConnector, callContext, errorMsg, 400), callContext)
}

def updateJsonDynamicMessageDoc(bankId: Option[String], entity: JsonDynamicMessageDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicMessageDoc] =
Future {
val updatedConnectorMethod = DynamicMessageDocProvider.provider.vend.update(bankId: Option[String], entity: JsonDynamicMessageDoc)
val updatedByUserId = callContext.flatMap(_.user).map(_.userId)
val updatedConnectorMethod = DynamicMessageDocProvider.provider.vend.update(bankId, entity, updatedByUserId)
val errorMsg = s"$UnknownError Can not update Dynamic Message Doc in the backend. "
(unboxFullOrFail(updatedConnectorMethod, callContext, errorMsg, 400), callContext)
}
Expand Down
Loading
Loading