/** * Inbound message dispatcher (ADR-109). * * The federation plugin's `transport.listen()` accepts inbound bytes * but until this module landed, the bytes had no consumer — they sat * in the transport's per-address message queue with no one polling. * * `dispatchInbound()` is what the plugin registers as the * transport.onMessage handler. For each received message it: * * 1. Resolves the sender via `sourceNodeId` in metadata * 2. Verifies the peer is in the discovery registry (unknown → reject) * 3. Verifies peer is ACTIVE (SUSPENDED/EVICTED → reject defense-in-depth) * 4. Audits success or rejection * 5. Emits a typed event on the eventBus for the integrator to handle * * Anti-coupling: dispatcher does NOT actually execute inbound tasks. * That's the integrator's job. The dispatcher's contract is "deliver * an envelope to the integrator's handler IFF it passes safety gates." */ import type { AgentMessage } from '../transport/midstream-aware-loader.js'; import type { DiscoveryService } from '../domain/services/discovery-service.js'; import type { AuditService } from '../domain/services/audit-service.js'; import type { FederationNode } from '../domain/entities/federation-node.js'; /** What gets emitted on the event bus per messageType. */ export declare const FEDERATION_INBOUND_EVENT_PREFIX = "federation:inbound"; /** Reasons we reject an inbound message — constant strings, no oracle leak. */ export type InboundRejectionReason = 'PEER_UNKNOWN' | 'PEER_SUSPENDED' | 'PEER_EVICTED' | 'MISSING_METADATA' | 'INVALID_PAYLOAD' | 'INVALID_SIGNATURE' | 'LEGACY_SIGNATURE_TYPE_REJECTED' | 'AUTHORIZATION_DENIED' | 'AUTHORIZATION_ERROR'; /** * Verifier function for inbound envelopes. Given the canonical bytes of * the message + the claimed signature + the peer's published public * key, returns true iff the signature is valid. Plugin wires this with * `@noble/ed25519`; tests inject a mock. * * Returning `null` means "no signature provided" — handled by the * dispatcher as INVALID_SIGNATURE (defense: unsigned messages from * known peers are still rejected). */ export type EnvelopeVerifier = (canonicalBytes: string, signatureHex: string | null, peerPublicKeyHex: string) => boolean; export declare const JCS_SIGNATURE_PROTOCOL: "ruflo-signature-jcs-v1"; export type EnvelopeSignatureVersion = 'legacy-v1' | 'jcs-v1'; export type EnvelopeSignatureMode = 'legacy' | 'prefer-jcs' | 'require-jcs'; /** * Updated peers advertise and negotiate recursively complete JCS signatures * without operator configuration. Legacy mode remains explicit for low-risk * heartbeat/status interoperability only. */ export declare const DEFAULT_ENVELOPE_SIGNATURE_MODE: EnvelopeSignatureMode; export type InboundAuthorizationMode = 'legacy' | 'observe' | 'enforce'; export declare function isLegacyEnvelopeTypeAllowed(messageType: string): boolean; export interface InboundAuthorizationRequest { readonly address: string; readonly sourceNodeId: string; readonly messageType: string; readonly message: AgentMessage; readonly peer: FederationNode; readonly signatureVersion: EnvelopeSignatureVersion; readonly messageSizeBytes: number; } export interface InboundAuthorizationDecision { readonly allowed: boolean; readonly reason?: string; } export type InboundAuthorizationEvaluator = (request: InboundAuthorizationRequest) => InboundAuthorizationDecision | Promise; /** Dispatch dependencies (kept narrow for testability). */ export interface InboundDispatchDeps { readonly discovery: Pick; readonly audit: Pick; readonly eventBus: { emit: (event: string, data: unknown) => void; }; readonly logger: { debug: (m: string) => void; warn: (m: string) => void; }; /** * Optional Ed25519 envelope verifier. When PROVIDED, every accepted * message MUST pass verification — `null` signature or false-returning * verifier rejects the message as INVALID_SIGNATURE. * * When OMITTED, the dispatcher operates in legacy "trust the metadata" * mode (backward compat for tests that inject minimal deps). Production * MUST inject this — see the plugin.ts wiring. */ readonly verifyEnvelope?: EnvelopeVerifier; /** Defaults to both versions; enforce-only deployments pass `['jcs-v1']`. */ readonly acceptedSignatureVersions?: readonly EnvelopeSignatureVersion[]; /** Compatibility mode defaults to legacy; enforce is fail-closed. */ readonly authorizationMode?: InboundAuthorizationMode; /** Injected policy decision point. It is always called before event emission. */ readonly authorizeInbound?: InboundAuthorizationEvaluator; } export declare function selectEnvelopeSignatureVersion(mode: EnvelopeSignatureMode, peerProtocols: readonly string[], messageType?: string): EnvelopeSignatureVersion; export declare function canonicalizeEnvelopeForVerify(message: AgentMessage, requestedVersion?: EnvelopeSignatureVersion): string; /** Outcome reported to the caller (mostly for tests + observability). */ export type InboundDispatchOutcome = { readonly accepted: true; readonly sourceNodeId: string; readonly messageType: string; } | { readonly accepted: false; readonly reason: InboundRejectionReason; }; /** * Process one received message. Pure-ish: no side effects beyond audit * + event emission, both injected. * * `address` is the wire-level remote (e.g. `192.168.1.42:54321`). * `message.metadata.sourceNodeId` is the cryptographic identity claim. * The two MAY differ (e.g. behind NAT) — we trust `sourceNodeId` for * peer lookup since it's bound to the Ed25519 keypair. */ export declare function dispatchInbound(address: string, message: AgentMessage, deps: InboundDispatchDeps): Promise; //# sourceMappingURL=inbound-dispatcher.d.ts.map