Skip to content

fix!: adapt the Hub client to the FLAME Hub 0.13.0 API - #275

Open
tada5hi wants to merge 4 commits into
developfrom
feat/hub-0.13-camelcase
Open

fix!: adapt the Hub client to the FLAME Hub 0.13.0 API#275
tada5hi wants to merge 4 commits into
developfrom
feat/hub-0.13-camelcase

Conversation

@tada5hi

@tada5hi tada5hi commented Jul 30, 2026

Copy link
Copy Markdown

FLAME Hub 0.13.0 landed two breaking changes together:

The broker's Hub surface is small — two GETs and two DTOs — but every one of these failures is silent. That is the reason for the PR rather than the size of the diff.

The rapiq filter keys

HttpHubClient sent filter[analysis_id] and filter[client_id]. rapiq prunes an unknown filter key instead of rejecting it, so against 0.13.0:

Call Behaviour with the stale key Severity
fetchAnalysisNodesGET /analysis-nodes?filter[analysis_id]=… filter dropped → responds with every analysis node the client can see, not the analysis's. The broker would treat unrelated nodes as participants of the analysis and route messages to them. confidentiality
fetchPublicKeyGET /nodes?filter[client_id]=… filter dropped → resp.data.size() != 1NoMatchingNodeFoundException as soon as more than one node exists fails loudly, at least

No 4xx in either case. The first one looks like success.

The DTOs

Node and AnalysisNode are @JsonIgnoreProperties(ignoreUnknown = true), so a stale @JsonProperty deserializes to null rather than throwing:

  • Node.public_keypublicKey. This is the one that matters: a null public key means NoPublicKeyException on every send, i.e. end-to-end encryption off for the whole node.
  • Node.client_idclientId — participant identity and the discoverSelf comparison.
  • AnalysisNode.node_idnodeId (the Java field is renamed alongside the annotation; it has no readers).

HubResponseContainer is unchanged: both HubClient calls target collection endpoints, whose { data, meta } envelope long predates this release. The Hub's record envelope does not reach this client.

The integration tests build their stub bodies by serialising these same DTOs through Jackson, so only the four query-key assertions in HttpHubClientIT needed touching.

System tests

system-tests/environment/resources/hub/setup-hub-resources.sh provisions its fixtures through the Hub and authup REST APIs, so it is affected on both sides:

  • Request bodies — the six *-setup-template.json files move to camelCase. That includes client-account-setup-template.json, which posts to authup (secret_hashedsecretHashed, is_confidentialisConfidential): authup renamed in lockstep. Hub's validator drops an unknown body key silently, so a stale realm_id would have created the entity with no realm rather than erroring.
  • Response readsjq -r '.id'jq -r '.data.id // empty' at all 14 record-creating calls. The two collection reads (.data[0].id) already matched the envelope and just gained the same guard.

The // empty is load-bearing. Without it jq -r '.data.id' emits the string "null" for a missing key, which the script's if [ -z "${X}" ] guards accept as success and then propagate as a literal null id into every subsequent request. Verified:

$ echo '{"data":{"id":"abc"},"meta":{}}' | jq -r '.data.id // empty'
abc
$ echo '{"id":"abc"}' | jq -r '.data.id // empty'          # pre-0.13.0 shape
                                                           # → empty, guard fires

Deliberately not renamed

The OAuth2 surface is spec-frozen and stays snake_case — HubOIDCAuthenticator's client_id form parameter, the same in system-tests/tests/message-broker/auth.go, and jq -r '.access_token'. Also untouched: the shell variable names in setup.sh, the keycloak realm export, and the socket.io message DTOs (HubMessageMetadata, HubMessageSender, HubMessageRecipient), which were already camelCase.

The system-test environment had to move too (commits 2–5)

Run System Tests was already failing before this PR, and for a related reason. The stack pinned ghcr.io/privateaim/hub:0.8.31 but pulled authup/authup unpinned, so latest drifted onto a release 0.8.31 cannot talk to and Hub core stopped becoming healthy. The last green CI run was 2026-06-20.

