196 lines
8.1 KiB
Markdown
196 lines
8.1 KiB
Markdown
# OpenSpec Change: network-plugin-relay
|
|
|
|
- **Change ID**: network-plugin-relay
|
|
- **Package**: `@suaveplan/network-plugin-relay` (new)
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-05-01
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Implement cross-mesh message relay — the "federation layer" that enables Tier 0 hosts of different meshes to route messages to one another. Without this plugin, the network suite only supports intra-mesh communication; peers in Mesh A cannot send messages to peers in Mesh B. This is a P0 gap blocking the user's stated requirement: *"message relaying through the hosts through the nominated tier host to be able to transmit in any pocket mesh on any tier to any other pocket mesh on any tier."*
|
|
|
|
---
|
|
|
|
## Why
|
|
|
|
**User requirement**: *"message relaying through the hosts through the nominated tier host to be able to transmit in any pocket mesh on any tier to any other pocket mesh on any tier"*
|
|
|
|
**Current state**: ALL existing network proposals handle **intra-mesh** communication only (full mesh within one group). No package addresses **inter-mesh** routing.
|
|
|
|
**What's missing**:
|
|
|
|
- Host-to-host backbone network between Tier 0 hosts of different meshes
|
|
- Message forwarding/relay logic at the host level
|
|
- Cross-mesh addressing scheme (`meshId + peerId` vs just `peerId`)
|
|
- Route discovery between meshes
|
|
- Concept of a "super-peer" or "federation backbone"
|
|
|
|
---
|
|
|
|
## Core components
|
|
|
|
### 1. `RelayRouter`
|
|
|
|
Maintains a routing table: `(destinationMeshId, destinationPeerId) → nextHopHostId`. Runs on every Tier 0 host. Updates dynamically from `MeshDirectory` sync events.
|
|
|
|
```ts
|
|
class RelayRouter {
|
|
addRoute(destMeshId: string, destPeerId: string, nextHopHostId: string): void;
|
|
removeRoute(destMeshId: string, destPeerId: string): void;
|
|
resolve(targetMeshId: string, targetPeerId: string): string | null; // returns nextHopHostId
|
|
getRoutesForMesh(meshId: string): Map<string, string>;
|
|
}
|
|
```
|
|
|
|
### 2. `HostBackboneTransport`
|
|
|
|
Dedicated transport for Tier 0 ↔ Tier 0 communication. Reuses `WebSocket` or `RTCDataChannel` but is logically separate from peer mesh transport. Negotiates capabilities (max message size, supported compression, crypto suites).
|
|
|
|
```ts
|
|
interface HostBackboneTransport {
|
|
connect(hostId: string, address: string): Promise<void>;
|
|
disconnect(hostId: string): void;
|
|
send(hostId: string, envelope: CrossMeshEnvelope): void;
|
|
onMessage(handler: (envelope: CrossMeshEnvelope, fromHostId: string) => void): void;
|
|
getConnectedHosts(): string[];
|
|
}
|
|
```
|
|
|
|
### 3. `CrossMeshEnvelope`
|
|
|
|
Extends `MessageEnvelope` with cross-mesh routing metadata:
|
|
|
|
```ts
|
|
interface CrossMeshEnvelope {
|
|
// Inherited from MessageEnvelope
|
|
id: string;
|
|
type: string;
|
|
senderId: string;
|
|
senderMeshId: string;
|
|
targetId: string;
|
|
targetMeshId: string;
|
|
payload: Uint8Array;
|
|
timestamp: number;
|
|
headers: Record<string, string>;
|
|
// Relay-specific
|
|
hopCount: number; // incremented per hop
|
|
maxHops: number; // default 16
|
|
visitedNodes: string[]; // peerIds / hostIds visited
|
|
ttl: number; // ms remaining
|
|
}
|
|
```
|
|
|
|
Zod-validated. Dropped when `hopCount >= maxHops`, `ttl <= 0`, or `visitedNodes` contains current node.
|
|
|
|
### 4. `RelayManager`
|
|
|
|
Plugin entry point. Intercepts outbound messages where `targetMeshId !== localMeshId` and forwards them via `RelayRouter` → `HostBackboneTransport`. On inbound cross-mesh messages, delivers to local `NetworkBus` scoped to the target peer.
|
|
|
|
```ts
|
|
class RelayManager {
|
|
constructor(options: {
|
|
router: RelayRouter;
|
|
backbone: HostBackboneTransport;
|
|
localMeshId: string;
|
|
localHostId: string;
|
|
maxHops: number;
|
|
defaultTtlMs: number;
|
|
});
|
|
start(): void;
|
|
stop(): void;
|
|
onOutbound(envelope: MessageEnvelope): CrossMeshEnvelope | null; // null = not relayed
|
|
onInbound(envelope: CrossMeshEnvelope): void;
|
|
}
|
|
```
|
|
|
|
### 5. `MeshDirectory`
|
|
|
|
Discovery/registration of known meshes and their Tier 0 hosts. Can be seed-based (static config), server-assisted (via `network-plugin-server-bridge`), or gossip-based (Tier 0 hosts exchange mesh advertisements).
|
|
|
|
```ts
|
|
class MeshDirectory {
|
|
registerMesh(meshId: string, tier0HostIds: string[], hostAddresses: Map<string, string>): void;
|
|
unregisterMesh(meshId: string): void;
|
|
getTier0Hosts(meshId: string): string[];
|
|
getHostAddress(hostId: string): string | undefined;
|
|
listKnownMeshes(): string[];
|
|
on(event: "mesh:added" | "mesh:removed" | "mesh:updated", handler: Function): void;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Routing rules
|
|
|
|
1. **Same mesh** — `RelayManager` returns `null`; message stays local.
|
|
2. **Different mesh, direct backbone exists** — forward to connected Tier 0 host that advertises the target mesh.
|
|
3. **Different mesh, multi-hop required** — `RelayRouter` selects next hop from its table; `hopCount` increments; `visitedNodes` appends current node.
|
|
4. **Broadcast across meshes** — `targetMeshId = "*"`, `targetId = "*"`. Relayed to ALL known meshes. Deduplicated at each mesh boundary via `messageId` Bloom filter.
|
|
5. **Loop prevention** — Hard drop if `hopCount >= maxHops` or `visitedNodes` contains current node.
|
|
|
|
---
|
|
|
|
## Error codes
|
|
|
|
```ts
|
|
const RELAY_CODES = defineCodes("RELAY", [
|
|
"ROUTE_NOT_FOUND",
|
|
"BACKBONE_CONNECT_FAILED",
|
|
"BACKBONE_DISCONNECTED",
|
|
"MAX_HOPS_EXCEEDED",
|
|
"TTL_EXPIRED",
|
|
"LOOP_DETECTED",
|
|
"INVALID_ENVELOPE",
|
|
"MESH_NOT_REGISTERED",
|
|
"HOST_NOT_FOUND",
|
|
"RELAY_REJECTED",
|
|
] as const);
|
|
```
|
|
|
|
`RelayError extends SuaveplanError<RelayErrorCode>`.
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
- **Consumes**: `@suaveplan/network` (`NetworkNode`, `NetworkBus`, `MessageEnvelope`, `NetworkPlugin`), `@suaveplan/network-plugin-topology` (tier info for backbone eligibility), `@suaveplan/network-plugin-registry` (peer lists for `MeshDirectory`), `@suaveplan/network-plugin-crypto-messages` (signature verification on cross-mesh envelopes), `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/types`, `zod`, `@opentelemetry/api`.
|
|
- **Blocked on**: `network-core` (shipped), `network-plugin-topology`, `network-plugin-registry`.
|
|
|
|
---
|
|
|
|
## Verification approach
|
|
|
|
- `src/router/router.spec-conformance.test.ts` — pins routing table add/remove/resolve, multi-hop resolution.
|
|
- `src/backbone/backbone.spec-conformance.test.ts` — pins HostBackboneTransport connect/send/onMessage.
|
|
- `src/envelope/envelope.spec-conformance.test.ts` — pins Zod validation, hopCount/ttl/visitedNodes rules.
|
|
- `src/manager/manager.spec-conformance.test.ts` — pins outbound intercept, inbound delivery, same-mesh bypass.
|
|
- `src/directory/directory.spec-conformance.test.ts` — pins mesh registration, host lookup, event emission.
|
|
- `src/loop-prevention/loop-prevention.spec-conformance.test.ts` — pins maxHops drop, ttl drop, visitedNodes drop.
|
|
- `tests/integration/relay.spec-conformance.test.ts` — end-to-end: Mesh A peer → Mesh B peer via Tier 0 backbone.
|
|
- 100/100/100/100 Istanbul coverage.
|
|
|
|
---
|
|
|
|
## Conformance requirements (proposed)
|
|
|
|
- `NET-61` — `RelayRouter` maintains a routing table from `(destMeshId, destPeerId)` to `nextHopHostId`.
|
|
- `NET-62` — `HostBackboneTransport` connects Tier 0 hosts with capability negotiation.
|
|
- `NET-63` — `CrossMeshEnvelope` extends `MessageEnvelope` with `senderMeshId`, `targetMeshId`, `hopCount`, `maxHops`, `visitedNodes`, and `ttl`.
|
|
- `NET-64` — Cross-mesh messages are dropped when `hopCount >= maxHops`.
|
|
- `NET-65` — Cross-mesh messages are dropped when `ttl <= 0`.
|
|
- `NET-66` — Cross-mesh messages are dropped when `visitedNodes` contains the current node.
|
|
- `NET-67` — `MeshDirectory` registers known meshes and their Tier 0 hosts.
|
|
- `NET-68` — Broadcast across meshes (`targetMeshId = "*"`) is relayed to all known meshes and deduplicated at mesh boundaries.
|
|
- `NET-69` — `RelayManager` intercepts outbound messages where `targetMeshId !== localMeshId` and forwards them.
|
|
- `NET-70` — Inbound cross-mesh envelopes are delivered to the local `NetworkBus` scoped to the target peer.
|
|
|
|
---
|
|
|
|
## Out of scope
|
|
|
|
- Cross-mesh **state synchronization** → `network-plugin-state` handles state sync within a mesh; cross-mesh state is application-level.
|
|
- Global DHT for mesh discovery → future hardening; seed-based and server-assisted discovery are sufficient for v1.
|
|
- Cross-mesh **host election** → each mesh governs itself; no global host.
|