refactor(broker): merge HTTP+WS to single port, populate senderPubkey on push

Single-port refactor:
- Drop the BROKER_PORT+1 HTTP side-port. Use `ws` with noServer:true
  and attach to a single node:http server via the 'upgrade' event.
- Clients connect to ws://host:PORT/ws
- Hook POSTs go to http://host:PORT/hook/set-status
- Health probe at http://host:PORT/health
- One port = one Traefik label, one cert, one deploy route. Matches
  the Coolify/VPS operational constraints.

senderPubkey on push:
- drainForMember now joins mesh.message_queue → mesh.member to return
  the sender's peerPubkey alongside each envelope. No extra round-trip,
  no cache invalidation needed (option A from review).
- index.ts populates WSPushMessage.senderPubkey from the join result
  instead of the empty-string placeholder.
- Receivers can now identify who sent a message directly from the push.

README updated with a routes table for the single-port layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:35:05 +01:00
parent 3c0154ae70
commit 0a97a0c369
3 changed files with 106 additions and 76 deletions

View File

@@ -369,13 +369,17 @@ function deliverablePriorities(status: PeerStatus): Priority[] {
/**
* Drain deliverable messages addressed to a specific member in a mesh.
* Marks them delivered and returns the envelopes for the caller to
* push over WebSocket. Does NOT handle targetSpec routing — that's the
* responsibility of the ingress fanout (see queueForTargets).
* Joins mesh.member so each envelope carries the sender's pubkey, which
* the receiving client needs to identify who sent it. Marks drained
* rows as delivered and returns the envelopes for WS push.
*
* targetSpec routing: matches either the member's pubkey directly or
* the broadcast wildcard ("*"). Channel/tag resolution is per-mesh
* config that lives outside this function.
*/
export async function drainForMember(
meshId: string,
memberId: string,
_memberId: string,
memberPubkey: string,
status: PeerStatus,
): Promise<
@@ -386,13 +390,10 @@ export async function drainForMember(
ciphertext: string;
createdAt: Date;
senderMemberId: string;
senderPubkey: string;
}>
> {
const priorities = deliverablePriorities(status);
// A message is deliverable to this member if its targetSpec
// addresses them directly (pubkey match) or is a broadcast.
// Channel/tag resolution is a per-mesh concern layered on top.
const targetFilter = or(
eq(messageQueue.targetSpec, memberPubkey),
eq(messageQueue.targetSpec, "*"),
@@ -406,8 +407,10 @@ export async function drainForMember(
ciphertext: messageQueue.ciphertext,
createdAt: messageQueue.createdAt,
senderMemberId: messageQueue.senderMemberId,
senderPubkey: memberTable.peerPubkey,
})
.from(messageQueue)
.innerJoin(memberTable, eq(memberTable.id, messageQueue.senderMemberId))
.where(
and(
eq(messageQueue.meshId, meshId),
@@ -432,6 +435,7 @@ export async function drainForMember(
ciphertext: r.ciphertext,
createdAt: r.createdAt,
senderMemberId: r.senderMemberId,
senderPubkey: r.senderPubkey,
}));
}