Skip to content

[ANCHOR-1270]: Move client (wallet) config from file to database, with a REST management API and JWT-carried client attribution - #1992

Merged
amandagonsalves merged 15 commits into
developfrom
feat/anchor-1270
Aug 19, 2026
Merged

amandagonsalves merged 15 commits into
developfrom
feat/anchor-1270

Conversation

@amandagonsalves

@amandagonsalves amandagonsalves commented Aug 16, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Raised by an anchor running the Anchor Platform: their partner-config YAML has grown to ~1,200 lines, and every change requires a rolling restart on their end. They asked whether config could move to the AP database instead — either AP checking the DB directly per request, or AP refreshing an in-memory cache every ~60 minutes — so onboarding a new partner (their stated goal is to automate onboarding as much as possible) or updating an existing one no longer needs a restart on their end.

Client (wallet) definitions — SEP-10 signing keys, SEP-24 domains, callback URLs, destination accounts — are configured today via clients.type: file|inline|json|yaml, parsed once into an in-memory list at startup. Every lookup is a linear scan over that list, onboarding or revoking a single wallet means editing one shared YAML/JSON file, and any change requires a full restart. That breaks down past a few hundred clients: at 600+ entries the file becomes unwieldy to hand-edit safely, and there is no way to add or revoke a client without redeploying.

Three designs were prototyped before this one: an in-memory cache refreshed on a timer/signal (the anchor's 60-minute-refresh suggestion; largest diff, up to an hour of staleness), a purely live DB lookup on every request (the anchor's other suggestion — AP checks the DB directly; simplest, but a DB round trip on every SEP-10/24/6/31/12 call), and this one — DB-backed live lookup, but only at SEP-10/SEP-45 auth time. This PR implements the third: SEP-10/SEP-45 resolve the client once via ClientFinder against the database and stamp the result into a new client_name JWT claim; SEP-6/12/24/31 read token.getClientName() directly instead of each doing their own lookup. That removes the per-request database traffic the live-lookup-only design would add on every SEP-6/12/24/31 call, at the cost of client changes not taking effect for a session until its JWT expires (see Known limitations).

A full REST API on the platform server lets anchors manage clients without a restart: PUT/GET/DELETE /clients/{name}, GET /clients (all), GET /clients/custodial / GET /clients/non-custodial (filtered by type), plus incremental POST/DELETE /clients/{name}/signing-keys/{key} and /destination-accounts/{account}. The incremental endpoints exist specifically for high-volume clients — one anchor's production config has a single custodial client with 600+ destination accounts — so adding or revoking one wallet doesn't require resending the entire list. Setting clients.type: db with clients.value still pointing at an existing file/yaml/json source auto-imports those clients into the database on every startup (upsert, not destructive), so an anchor can cut over without hand-transcribing every entry; switching clients.type away from db afterward does not delete the imported rows.

