Skip to content

feat(node-message-broker): container-facing message + participant endpoints#15

Merged
tada5hi merged 2 commits into
masterfrom
feat/message-endpoints
Jun 26, 2026
Merged

feat(node-message-broker): container-facing message + participant endpoints#15
tada5hi merged 2 commits into
masterfrom
feat/message-endpoints

Conversation

@tada5hi

@tada5hi tada5hi commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the container-facing message + participant API for the node message-broker
(Plan 013 Track B, slice S5 — issue #8). The HTTP surface was verified by reading the
legacy PrivateAIM/node-message-broker server and the PrivateAIM/python-sdk
(flamesdk) client, so the SDK needs no change.

Endpoints

Route Request Response
POST /analyses/:id/messages { recipients: nodeId[], message: <JSON> } 202, empty body
POST /analyses/:id/messages/broadcast { message: <JSON> } 202, empty body
GET /analyses/:id/participants bare array [{ nodeId, nodeType }]
GET /analyses/:id/participants/self { nodeId, nodeType }, 404 if absent

(Webhook-subscription CRUD was already present; inbound delivery stays webhook-push — there
is no pull endpoint in the contract.)

Behaviour

  • message is an opaque JSON payload relayed verbatim — serialized, sealed per
    recipient under their public key, and tagged with metadata.analysisId on the Hub wire.
    The SDK embeds its own meta envelope (id/sender/category/…) inside message, so
    the broker never mints ids or wraps the payload.
  • recipients carry participant node ids; an unknown recipient is rejected with 400.
  • Participant responses expose only nodeId/nodeType — the internal clientId /
    publicKey never leak to containers (covered by a test).
  • Authorization: every analysis-scoped route is gated by the
    ANALYSIS_SELF_MESSAGE_BROKER_USE capability (request permission checker) plus an
    analysis-ownership check. This is stricter than the legacy server (which only required an
    authenticated identity); it is the node-side analysis policy introduced in feat(node-message-broker): analysis authorization — scope binding + capability check #13, and the
    analysis client satisfies it transparently.

Internals

  • Wire CryptoService into the components DI module (it had been unwired since S1).
  • Add the core send orchestration core/messaging/dispatchAnalysisMessage /
    broadcastAnalysisMessage (resolve participants → seal per recipient → one Hub send
    each), kept port-only so it is testable against in-memory fakes.

Tests

67 unit tests pass (+4 core dispatch, +9 controller) against in-memory fakes of the
participant resolver, crypto service, hub client, and analysis-client lookup. Lint,
typecheck, and the tsdown build are clean.

Closes #8. Supersedes #7 (folded into the S3/S5 authorization design).

Summary by CodeRabbit

  • New Features

    • Added analysis messaging actions so you can send messages to specific participants, broadcast to all participants, and view participant details.
    • Improved webhook subscription handling with clearer input validation.
  • Bug Fixes

    • Tightened access checks for analysis messaging actions.
    • Added safer message validation and recipient checks to prevent invalid sends.
    • Ensured message delivery uses encrypted outbound messages and excludes the current participant from broadcasts.

… endpoints

Implement the SDK-compatible container API, verified against the legacy
node-message-broker server and the python-sdk client:

- POST /analyses/:id/messages            send to recipient node ids (202, empty)
- POST /analyses/:id/messages/broadcast  fan out to all participants (202, empty)
- GET  /analyses/:id/participants         bare array of { nodeId, nodeType }
- GET  /analyses/:id/participants/self    { nodeId, nodeType }, 404 if absent

`message` is an opaque JSON payload relayed verbatim — serialized, then sealed
per recipient under their public key and tagged with metadata.analysisId on the
Hub wire. `recipients` carry participant node ids; an unknown recipient is a 400.
Inbound delivery stays webhook-push (no pull endpoint), matching the contract.

Every analysis-scoped route is gated by the ANALYSIS_SELF_MESSAGE_BROKER_USE
capability (request permission checker) and an analysis-ownership check.

Also wire CryptoService into the components module and add the core send
orchestration (dispatchAnalysisMessage / broadcastAnalysisMessage) it builds on,
with core dispatch tests and controller tests against in-memory fakes.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tada5hi, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 35 minutes and 48 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d78a1e7d-24d0-40e4-ac40-f5ea4a8042bb

📥 Commits

Reviewing files that changed from the base of the PR and between d73ce42 and 5e41958.

📒 Files selected for processing (7)
  • apps/node-message-broker/src/adapters/http/controllers/messages/module.ts
  • apps/node-message-broker/src/adapters/http/controllers/messages/types.ts
  • apps/node-message-broker/src/adapters/http/controllers/messages/validators/index.ts
  • apps/node-message-broker/src/adapters/http/controllers/messages/validators/message.ts
  • apps/node-message-broker/src/adapters/http/controllers/messages/validators/webhook-subscription.ts
  • apps/node-message-broker/test/unit/adapters/http/messages-controller.spec.ts
  • apps/node-message-broker/test/unit/adapters/http/messages-validators.spec.ts
📝 Walkthrough

Walkthrough

Adds analysis message send and broadcast endpoints, participant lookup endpoints, crypto-backed fan-out dispatch, new container wiring for crypto and core-client dependencies, and unit tests for dispatch and controller behavior.

Changes

Node message broker analysis messaging

Layer / File(s) Summary
Shared contracts and wiring
apps/node-message-broker/src/adapters/http/controllers/messages/types.ts, apps/node-message-broker/src/adapters/http/controllers/messages/module.ts, apps/node-message-broker/src/app/modules/components/constants.ts, apps/node-message-broker/src/app/modules/components/module.ts, apps/node-message-broker/src/app/modules/http/controller.ts, apps/node-message-broker/src/app/modules/http/module.ts
Adds request/body contracts, the Crypto injection token and service registration, and controller/container resolution for the new dependencies.
Dispatch fan-out
apps/node-message-broker/src/core/messaging/types.ts, apps/node-message-broker/src/core/messaging/index.ts, apps/node-message-broker/src/core/messaging/dispatch.ts
Adds dispatch dependency/output types, the package re-exports, and the recipient validation, sealing, and hub fan-out helpers.
Analysis message controller endpoints
apps/node-message-broker/src/adapters/http/controllers/messages/module.ts
Adds send, broadcast, participant lookup, authorization, and webhook validation on the analysis message controller.
Dispatch test doubles and coverage
apps/node-message-broker/test/unit/core/messaging/fake-crypto-service.ts, apps/node-message-broker/test/unit/core/messaging/fake-hub-client.ts, apps/node-message-broker/test/unit/core/messaging/fake-participant-resolver.ts, apps/node-message-broker/test/unit/core/messaging/dispatch.spec.ts
Adds test doubles for crypto, hub, and participant resolution plus unit coverage for per-recipient sealing, broadcast exclusion, and unknown-recipient errors.
Controller coverage
apps/node-message-broker/test/unit/adapters/http/messages-controller.spec.ts
Adds controller unit coverage for permission checks, ownership checks, validation failures, broadcast fan-out, participant shaping, and self-participant lookup.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AnalysisMessageController
  participant dispatchAnalysisMessage
  participant IParticipantResolver
  participant ICryptoService
  participant IHubClient
  Client->>AnalysisMessageController: POST /analyses/:id/messages
  AnalysisMessageController->>dispatchAnalysisMessage: dispatchAnalysisMessage(...)
  dispatchAnalysisMessage->>IParticipantResolver: resolve(analysisId)
  dispatchAnalysisMessage->>ICryptoService: seal(data, recipientPublicKey)
  dispatchAnalysisMessage->>IHubClient: send({ metadata.analysisId })
  AnalysisMessageController-->>Client: 202 Accepted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • None identified.

Possibly related PRs

  • PrivateAIM/node#12: Introduces participant resolution and publicKey data used by the new sealed recipient fan-out path.
  • PrivateAIM/node#13: Adds analysis lookup wiring used by AnalysisMessageController.authorize() to enforce analysis ownership.

Poem

🐰 I hopped through keys and sealed each note,
Then sent each spark to every boat.
One hop per peer, one hub-side cheer,
Analysis messages now appear.
Hop hop! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: adding container-facing message and participant endpoints.
Linked Issues check ✅ Passed The PR implements the requested send/broadcast and participant endpoints with authorization, per-recipient sealing, metadata.analysisId, and SDK-shaped responses.
Out of Scope Changes check ✅ Passed No obvious unrelated code changes were introduced; the crypto, dispatch, DI, and tests all support the messaging endpoint work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/message-endpoints

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/node-message-broker/src/adapters/http/controllers/messages/module.ts (2)

138-162: 🔒 Security & Privacy | 🟠 Major

Subscription CRUD endpoints are missing the authorize gate.

subscribe, listSubscriptions, and unsubscribe apply only ForceLoggedInMiddleware and never call await this.authorize(event, analysisId), unlike send, broadcast, listParticipants, and getSelfParticipant. This allows any authenticated client to register, enumerate, or delete webhook subscriptions for any analysisId they do not own (IDOR) and bypasses the ANALYSIS_SELF_MESSAGE_BROKER_USE capability check. This also contradicts the class-level documentation stating that all analysis-scoped routes are gated by authorize.

Add await this.authorize(event, analysisId) at the start of subscribe, listSubscriptions, and unsubscribe.

🤖 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 `@apps/node-message-broker/src/adapters/http/controllers/messages/module.ts`
around lines 138 - 162, The subscription endpoints are missing the same analysis
authorization gate used by other routes in this controller. Update the
MessageBrokerController methods subscribe, listSubscriptions, and unsubscribe to
call await this.authorize(event, analysisId) at the start, matching send,
broadcast, listParticipants, and getSelfParticipant so analysis-scoped webhook
CRUD is properly restricted.

198-203: 🔒 Security & Privacy | 🟡 Minor

Validate webhookUrl to prevent SSRF attacks

The requireWebhookUrl method accepts any non-empty string, which is then passed directly to fetch() in apps/node-message-broker/src/adapters/delivery/memory.ts without validation. This allows attackers to register internal URLs (e.g., http://169.254.169.254, http://localhost) or non-HTTP schemes, leading to Server-Side Request Forgery (SSRF).

Update requireWebhookUrl to:

  1. Parse the URL using new URL() to ensure validity.
  2. Enforce http or https schemes only.
  3. Reject private/internal IP addresses and localhost.
Example fix
protected requireWebhookUrl(data: WebhookSubscriptionBody): string {
    if (!data || typeof data.webhookUrl !== 'string' || data.webhookUrl.length === 0) {
        throw new BadRequestError('A webhookUrl is required.');
    }

    let parsedUrl: URL;
    try {
        parsedUrl = new URL(data.webhookUrl);
    } catch {
        throw new BadRequestError('Invalid webhookUrl format.');
    }

    if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
        throw new BadRequestError('webhookUrl must use http or https scheme.');
    }

    // Block private/internal IPs and localhost
    const ip = parsedUrl.hostname;
    if (ip === 'localhost' || ip.startsWith('127.') || ip === '169.254.169.254') {
        throw new BadRequestError('webhookUrl cannot point to internal hosts.');
    }

    return data.webhookUrl;
}
🤖 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 `@apps/node-message-broker/src/adapters/http/controllers/messages/module.ts`
around lines 198 - 203, Update requireWebhookUrl in the messages module so it no
longer accepts arbitrary non-empty strings before being used by fetch() in
memory delivery. Parse data.webhookUrl with new URL(), reject invalid URLs with
BadRequestError, allow only http: and https: schemes, and block localhost plus
private/internal IP targets before returning the URL. Use the requireWebhookUrl
method name and WebhookSubscriptionBody to locate the validation logic.
🧹 Nitpick comments (3)
apps/node-message-broker/test/unit/adapters/http/messages-controller.spec.ts (2)

183-223: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add denied-path coverage for the other analysis-scoped routes.

This suite only proves capability/ownership enforcement on send(). broadcast(), listParticipants(), and getSelfParticipant() are part of the same analysis-scoped surface, so a regression there could expose message fan-out or participant topology without tripping these tests.

🤖 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 `@apps/node-message-broker/test/unit/adapters/http/messages-controller.spec.ts`
around lines 183 - 223, Add denied-path tests for the other analysis-scoped
controller methods, not just send(). In MessagesControllerSpec, extend the setup
to cover unauthorized/ownership failures for broadcast(), listParticipants(),
and getSelfParticipant(), asserting they reject or return the expected denial
behavior when the caller lacks access to the analysis. Use the existing
controller, resolver, and event setup to verify these routes enforce the same
capability checks as send() and do not leak recipients or participant data.

218-223: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the 404 contract explicitly.

This test only matches /self participant/i, so a generic 400/500 with the same message would still pass. The contract here is specifically 404, so the spec should assert the HTTP-specific error type or response status as well.

🤖 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 `@apps/node-message-broker/test/unit/adapters/http/messages-controller.spec.ts`
around lines 218 - 223, The getSelfParticipant spec currently only checks the
error message, so it can pass for the wrong HTTP status. Update the test in
messages-controller.spec.ts to assert the HTTP-specific 404 contract for
getSelfParticipant, using the controller’s error type/response status in
addition to the self participant message. Keep the check anchored on
getSelfParticipant and the “no self participant exists” case so the intended 404
behavior is verified explicitly.
apps/node-message-broker/src/core/messaging/dispatch.ts (1)

58-67: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Fan-out is not atomic — a single recipient failure leaves partial deliveries.

Promise.all rejects on the first failing crypto.seal/hub.send, but the other in-flight sends still complete. The caller (controller) only returns 202 on full success, so a partial failure surfaces as an error to the container even though some recipients already received the message, with no record of which ids succeeded. For an at-least-once relay this is often acceptable, but consider documenting the semantics or using Promise.allSettled to surface per-recipient outcomes.

🤖 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 `@apps/node-message-broker/src/core/messaging/dispatch.ts` around lines 58 -
67, The fan-out in dispatch’s message send flow is using Promise.all, so one
recipient failure can hide which recipients already succeeded while still
causing the whole operation to reject. Update the dispatch logic around the
recipients map in dispatch.ts to either use Promise.allSettled and return
per-recipient outcomes (including which clientIds succeeded or failed) or
explicitly document that partial deliveries are expected. Make sure the behavior
is clear at the dispatch/send boundary and, if you keep failure-tolerant
delivery, preserve enough result detail for the controller or caller to report
partial success.
🤖 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.

Outside diff comments:
In `@apps/node-message-broker/src/adapters/http/controllers/messages/module.ts`:
- Around line 138-162: The subscription endpoints are missing the same analysis
authorization gate used by other routes in this controller. Update the
MessageBrokerController methods subscribe, listSubscriptions, and unsubscribe to
call await this.authorize(event, analysisId) at the start, matching send,
broadcast, listParticipants, and getSelfParticipant so analysis-scoped webhook
CRUD is properly restricted.
- Around line 198-203: Update requireWebhookUrl in the messages module so it no
longer accepts arbitrary non-empty strings before being used by fetch() in
memory delivery. Parse data.webhookUrl with new URL(), reject invalid URLs with
BadRequestError, allow only http: and https: schemes, and block localhost plus
private/internal IP targets before returning the URL. Use the requireWebhookUrl
method name and WebhookSubscriptionBody to locate the validation logic.

---

Nitpick comments:
In `@apps/node-message-broker/src/core/messaging/dispatch.ts`:
- Around line 58-67: The fan-out in dispatch’s message send flow is using
Promise.all, so one recipient failure can hide which recipients already
succeeded while still causing the whole operation to reject. Update the dispatch
logic around the recipients map in dispatch.ts to either use Promise.allSettled
and return per-recipient outcomes (including which clientIds succeeded or
failed) or explicitly document that partial deliveries are expected. Make sure
the behavior is clear at the dispatch/send boundary and, if you keep
failure-tolerant delivery, preserve enough result detail for the controller or
caller to report partial success.

In
`@apps/node-message-broker/test/unit/adapters/http/messages-controller.spec.ts`:
- Around line 183-223: Add denied-path tests for the other analysis-scoped
controller methods, not just send(). In MessagesControllerSpec, extend the setup
to cover unauthorized/ownership failures for broadcast(), listParticipants(),
and getSelfParticipant(), asserting they reject or return the expected denial
behavior when the caller lacks access to the analysis. Use the existing
controller, resolver, and event setup to verify these routes enforce the same
capability checks as send() and do not leak recipients or participant data.
- Around line 218-223: The getSelfParticipant spec currently only checks the
error message, so it can pass for the wrong HTTP status. Update the test in
messages-controller.spec.ts to assert the HTTP-specific 404 contract for
getSelfParticipant, using the controller’s error type/response status in
addition to the self participant message. Keep the check anchored on
getSelfParticipant and the “no self participant exists” case so the intended 404
behavior is verified explicitly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44ca2c89-5aa9-45e3-afac-f552d2c9cb5a

📥 Commits

Reviewing files that changed from the base of the PR and between fb1fa78 and d73ce42.

📒 Files selected for processing (14)
  • apps/node-message-broker/src/adapters/http/controllers/messages/module.ts
  • apps/node-message-broker/src/adapters/http/controllers/messages/types.ts
  • apps/node-message-broker/src/app/modules/components/constants.ts
  • apps/node-message-broker/src/app/modules/components/module.ts
  • apps/node-message-broker/src/app/modules/http/controller.ts
  • apps/node-message-broker/src/app/modules/http/module.ts
  • apps/node-message-broker/src/core/messaging/dispatch.ts
  • apps/node-message-broker/src/core/messaging/index.ts
  • apps/node-message-broker/src/core/messaging/types.ts
  • apps/node-message-broker/test/unit/adapters/http/messages-controller.spec.ts
  • apps/node-message-broker/test/unit/core/messaging/dispatch.spec.ts
  • apps/node-message-broker/test/unit/core/messaging/fake-crypto-service.ts
  • apps/node-message-broker/test/unit/core/messaging/fake-hub-client.ts
  • apps/node-message-broker/test/unit/core/messaging/fake-participant-resolver.ts

… zod

Replace the hand-rolled requireRecipients / requireMessage / requireWebhookUrl
guards in the message controller with validup `Container` validators backed by zod
schemas, matching the Hub's validation convention and the local config validator.

- AnalysisMessageValidator: `recipients` (non-empty node-id array, gated to a `send`
  group so a broadcast validates `message` alone) and `message` (opaque non-null JSON —
  object, array, or scalar — relayed verbatim). Unknown top-level keys are stripped.
- WebhookSubscriptionValidator: a required, absolute `webhookUrl`.

Validation failures throw a ValidupError that the already-mounted error middleware
renders as a 400 with issue details. Adds a focused validator spec alongside the
controller tests.
@tada5hi tada5hi merged commit 27fbd3f into master Jun 26, 2026
7 checks passed
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.

S5 — Container-facing message endpoints (send/broadcast/participants/pull)

1 participant