Five things were needed to get the stack working on 0.13.0, each found by running it rather than by reading source:

  • Both Hub images → 0.13.0; authup pinned to 1.0.0-beta.57, the version Hub 0.13.0 declares across its @authup/* dependencies.
  • HARBOR_URL: ${HARBOR_URL:-} removed. 0.13.0 validates harborURL as a URL when the key is present, and the empty string aborts startup with Property harborURL is invalid. Harbor is unused here, so it is better left unset than empty.
  • Hub permissions are provisioned into authup. Since 0.13.0 the authorization middleware genuinely enforces permissions where it previously ran in dry-run mode, and the Hub does not register its own permission names. Without them every write fails with The project_create permission could not be resolved. A new authup-provisioning.mjs declares all 30 PermissionName values and is mounted at /usr/src/app/writable/provisioning/hub.mjs; authup's built-in admin role resolves them via its globalPermissions: ['*']. This mirrors buildAuthupProvisioning() in @privateaim/server-test-kit.
  • The node client accounts needed an explicit auth method. Under beta.57 isConfidential is gone from the client read model and no longer implies secret-based authentication, so the template produced clients with authMethod: "none" and grantTypes: null. Every client-credentials request returned invalid_client, the broker could never obtain a Hub token, and GET /analyses/{id}/participants/self answered 502 — exactly what CI showed. The template now declares authMethod: "secret" and grantTypes: "client_credentials".
  • The messenger needed a database and authup credentials. With discovery fixed, the send/receive test still reported sent (10) and received (0) differs. The messenger container was crash-looping the whole time — nothing in the harness waits on it, only on core. Two separate gaps: 0.13.0's messenger gained a durable mailbox and now needs a datasource where 0.8.31 was a stateless relay (The database configuration could not be read from env variables.), and once it booted, every socket handshake was rejected with Property token is invalid because its authorization middleware introspects incoming tokens through an authup client token creator built from CLIENT_ID / CLIENT_SECRET / REALM, which core is given and the messenger was not.

Two things worth noting for review:

  • The Hub's read endpoints are not permission-gated, so the node clients need no role or permission binding of their own. Only writes are, and the provisioning script makes those with the admin token.
  • The broker's socket contract needed no changes. Event names, the {to:[{type,id}], data, metadata} payload and the client-connection:${id} room key are identical between the two Hub versions, and the messenger DTOs were already camelCase. Every messenger problem here was environment configuration.

Verification

I brought the stack up locally against Hub 0.13.0 and iterated on it until it worked. Core reaches healthy, and setup-hub-resources.sh completes end to end — three clients, project, analysis, three nodes, three project nodes, three analysis nodes. After a clean provisioning run the node A client obtains a token with no manual patching, and both queries the broker makes succeed with it (/analysis-nodes → the analysis's 3 nodes; /nodes?filter[clientId] → exactly 1, carrying a non-null publicKey).

Both of the broker's queries were also exercised old key versus new, using the admin token:

# GET /analysis-nodes — the participant lookup
filter[analysisId]=<real id>    → total 3     ✅
filter[analysis_id]=<BOGUS id>  → total 3     ⬅ filter pruned; every node returned

# GET /nodes — the public-key lookup
filter[clientId]=<real id>      → total 1     ✅ (the client requires exactly 1)
filter[client_id]=<real id>     → total 3     ⬅ NoMatchingNodeFoundException

That first line is the confidentiality issue, demonstrated rather than argued: a deliberately non-existent analysis id still returned all three analysis nodes.

The include=node payload also confirms the DTO annotations — the relation carries clientId, publicKey, robotId, externalName, realmId, registryId, registryProjectId, createdAt, updatedAt.

The messenger relay was verified separately, with two node clients connected to it over socket.io: A emits send addressed to B's client id and B receives it, from resolving to A's client identity. Both messenger changes are required — with either missing the relay stays silent rather than erroring, because the sender's acknowledgement callback still reports success, the same failure shape as a pruned rapiq filter.

Not verified locally: mvn verify, and the node half of the system tests. There is no JVM or Maven on the machine I worked from, and the node compose builds its image from target/*.jar, so that half is only exercised by CI. The Java changes are annotation strings and query-parameter strings; no control flow moved.

Ordering

Independent of the other 0.13.0 follow-ups (hub-python-client#129, node-ui#401) — this repo talks to the Hub directly. It must land with the 0.13.0 rollout, though: the current develop and 0.13.0 cannot interoperate in either direction.

The FLAME Hub renamed its entity/HTTP-API vocabulary from snake_case to
camelCase (PrivateAIM/hub#1806) and wrapped every entity record response in a
data/meta envelope (PrivateAIM/hub#1801). Both ship in 0.13.0. Its OAuth2/OIDC
parameters stay snake_case.

Every one of these fails silently rather than loudly, which is why they are worth
enumerating:

- HttpHubClient sent filter[analysis_id] and filter[client_id]. rapiq prunes an
  unknown filter key instead of rejecting it, so against 0.13.0 the analysis-node
  lookup would have answered with EVERY analysis node the client can see rather
  than the analysis's, and the broker would have treated unrelated nodes as
  participants of the analysis. The node lookup would have degraded to
  NoMatchingNodeFoundException once more than one node existed.
- Node and AnalysisNode carry @JsonIgnoreProperties(ignoreUnknown = true), so a
  stale @JsonProperty deserializes to null rather than throwing. public_key ->
  publicKey in particular would have turned every message into
  NoPublicKeyException, disabling end-to-end encryption for the whole node.

Both HubClient calls target collection endpoints, whose data/meta envelope
predates this release, so HubResponseContainer is unchanged.

The system-test provisioning script creates its fixtures through the Hub and
authup REST APIs and read the created id straight off the response body, which is
now nested under data. Both request bodies and response reads move: the six
setup templates to camelCase (including the authup client account, since authup
renamed in lockstep), and `jq -r '.id'` to `jq -r '.data.id // empty'`. The
`// empty` matters — without it jq emits the string "null" for a missing key,
which the script's `[ -z ... ]` guards accept as success and then propagate as a
literal "null" id into every later request.

The OAuth2 token surface is untouched: HubOIDCAuthenticator's client_id form
parameter and the system tests' equivalent stay snake_case, as does
.access_token.

BREAKING CHANGE: requires FLAME Hub 0.13.0 or newer.
Copilot AI review requested due to automatic review settings July 30, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Hub integrations and system-test resources now use camelCase property and filter names. Hub setup parsing reads resource IDs from wrapped data responses, while test provisioning adds explicit permissions and updated Hub/Authup container configuration.

Changes

Hub contract alignment

Layer / File(s) Summary
Java Hub filters and mappings
src/main/java/de/privateaim/node_message_broker/common/hub/..., src/test/java/de/privateaim/node_message_broker/common/hub/HttpHubClientIT.java
Hub query filters, JSON mappings, and integration-test URL assertions use camelCase names.
System-test payload naming
system-tests/environment/resources/hub/*-setup-template.json
Hub setup templates rename identifier and configuration properties from snake_case to camelCase.
Wrapped resource ID parsing
system-tests/environment/resources/hub/setup-hub-resources.sh
Resource ID extraction reads list responses from data[0].id and creation responses from data.id, with empty fallbacks.
Hub test environment provisioning
system-tests/environment/hub/authup-provisioning.mjs, system-tests/environment/hub/hub-docker-compose.yml
Authup permissions, service image versions, provisioning mounts, network settings, and core environment configuration are updated.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: brucetony

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adapting the Hub client to the FLAME Hub 0.13.0 API.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hub-0.13-camelcase

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The stack pinned ghcr.io/privateaim/hub:0.8.31 while pulling authup/authup
unpinned, so `latest` eventually moved to a release 0.8.31 could not talk to and
the Hub core container stopped becoming healthy — the last green run was
2026-06-20. Bump both Hub images to 0.13.0 and pin authup to 1.0.0-beta.57, the
version Hub 0.13.0 declares across its @authup/* dependencies.

Two further changes are needed before the stack comes up on 0.13.0:

- HARBOR_URL was passed as `${HARBOR_URL:-}`, i.e. an empty string. 0.13.0
  validates harborURL as a URL when the key is present, so the empty value now
  aborts startup with `Property harborURL is invalid`. Harbor is unused here, so
  leave the variable unset rather than empty.
- Since 0.13.0 the authorization middleware genuinely enforces permissions,
  where it previously ran in dry-run mode, and the Hub does not register its
  permission names with authup itself. Without them every write fails with
  `The project_create permission could not be resolved`. Mount a provisioning
  module declaring all 30 PermissionName values, which authup's built-in `admin`
  role resolves through its `globalPermissions: ['*']`; this mirrors
  buildAuthupProvisioning() in @privateaim/server-test-kit.

Verified by bringing the stack up locally: core reaches `healthy` and
setup-hub-resources.sh completes end to end, creating the clients, project,
analysis and all three nodes, project nodes and analysis nodes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
system-tests/environment/hub/hub-docker-compose.yml (1)

89-92: 🩺 Stability & Availability | 🔵 Trivial

Verify the external network’s IPAM before relying on the static address.

Because hub-node-intercomm is declared external, this file does not create its subnet or reserve addresses. A clean environment will fail if the network is absent, does not cover 172.99.20.11, or already assigns that address. Ensure provisioning creates the network with compatible IPAM and reserves .10.12.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@system-tests/environment/hub/hub-docker-compose.yml` around lines 89 - 92,
Update the provisioning for the external hub-node-intercomm network to create it
when absent with IPAM covering 172.99.20.11, reserve addresses 172.99.20.10
through 172.99.20.12, and validate existing networks before Docker Compose
relies on the static address in hub-node-intercomm.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@system-tests/environment/hub/hub-docker-compose.yml`:
- Around line 89-92: Update the provisioning for the external hub-node-intercomm
network to create it when absent with IPAM covering 172.99.20.11, reserve
addresses 172.99.20.10 through 172.99.20.12, and validate existing networks
before Docker Compose relies on the static address in hub-node-intercomm.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a92a8f73-2704-4fce-a248-7dc790a01814

📥 Commits

Reviewing files that changed from the base of the PR and between 2770dde and ede9c07.

📒 Files selected for processing (2)
  • system-tests/environment/hub/authup-provisioning.mjs
  • system-tests/environment/hub/hub-docker-compose.yml

tada5hi added 2 commits July 30, 2026 21:19
…ethod

The node clients are created through authup's REST API and then authenticate
with the client-credentials grant to reach the Hub. Under authup 1.0.0-beta.57
the template no longer produces a usable client: `isConfidential` is gone from
the client read model and no longer implies secret-based authentication, so the
account was created with `authMethod: "none"` and `grantTypes: null`, and every
token request came back `invalid_client`. The broker could then never obtain a
Hub token, and answered `GET /analyses/{id}/participants/self` with 502.

Declare `authMethod: "secret"` and `grantTypes: "client_credentials"` instead,
which is what `isConfidential: true` used to stand for.

Verified against the local stack: after a clean provisioning run the node A
client obtains a token with no manual patching, and both queries the broker
makes succeed with it — `/analysis-nodes?filter[analysisId]` returns the
analysis's 3 nodes and `/nodes?filter[clientId]` returns exactly 1, carrying a
non-null publicKey. Note that the Hub's read endpoints are not permission-gated,
so the node clients need no role or permission binding; only writes are, and
those are made with the admin token.
The messenger container crash-looped on 0.13.0 and nothing relayed, which is why
the send/receive test reported `number of sent messages (10) and received
messages (0) differs`. Two independent gaps, both surfaced by running the stack:

- `The database configuration could not be read from env variables.` The
  messenger gained a durable mailbox and now needs a datasource, where 0.8.31 was
  purely a stateless relay. Point it at the same MySQL as core with its own
  `messenger` database, and make it wait for MySQL to be healthy.
- Once it started, every socket handshake was rejected with `Property token is
  invalid`. The socket authorization middleware introspects the incoming token
  through an authup client token creator built from `CLIENT_ID`, `CLIENT_SECRET`
  and `REALM` — which core is given and the messenger was not.

Verified against the local stack with two node clients connected to the
messenger over socket.io: A emits `send` addressed to B's client id and B
receives it, `from` resolving to A's client identity. Both changes are required;
with either missing the relay stays silent rather than erroring, since the
sender's acknowledgement callback still reports success.
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.

2 participants