Changes

  • ClientsConfig.java: adds a db ClientsConfigType. (No separate migration flag — see below.)
  • WebAuthJwt.java: adds a client_name claim field, populated from the decoded JWT.
  • JwtService.encode: emits the CLIENT_NAME claim when token.getClientName() is set (the claim key already existed for the SEP-24 interactive/more-info JWTs; this reuses it for the SEP-10/45 web-auth JWT).
  • Sep10Service.generateWebAuthJwt: resolves clientFinder.getClientName(clientDomain, account) once, at token-issuance time, and stamps it onto the token before encoding. validateChallenge/generateWebAuthJwt now declare throws SepException (was SepValidationException) so a SepNotAuthorizedException from client resolution propagates and the login is refused, instead of silently issuing a token with no client name.
  • Sep45Service.validate: same resolve-and-stamp for the SEP-45 contract-account flow; adds a ClientFinder dependency.
  • Sep6Service, Sep24Service, Sep12Service, Sep31Service: drop their own ClientFinder/ClientService lookups; each now reads token.getClientName() (or Context.get().getWebAuthJwt().getClientName() for SEP-31) directly, since SEP-10/45 already vetted it.
  • V32__client_config.sql (new): creates client_config, client_domain, client_signing_key, client_destination_account, with unique indexes on domain and signing_key — a hard constraint the file-based config never enforced.
  • JdbcClientConfig.java (new): JPA entity for the tables above.
  • JdbcClientConfigRepo.java (new): Spring Data repo with indexed findByDomain/findBySigningKey/findByType queries, @EntityGraph-annotated so signingKeys/domains/destinationAccounts are eagerly fetched (avoids a LazyInitializationException when a config bean reads them outside a request-scoped Hibernate session).
  • JdbcClientService.java (new): ClientService implementation backed directly by JdbcClientConfigRepo — every lookup is a live, indexed query, no in-memory copy.
  • PropertyClientsConfig.java: type: db auto-detects and parses clients.value (file path, inline YAML, or inline JSON) into items for import, without clearing any items already bound directly; no separate migration flag.
  • PropertySep10Config.java: clientService field marked transient — it's a JDK dynamic proxy (inherent to any Spring Data repo/service, not specific to @Transactional), and Gson would otherwise try to reflectively serialize it during debug logging and fail on a JPMS InaccessibleObjectException.
  • ClientConfigController.java, ClientConfigRequest.java, ClientConfigResponse.java (new): PUT/GET/GET all/DELETE /clients/{name}, GET /clients/custodial, GET /clients/non-custodial, and POST/DELETE /clients/{name}/signing-keys/{key} + /destination-accounts/{account}, behind the same auth as the rest of the Platform API (see Known limitations).
  • ClientConfigService.java (new): validates and upserts/gets/lists/deletes clients, plus the incremental add/remove-signing-key and add/remove-destination-account operations (rejecting removal of a custodial client's last signing key); translates a DataIntegrityViolationException (duplicate domain/signing key) into a clean 400 instead of a raw SQL error.
  • ClientConfigImportRunner.java (new): a CommandLineRunner, gated only on clients.type=db, that imports every client currently resolvable from clients.value into the database on each startup (upsert — safe to run repeatedly), logging and skipping any individual client that fails to import rather than aborting startup.
  • ClientsBeans.clientService: returns JdbcClientService when clients.type: db, otherwise the existing DefaultClientService.
  • PlatformServerBeans.java: registers ClientConfigService and the conditional ClientConfigImportRunner.
  • SepBeans.java: drops ClientFinder/ClientService constructor args from the SEP-6/12/24/31 beans; adds sep45 to ClientFinder's enabled-SEPs list and wires it into the Sep45Service bean.
  • Sep10Controller.validateChallenge: throws clause widened to SepException.
  • TestProfileRunner.kt: fixes a test-harness bug where inline clients.value content (JSON/YAML, not a file path) was unconditionally treated as a classpath resource and crashed with RuntimeException("Resource ... not found"); now falls back to treating it as inline content with a warning log.
  • anchor-config-default-values.yaml: documents the new type: db option and its auto-import behavior (corrected from an earlier draft of this doc that described a since-removed migrate_from_file_on_startup flag).
  • Sep10ServiceTest.kt, Sep45ServiceTest.kt: new cases asserting client_name is stamped onto the JWT, and that a ClientFinder authorization failure propagates instead of issuing a token with no client name.
  • Sep12ServiceTest.kt, Sep24ServiceTest.kt, Sep31ServiceTest.kt, Sep6ServiceTest.kt: updated to set/read token.getClientName() directly instead of mocking ClientFinder.
  • JdbcClientServiceTest.kt (new, 8 tests): domain/signing-key/name resolution, domain-preferred-over-account-match precedence, custodial-vs-noncustodial filtering.
  • PropertyClientsConfigTest.kt (new, 5 tests): type: db with no value, with a YAML/JSON string, with a file path, and with items already bound directly — none of the four clear or duplicate the item list.
  • ClientConfigImportRunnerTest.kt (new, 3 tests): upserts every item, continues past a single item's import failure, no-ops on an empty list.
  • ClientConfigServiceTest.kt (29 tests): full coverage of validation, upsert-conflict handling, list/get/delete, and the incremental signing-key/destination-account add/remove operations including the custodial-last-key protection and cross-client uniqueness conflicts.
  • ClientConfigApiTests.kt (essential-tests, 19 tests): real-HTTP coverage against a live Postgres for the full /clients REST surface — CRUD lifecycle, 404s, validation (missing signing keys, no domain on a noncustodial client, malformed callback URL, unknown type, domain/signing-key collisions across clients), the four incremental endpoints (additive without disturbing existing entries, 404 on an unknown sub-resource, cross-client conflict, last-signing-key protection), and the two type-filtered list endpoints.
  • ClientsDbAttributionTests.kt + ClientsDbTestSuite.kt (new, extended-tests, 4 tests): the one thing essential-tests can't cover, since it only runs against the default profile's clients.type: file. A new clients-db service-runner profile (clients.type: db, wired into sub_extended_tests.yml) boots a real server with the DB-backed path active, and this suite runs an actual SEP-10 challenge/validate against it, decodes the resulting JWT, and asserts client_name is attributed correctly on create, follows a signing key moved between clients, stays unset for an unregistered key, and disappears after the client is deleted.

Acceptance Criteria

  • PUT /clients/{name} with a valid custodial or noncustodial payload creates the client and returns it; a second PUT to the same name updates rather than duplicates.
  • GET /clients/{name} and GET /clients reflect what's stored; GET /clients/{unknown} returns 404. GET /clients/custodial and GET /clients/non-custodial return only clients of the matching type.
  • PUT with a signing key or domain already used by another client, a noncustodial client with no domain, a custodial client with no signing keys, a malformed callback URL, or an unrecognized type all return 400 without creating/mutating a row.
  • POST /clients/{name}/signing-keys/{key} and /destination-accounts/{account} add without disturbing existing entries; the corresponding DELETE removes only the targeted entry; both 404 on an unknown client or an entry that isn't present; adding a signing key already used by another client returns 400; removing a custodial client's last signing key returns 400.
  • DELETE /clients/{name} removes the client and its domain/signing-key/destination-account rows; a subsequent GET returns 404.
  • With clients.type: db, a SEP-10 or SEP-45 login for a signing key/domain that matches a client in the database stamps that client's name into the client_name JWT claim — verified end-to-end (real HTTP, real Postgres) via ClientsDbAttributionTests, not just at the unit level.
  • With sep10.client_attribution_required: true, a login for a signing key/domain not in the database (or not on the allow list) is rejected outright, not issued a token with a null client name.
  • SEP-6/12/24/31 requests authenticated with such a token attribute the resulting transaction/event to that client_name, without making their own database call to resolve it.
  • With clients.type: db and clients.value pointing at an existing file/yaml/json source, every client there is upserted into the database on startup, and startup does not fail if one client fails to import.
  • Existing clients.type: file|inline|json|yaml deployments are unaffected — clients.type: db is opt-in, and switching away from it afterward does not delete previously-imported/managed rows.

Context

Origin ask (anchor operator, paraphrased): their ~1,200-line partner-config YAML requires a rolling restart on every change; they asked for either AP-checks-DB-directly or a ~60-minute AP-side cache refresh, with automating partner onboarding as the underlying goal. A separate concrete follow-up from the same anchor: one of their custodial clients has 600+ destination accounts, and they didn't want to resend the full list to add or remove a single one — that's what the incremental signing-key/destination-account endpoints are for.

Testing

  • Unit: ./gradlew :core:test :platform:test (includes ClientConfigServiceTest, JdbcClientServiceTest, PropertyClientsConfigTest, ClientConfigImportRunnerTest)
  • Integration: ./gradlew runEssentialTests (includes the expanded ClientConfigApiTests, real HTTP against a live Postgres)
  • Extended: TEST_PROFILE_NAME=clients-db ./gradlew startServersWithTestProfile then ./gradlew :extended-tests:test --tests org.stellar.anchor.platform.suite.ClientsDbTestSuite (real SEP-10 auth against a DB-backed server, wired into sub_extended_tests.yml for CI)
  • Manual: full PUT/GET/GET all/incremental-endpoint round trips against a running server, verified against a live Postgres, including a full clean-restart migration of an existing file-based config into the database.

Documentation

anchor-config-default-values.yaml updated with the clients.type: db option, its auto-import behavior, and the two new incremental-endpoint / type-filtered-list capabilities.

Known limitations

N/A

Migrating clients.type to db

This is the guide for anchors moving their client configuration (custodial and non-custodial
wallets, signing keys, domains, callback URLs, destination accounts) off a static file/yaml/
json config and onto the new db-backed option, where clients are managed live through the
/clients REST API instead of a redeploy.

Why move to db

With clients.type set to file, yaml, or json, the client list is fixed at deploy time —
onboarding or updating a wallet means editing the config and restarting the platform server. With
clients.type=db, the same client records live in the database and are managed through
PUT/GET/DELETE calls on /clients, so onboarding a new wallet doesn't require a deploy.

Prerequisites

  • A relational database, not the default h2. data.type must be postgres or aurora.
    The in-memory h2 default won't persist client rows across restarts, and sqlite doesn't
    support Flyway's foreign-key migrations.
  • Flyway enabled. data.flyway_enabled=true. On first startup against that database, Flyway
    applies V32__client_config.sql, which creates four tables: client_config, client_domain,
    client_signing_key, and client_destination_account. No manual migration step is needed
    beyond having Flyway on.
  • Platform API auth already configured. The /clients endpoints live on the platform server
    and are protected the same way as the rest of the Platform API (platform_api.auth.type: JWT
    or API_KEY). If you can already call other Platform API endpoints (e.g. PATCH /transactions), you can call /clients.

1. Point clients at db

clients.type=db
clients.value=path/to/your/existing/clients.yaml

clients.value can stay pointed at whatever file (or inline YAML/JSON string) you were already
using with type=file/yaml/json — the format is unchanged. On db, that value is no longer
read by the running server; it's only used once, on startup, to seed the database.

2. Start the platform server once to import

On startup, if clients.type=db, a one-time CommandLineRunner reads every client out of
clients.value and upserts it into the database by name. Check the startup logs for:

Imported {N} of {N} clients into the database

A per-client failure (e.g. a duplicate signing key) is logged and skipped — it doesn't stop the
other clients from importing or block startup.

3. Stop re-importing on every restart

This import isn't a one-shot migration flag — it runs on every startup where
clients.type=db and clients.value is non-empty, and it re-upserts every client from that file
each time, overwriting whatever is currently in the database for those client names. Once you've
confirmed the import succeeded, clear the value so subsequent restarts don't clobber changes made
through the API afterward:

clients.type=db
clients.value=

4. Verify the import

curl -H "<your platform auth header>" http://localhost:8085/clients

Confirm the count and names match what was in your old config file.

Managing clients going forward

Method Path Purpose
PUT /clients/{name} Create or fully replace a client
GET /clients/{name} Fetch one client
GET /clients List all clients
GET /clients/custodial List custodial clients only
GET /clients/non-custodial List non-custodial clients only
DELETE /clients/{name} Delete a client
POST /clients/{name}/signing-keys/{signingKey} Add a signing key
DELETE /clients/{name}/signing-keys/{signingKey} Remove a signing key
POST /clients/{name}/destination-accounts/{account} Add a destination account
DELETE /clients/{name}/destination-accounts/{account} Remove a destination account

PUT example (custodial client):

curl -X PUT -H "<your platform auth header>" -H "Content-Type: application/json" \
  http://localhost:8085/clients/my-wallet \
  -d '{"type":"custodial","signingKeys":["GABC...XYZ"]}'

Validation rules and constraints

  • A custodial client needs at least one signing key; a non-custodial client needs at least one
    domain. Requests violating this are rejected with 400.
  • A signing key or domain can only belong to one client at a time — client_domain.domain and
    client_signing_key.signing_key are both unique across all clients. Reusing one on a different
    client returns 400.
  • You can't remove the last signing key from a custodial client via DELETE /clients/{name}/signing-keys/{signingKey} — that also returns 400.
  • Callback URLs, when set, must parse as valid http/https URLs.

Rolling back

Deleting client rows isn't required to roll back. Setting clients.type back to file, yaml,
or json (with value pointed at your old config) reverts to reading clients from that source;
the database rows are simply unused while type isn't db.

Troubleshooting

Startup log shows fewer imported than expected, with no per-client error. Check for a
duplicate signing key or domain across clients in your source file — the import calls the same
uniqueness-checked upsert the API uses, so a collision there fails the same way a PUT would.

Clients you edited through the API reverted after a restart. clients.value is still set —
see Stop re-importing on every restart.

/clients returns 404 Not Found on a client you're sure exists. Client lookups are by
name and are case-sensitive; confirm the exact name used in the original PUT.

* add `ismigratefromfileonstartup` flag to clients config

* add `db` as a new client config type
* add `client_name` field to webauth jwt and jwt service.

* refactor client name resolution to happen once during sep-10 and sep-45 challenge validation.
  * this simplifies client attribution in other sep services (sep-6, sep-12, sep-24, sep-31) by directly reading `client_name` from the jwt.

* add database tables and rest api endpoints for client configuration management.
  * this allows dynamic updates to client configurations without redeploying the anchor.

* add a migration runner to import clients from file-based configuration into the database on startup.
@amandagonsalves amandagonsalves self-assigned this Aug 16, 2026
Copilot AI balanced review requested due to automatic review settings August 16, 2026 19:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds database-backed client configuration management, including REST endpoints and a one-time import path from file/yaml/json configs, and propagates resolved client_name into auth JWTs so SEP services can attribute requests without repeated lookups.

Changes:

  • Introduces client_config DB schema + JPA entity/repo and a JDBC-backed ClientService implementation.
  • Adds /clients PUT/GET/DELETE endpoints and an optional startup import runner.
  • Stamps client_name into SEP-10/SEP-45 JWTs and updates SEP services/tests to read clientName from the token.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
platform/src/test/kotlin/org/stellar/anchor/platform/service/ClientConfigServiceTest.kt Unit tests for DB client config CRUD/upsert validation and error translation
platform/src/test/kotlin/org/stellar/anchor/platform/service/ClientConfigImportRunnerTest.kt Tests import runner iterates items and continues on failures
platform/src/test/kotlin/org/stellar/anchor/platform/data/JdbcClientServiceTest.kt Tests JDBC-backed ClientService mapping and lookup behavior
platform/src/test/kotlin/org/stellar/anchor/platform/config/PropertyClientsConfigTest.kt Tests DB config type ignores value and new migrate flag default
platform/src/main/resources/db/migration/V32__client_config.sql Flyway migration introducing client config tables and uniqueness constraints
platform/src/main/resources/config/anchor-config-default-values.yaml Documents new clients.type: db option and migration flag usage
platform/src/main/java/org/stellar/anchor/platform/service/ClientConfigService.java Implements upsert/get/list/delete against JDBC repo with validation
platform/src/main/java/org/stellar/anchor/platform/service/ClientConfigImportRunner.java CommandLineRunner to import config-defined clients into DB
platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientService.java JDBC implementation of core ClientService
platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientConfigRepo.java JPA repository with domain/signingKey lookup queries
platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientConfig.java JPA entity mapping for client config and element collections
platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep10Controller.java Aligns controller exception signature with updated SEP-10 service
platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigResponse.java DTO for client config API responses
platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigRequest.java DTO for client config API requests
platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigController.java Adds /clients CRUD endpoints
platform/src/main/java/org/stellar/anchor/platform/config/PropertyClientsConfig.java Adds DB config type and migrate-from-file flag
platform/src/main/java/org/stellar/anchor/platform/component/share/ClientsBeans.java Switches ClientService bean to JDBC when clients.type=db
platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java Wires ClientFinder for SEP-45 and updates SEP service constructors
platform/src/main/java/org/stellar/anchor/platform/component/platform/PlatformServerBeans.java Registers ClientConfigService + conditional import runner beans
essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ClientConfigApiTests.kt Integration tests for /clients API and uniqueness enforcement
core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt Updates tests to rely on token.clientName instead of ClientFinder
core/src/test/kotlin/org/stellar/anchor/sep45/Sep45ServiceTest.kt Adds tests ensuring SEP-45 stamps client_name and propagates auth failures
core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt Refactors tests to read client name from token/context rather than lookups
core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt Updates SEP-24 tests to set clientName on WebAuthJwt
core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt Updates SEP-12 tests to pass clientName via token rather than ClientFinder
core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt Adds tests for stamping client_name during SEP-10 validation
core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java Uses token.getClientName() for attribution (removes ClientFinder dependency)
core/src/main/java/org/stellar/anchor/sep45/Sep45Service.java Stamps client_name into SEP-45 JWT using ClientFinder
core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java Removes client-name resolution logic; reads from token/context
core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java Uses token.getClientName() for attribution (removes ClientFinder dependency)
core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java Uses token.getClientName(); removes ClientFinder + warning log path
core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java Stamps client_name into web auth JWT during generation; surfaces as SepException
core/src/main/java/org/stellar/anchor/config/ClientsConfig.java Adds DB config type and isMigrateFromFileOnStartup()
core/src/main/java/org/stellar/anchor/auth/WebAuthJwt.java Adds client_name claim parsing/storage
core/src/main/java/org/stellar/anchor/auth/JwtService.java Encodes client_name claim when present

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

* refactor client config service to use objectprovider

* update client service to use objectprovider

* update platform server beans to use objectprovider

* fix unique constraint error handling in client config service

* add specific http/https callback url validation

* update client config validation to clear items when switching to db

* delete method-level cross-origin annotations from client config controller
* delete `clients.migrate-from-file-on-startup` config property

* update client config import runner condition to `clients.type=db`
* add parsing of clients.value for db backend migration

* update client service in sep10 config to be transient
* add endpoints to manage client destination accounts

* add endpoints to manage client signing keys
* add methods to manage client destination accounts

* add methods to manage client signing keys

* update client config data fetching with eager loading
* refactor clients.value resource loading for robustness
* update behavior to log warning instead of failing on invalid resource paths
* add functions to manage client signing keys

* add functions to manage client destination accounts

* refactor client config parsing for legacy value formats when type is db
* add new `clients-db` configuration profile to use a database for client management

* add extended tests for `clients-db` configuration, verifying sep-10 client attribution

* update github actions workflow to run `clients-db` extended tests
* add new api endpoints for listing custodial clients

* add new api endpoints for listing non-custodial clients

* add service methods to list clients by their type
* add tests for client creation validation
* add tests for managing client signing keys
* add tests for managing client destination accounts
* add tests for listing clients by type (custodial, noncustodial)
* refactor client database import instructions

* delete 'migrate_from_file_on_startup' documentation

* update client configuration to clarify direct import via 'value' field

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 42 changed files in this pull request and generated 2 comments.

Suppressed comments (6)

platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientService.java:54

  • These lookups convert any matching row to the requested subtype without checking its declared type. The API and legacy RawClient format permit irrelevant fields, so a noncustodial row with a signing key can be returned as a CustodialClient (and a custodial row with a domain as NonCustodialClient), unlike DefaultClientService, which searches only the corresponding typed list. Filter by type before mapping (and ideally in the query).
    platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigController.java:57
  • This static route conflicts with /clients/{name}. Since validation permits any nonblank name, a client named custodial can be created via PUT but GET resolves to this filtered-list endpoint instead of returning that client; non-custodial has the same problem. Use a non-conflicting filter shape (for example a query parameter or /clients/type/...) or reject these reserved names consistently.
      value = "/clients/custodial",

platform/src/main/resources/config/anchor-config-default-values.yaml:660

  • This documentation says the import happens once and existing rows remain untouched, but the conditional runner executes on every startup and calls upsert, which replaces matching rows and their collections. Operators following this text may lose API-managed edits after a restart. Document the actual repeated overwrite behavior.
    platform/src/main/java/org/stellar/anchor/platform/service/ClientConfigService.java:22
  • The incremental endpoints use detached read-modify-save cycles without optimistic locking or atomic collection-row operations. Two concurrent additions based on the same client state can both return success while the later merge overwrites the earlier addition, which defeats the purpose of safely managing high-volume collections incrementally. Add version-based conflict detection/retry or repository operations that insert/delete the targeted collection row atomically.
    platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientConfigRepo.java:30
  • The eager graph makes every signing-key lookup materialize all domains, keys, and destination accounts. SepRequestValidator calls this lookup on SEP-6/24 requests whose requested account differs from the JWT account, so the cited 600-account client causes hundreds of rows (potentially a cartesian product across all three collections) to be hydrated per request rather than limiting DB work to authentication. Add narrow attribution/name and destination-membership queries instead of using this full graph for every lookup.
  @EntityGraph(attributePaths = {"signingKeys", "domains", "destinationAccounts"})
  @Query("SELECT c FROM JdbcClientConfig c JOIN c.signingKeys k WHERE k = :signingKey")
  JdbcClientConfig findBySigningKey(@Param("signingKey") String signingKey);

platform/src/main/java/org/stellar/anchor/platform/config/PropertyClientsConfig.java:135

  • When both directly bound clients.items and a migration clients.value are present, this assignment discards all pre-bound items. That contradicts the stated preservation behavior and can omit clients from startup import. Merge the parsed entries with the existing list using a deterministic name-conflict policy, or reject the combination explicitly.
          items = parseLegacyValueForMigration(this.getValue());

Comment thread core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java
* fix old jwt tokens by rejecting those without the client name claim
* update jwt generation to always include the CLIENT_NAME claim, defaulting to empty string
* fix client config parsing to prevent silent failures during migration
* add tests for client config parsing error scenarios
@amandagonsalves
amandagonsalves merged commit ad35e60 into develop Aug 19, 2026
11 checks passed
@amandagonsalves
amandagonsalves deleted the feat/anchor-1270 branch August 19, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants