Nexus Architecture

Complete Overview of @hyperfrontend/nexus


Overview

@hyperfrontend/nexus is a cross-window communication library designed for micro-frontend architectures. It implements a TCP-like connection protocol over the browser's postMessage API, providing secure, contract-validated messaging between browser contexts (iframes, windows, and web workers).

Target Use Cases

  1. Micro-frontend communication: Host applications coordinating multiple micro-apps
  2. Iframe integration: Secure bidirectional messaging with embedded content
  3. Multi-window applications: Communication between browser windows/tabs
  4. Plugin architectures: Host-to-plugin communication with contract enforcement

Architecture Overview


Design Philosophy

The library follows functional programming principles with factory-based architecture.

Key Design Decisions

AspectImplementationRationale
State ManagementClosure-based encapsulationTrue information hiding, prevents external mutation
Factory PatterncreateBroker(), createChannel()Testable, composable, no class inheritance complexity
RegistryInstance-based with WeakMap/MapO(1) lookups, memory-efficient, multiple brokers supported
Process TrackingUUID-based ProcessManagerClean lifecycle management for in-flight connections
Router PatternHandler registry keyed by action typeSingle responsibility, extensible protocol
ImmutabilityObject.freeze(), spread patternsPredictable state transitions

Notable Patterns

  • Functional Core, Imperative Shell: Pure logic in core, side effects at boundaries
  • Handler Registry: Extensible protocol handling
  • WeakMap for Window References: Prevents memory leaks
  • Immutable State Updates: Predictable state transitions

Module Organization

The library is organized into logical modules by responsibility:

ModuleResponsibility
BrokerCentral message coordinator, channel management
ChannelBidirectional communication endpoints, lifecycle
SecurityOrigin filtering, protocol negotiation, sealed transport
FiltersEvent and message filtering utilities
SchemaJSON Schema validation for contracts

Core Concepts

1. Broker (BrokerHandle)

The broker is the central coordinator that:

  • Manages multiple channels
  • Listens for incoming postMessage events
  • Routes messages to appropriate handlers
  • Enforces security policies
interface BrokerHandle {
  readonly id: string
  readonly name: string
  readonly contract: IChannelContract
  readonly settings: BrokerSettings
  readonly channels: ReadonlyArray<ChannelJSON>
  readonly acceptedActionTypes: readonly string[]
  readonly logger: Logger

  addChannel(name: string, target: Window, settings?: Record<string, unknown>): ChannelHandle
  getChannel(reference: string | Window): ChannelHandle | null
  removeChannel(reference: string | Window): void
  setSecurityPolicy(policy: SecurityPolicy): BrokerHandle
  extendContract(contract: IChannelContract): BrokerHandle
  toJSON(): Record<string, unknown>

  // Security protocol management
  registerProtocol(version: SecurityProtocolVersion, provider: SecurityProvider): BrokerHandle
  unregisterProtocol(version: SecurityProtocolVersion): BrokerHandle
  hasProtocol(version: SecurityProtocolVersion): boolean
  getSupportedProtocols(): SecurityProtocolVersion[]
}

2. Channel (ChannelHandle)

A channel represents a communication endpoint to another window:

interface ChannelHandle {
  readonly id: string
  readonly name: string
  readonly target: Window

  // State queries
  isActive(): boolean
  toJSON(): ChannelJSON

  // Lifecycle
  connect(): void
  disconnect(notify?: boolean): void
  cancel(notify?: boolean): void
  destroy(notify?: boolean): void

  // Communication
  send(type: string, data?: unknown): void

  // Subscriptions
  on(handler: EventHandler): () => void
  on<E extends ChannelEvent>(event: E, handler: EventCallbackMap[E]): () => void
  onMessage(handler: MessageHandler): () => void
}

3. Contract (IChannelContract)

Contracts define the messaging agreement between parties:

interface IChannelContract {
  emitted: IActionDescription[] // Message types this party sends
  accepted: IActionDescription[] // Message types this party receives
  version?: string // Optional version announcement crossed during the handshake
}

interface IActionDescription {
  type: string // Message type identifier
  description?: string // Human-readable description
  schema?: object // Optional JSON Schema for validation
  required?: boolean // Accepted entries only: deny the connection unless the peer emits this type
}

Contracts are exchanged during the handshake. Unknown inbound types are dropped and logged; only accepted entries flagged required gate the connection (the peer must emit them), so additive contract evolution is non-breaking in both directions. See Contract Compatibility for the full gating rules, including the optional version announcement and the channel-supplied contractCompat rule.

4. Actions (Protocol Messages)

The protocol defines 12 action types for connection lifecycle:

Action TypePurpose
REQUEST_CONNECTIONInitiate connection (SYN)
ACCEPT_CONNECTIONAccept connection (SYN-ACK)
OPEN_CONNECTIONConfirm connection (ACK)
DENY_CONNECTIONReject connection (RST)
CANCEL_CONNECTIONCancel pending connection
CANCEL_CONNECTION_ACKNOWLEDGEDAcknowledge cancellation
CLOSE_CONNECTIONGraceful disconnect
CLOSE_CONNECTION_ACKNOWLEDGEDAcknowledge disconnect
DESTROY_CONNECTIONForce disconnect
NEW_MESSAGEUser data transmission
INVALID_REQUESTProtocol violation
SECURITY_CONFIRMEDSealed session confirmation (consumed by the transport, never routed)

The first six are the handshake actions and always travel in plaintext; once a channel has a security transport, every other action crosses the wire sealed (see Layer 4).


Protocol Design

Three-Way Handshake

Nexus implements a TCP-like handshake for reliable connection establishment:

Initiation is symmetric: either side may connect() first, and simultaneous requests (glare) resolve by broker-id tie-break (the lower id yields and answers as responder). Pending REQUEST/ACCEPT frames are re-sent every requestRetryMs (default 500 ms) until answered; all three handshake messages are idempotent under replay. A handshake that stays unanswered past connectTimeoutMs (default 10 000 ms) fires connect-timeout and leaves the channel inactive, reconnectable, with queued messages retained. Each side pins the counterpart's origin during the handshake; subsequent sends target the pin and mismatched inbound origins are dropped. A channel can also be pre-pinned via the origin setting before the first message leaves.

Internal Sequence:

Instance Identity

Every broker mints a UUID when it boots and stamps it on every action it sends (senderId). It is a machine identity, not a label (the broker's name is the readable one), and it does three jobs a name cannot: it is the endpoint identifier the encrypted wire format requires (each packet carries the sender's and the target's id, both validated as UUID v4), the ordinal that settles glare without an extra round trip, and the identity of one incarnation of the counterpart.

That last job matters because a window outlives the documents loaded into it. Routing resolves an inbound frame by its source window, which identifies the window, not what is running inside it. So the channel records the counterpart's id at handshake time (peerId, exposed on ChannelJSON) and, while a session is open, ignores frames stamped with any other id: product messages, CLOSE, CANCEL, DESTROY, and the OPEN that completes a handshake. Traffic left over from a document that has been replaced is dropped instead of entering the session that replaced it.

REQUEST is the exception, because it is how a new incarnation announces itself. A REQUEST carrying a different id on a connected channel means the window reloaded (or navigated in-frame): the session it belonged to ends silently. close fires with reason: 'peer-reload' so subscribers can drop session-scoped state, and the same channel re-handshakes with the new instance. The channel is never removed, and security and contract compatibility are renegotiated from scratch; only the origin pin carries over.

The id is cooperative, not a credential: any script that can post to the window can claim one, so the check is a correctness mechanism (and a hurdle for a co-resident script that has never observed the id), while origin pinning stays the boundary. Inside a sealed envelope it is authenticated, because producing a frame at all requires the session keys.

Contract Compatibility

Both sides exchange contracts during the handshake. Vocabulary differences never gate the connection: only accepted entries flagged required: true do, and each must appear in the counterpart's emitted list or the connection is denied with Incompatible contract: missing required actions …. This keeps additive contract evolution non-breaking in both directions:

SituationFatal?Handling
Peer emits a type outside my vocabularyNoDropped and logged at receive
I accept a type the peer never emitsNoNothing: dormant vocabulary
I emit a type the peer does not acceptNoPeer drops it; the peer's own required flags decide
I require an input the peer never emitsYesConnection denied at handshake time

A contract may also carry an optional version string. Nexus attaches no semantics to it; supply a contractCompat rule in the channel settings to decide whether two contracts may interoperate:

const channel = broker.addChannel('partner', partnerWindow, {
  contractCompat: (own, peer) =>
    own.version === peer.version ? { compatible: true } : { compatible: false, reason: `${own.version} does not match ${peer.version}` },
})

The rule runs at the same handshake gate as the required-actions check, on whichever side holds it, receiving the local contract and the counterpart's. On the responder (REQUEST time) an incompatible result sends DENY with the rule's reason and reason: 'incompatible-contract', so the deny event fires on both the denying responder and the denied initiator. On the initiator (ACCEPT time) an incompatible result aborts the handshake: the initiator fires its own deny event with the same reason and sends CANCEL to the counterpart, which observes a cancel, not a deny.

Denial Flow

When a connection is rejected during REQUEST handling:

Every gate carries a machine-readable reason ('invalid-contract', 'missing-required-actions', 'policy-rejected', 'incompatible-contract', or 'security-unavailable') alongside the human-readable error, and every gate fires the denial locally on the responder as a deny event. A denying side is never left waiting on a channel it refused: without the local event, a responder that yielded the glare tie-break has already cleared its handshake timers and would see neither deny nor connect-timeout. The local event fires once per handshake process: the initiator retries REQUEST while pending, and each retry is answered with another DENY frame without re-notifying the responder's subscribers. On the initiator, handleDeny stops the request retries and removes the tracked process, so duplicate DENY frames are no-ops and the deny event fires once.

The DENY frame discloses less than the local event for one gate. A policy rejection tells the refused requester only error: 'Not accepted.' with no reason, because naming the gate would tell an origin the policy just refused how this side judges connections; the local event names the rejected origin and carries reason: 'policy-rejected'. The other gates disclose the same error and reason both ways: an invalid or under-specified contract is the requester's own artifact, so the detail is actionable on both ends.

Cancellation Flow

Either party can cancel before the connection completes: CANCEL_CONNECTION is answered with CANCEL_CONNECTION_ACKNOWLEDGED, and both sides fire the cancel event. The initiator-side gates that run at ACCEPT time (invalid contract, missing required actions, security policy, contract compatibility, fail-closed security) also abort through this verb: the aborting initiator sends CANCEL to the counterpart, logs an operator warning naming the channel, and fires a local deny with the gate's reason, once per handshake process, so a replayed ACCEPT that raced the CANCEL does not notify twice. The counterpart observes a cancel, not a deny: an aborted acceptance is indistinguishable on the wire from any other cancellation.

Graceful Disconnection

The polite close is a flush-then-confirm exchange. The disconnector posts CLOSE, fires closing ({ initiatedLocally: true }), and stays active so the partner's final sends still deliver; its single close fires only when the acknowledgement arrives, or when closeTimeoutMs (default 2 s) expires, so an unresponsive partner cannot hold the channel open. The partner fires closing ({ initiatedLocally: false }) while the channel still delivers (subscribers may synchronously send final messages, which arrive before the acknowledgement), then acknowledges, deactivates, and fires its single close. New sends issued after a close was proposed queue for the next connection instead of racing the CLOSE. Simultaneous polite closes (close glare) acknowledge each other and still fire exactly one closing and one close per side. destroy() remains the immediate, unacknowledged teardown.

State Transitions


Handler Reference

Each protocol action is processed by a dedicated handler. All handlers receive the broker state, channel registry, process manager, and incoming message.

HandlerResponsibilities
handleRequestEnforce origin pin, resolve glare/reload (a new instance in the window ends the stale session with reason: 'peer-reload'), validate contract + requirements + compat rule, apply policy, negotiate security against the protocol registry (a channel that selected a protocol offers that protocol or plaintext only; deny fail-closed plaintext outcomes, firing the local 'deny' once per process), attach the security transport before ACCEPT leaves (a scheduled answer attaches it when connect() composes the ACCEPT), track process, pin origin, send ACCEPT with retry/deadline (or schedule until connect()), start the transport so the hello follows ACCEPT
handleAcceptResolve by process or source window, enforce origin pin, drop an ACCEPT that does not answer the pending request (with an 'invalid' event), validate contract + requirements + compat rule, apply policy, settle security (a protocol other than the one the channel asked for counts as plaintext) and attach the transport before OPEN leaves (abort fail-closed plaintext outcomes via CANCEL + local 'deny'), activate + flush (queued traffic waits in the seal stage until keyed), send OPEN confirming the security outcome, start the transport so the hello follows OPEN, notify 'open'
handleOpenIgnore an OPEN from another instance (leaving the process intact), check the initiator's confirmation against the negotiated protocol (keep the transport attached at ACCEPT time when it matches; release it and record plaintext otherwise, refusing fail-closed outcomes via CANCEL + local 'deny'), activate from the pending accept, flush queue, terminate process, notify 'open' (responder side)
handleDenyAbandon the pending request (stop retrying), terminate process, notify 'deny' with error context
handleCancelIgnore a CANCEL from another instance, else cancel channel, send CANCEL_ACK, notify 'cancel'
handleCancelAcknowledgedTerminate process, notify 'cancel' (initiator side)
handleCloseIgnore a CLOSE from another instance, else notify 'closing' (flush window, channel still active), send CLOSE_ACK, then deactivate and notify a single 'close'
handleCloseAcknowledgedComplete the initiator's polite close: deactivate, terminate process, notify its single 'close' (ignores stray acks for channels not closing)
handleMessageDrop and log messages from another instance, validate payload, forward to subscribers via notifyMessage()
handleDestroyIgnore a DESTROY from another instance, else force-destroy connection, clean up resources
handleInvalidLog invalid requests, optionally notify sender: see handle-invalid.ts

Event System

Channels emit lifecycle events to subscribers. Each event has a specific trigger and payload structure.

Lifecycle Events

EventTriggerPayload
'open'Connection successfully established{ origin, contract }
'closing'Polite close proposed; channel still delivers{ initiatedLocally: boolean }
'close'Close completed (fires once per side){ notify: boolean, reason? }
'cancel'Pending connection cancelled{ notify: boolean }
'deny'Connection request rejected (either side's gates){ error?, reason?, origin?}
'invalid'Protocol violation or unexpected-origin drop{ error, action? }
'connect-timeout'Handshake deadline expired with no answer{ elapsedMs }

The close payload's optional reason (CloseReason) is set only when neither side asked for the close: 'peer-reload' when the session ended because the target window now hosts a different instance (see Instance Identity), and 'security-unconfirmed' when a sealed session was never confirmed within connectTimeoutMs (a silent close: no CLOSE frame travels, see Layer 4). The deny payload's reason is machine-readable and typed as DenyReason: 'invalid-contract' (the counterpart's contract failed structural validation), 'missing-required-actions' (it does not emit an action this side accepts as required: true), 'policy-rejected' (the broker's securityPolicy refused the exchange), 'incompatible-contract' (a contractCompat rule rejected the pair), or 'security-unavailable' (a fail-closed channel could not obtain an encrypted transport). The union stays open, so a counterpart running a newer protocol can report a reason this build does not know yet. The invalid event fires with { error, action? } for unexpected-origin drops, and with { reason, origin } when the counterpart reports an INVALID_REQUEST frame.

Connection Outcomes

A connection attempt ends in one of four distinct ways, each with its own event:

EventA session existed?Deliberate?Who decidedWire evidence of a peerNatural reaction
closeYesYesEither sideYes (CLOSE/ACK)Handle disconnect
cancelNoYesEither sideYes (CANCEL verb)Accept abandonment
denyNoYesA gate, with a reasonYes (DENY + reason)Fix the integration
connect-timeoutNoNoNobody: silenceNoneFallback UI, retry later

Security Events

EventPayloadDescription
security-ready{ protocol }The counterpart's first sealed frame authenticated: the session is confirmed on this side
security-error{ message, code, cause? }A frame was dropped in either direction, a hello was rejected, or the session failed

security-ready follows open: it fires once per session, when the first frame the counterpart sealed under the session keys authenticates, whether that frame is the counterpart's SECURITY_CONFIRMED control action or a product message. security-error reports every dropped frame, so a message one side sent and the other never delivered is never silent. Its code (SecurityErrorCode) is one of the wire protocol's verdicts on a single frame ('unsupported-version', 'replayed', 'authentication-failed', 'malformed', 'counter-exhausted', 'invalid-session') or a transport-level code: 'hello-rejected' (a hello arrived that differs from the one keying the session), 'security-unconfirmed' (the confirmation deadline expired), 'transport-error' (a packet could not be sealed or handed to the wire), or 'unknown'. Two codes end the session rather than describe a single frame: 'security-unconfirmed' and 'invalid-session' are followed by the silent close with reason: 'security-unconfirmed' (or, on a responder still awaiting OPEN, by a CANCEL and a local deny with reason: 'security-unavailable').

Event Subscription

// Subscribe to a specific event (recommended)
channel.on('open', (data) => console.log('Opened:', data.origin))
channel.on('close', (data) => console.log('Closed'))
channel.on('security-ready', (data) => console.log(`Secure channel using ${data.protocol}`))

// Subscribe to all events (for generic handling)
channel.on((event, data) => {
  switch (event) {
    case 'open':
      console.log(`Connected to ${data.origin}`)
      break
    case 'close':
      console.log('Connection closed')
      break
  }
})

// Use filter utilities for advanced composition
import { openFilter, closeFilter } from '@hyperfrontend/nexus'

channel.on(openFilter((data) => console.log('Opened:', data.origin)))
channel.on(closeFilter((data) => console.log('Closed')))

Logging System

Nexus provides a configurable logging system that routes all internal output through a Logger interface from @hyperfrontend/logging.

Logger Interface

interface Logger {
  error(...args: unknown[]): void
  warn(...args: unknown[]): void
  log(...args: unknown[]): void
  info(...args: unknown[]): void
  debug(...args: unknown[]): void
  setLogLevel(level: LogLevel): void
  getLogLevel(): LogLevel
}

type LogLevel = 'error' | 'warn' | 'log' | 'info' | 'debug' | 'none'

Logger Flow

  1. Broker initialization: createBroker() creates or adopts a logger based on settings.logLevel and settings.logger
  2. Channel inheritance: Channels created via broker.addChannel() inherit the broker's logger
  3. RoutingContext: All routing handlers receive the logger via RoutingContext

RoutingContext

All routing handlers receive a RoutingContext object containing shared dependencies:

interface RoutingContext {
  readonly state: BrokerState // Immutable broker state snapshot
  readonly registry: Registry // Channel registry for lookups
  readonly processManager: ProcessManager // Tracks handshake processes
  readonly actions: ActionCreators // Factory functions for protocol actions
  readonly logger: Logger // Logger instance for this broker
  readonly getSupportedProtocols: () => readonly SecurityProtocolVersion[] // Registry-sourced negotiable protocols, 'none' last
  readonly security: ChannelSecurityDependencies // localId, getProvider(protocol), dispatch(event): what a channel needs to run a transport
}

This pattern:

  • Eliminates parameter proliferation across handlers
  • Makes testing straightforward (mock the context)
  • Provides clean access to logger without prop drilling

Structured Logging Utilities

UtilityPurposeOutput Format
logActionProtocol action tracing[nexus] Action <direction>: <type> <action>
logEventChannel lifecycle event logging[nexus] Channel event: <event> <data>

createLogger Factory

import { createLogger, type NexusLoggerOptions } from '@hyperfrontend/nexus'

interface NexusLoggerOptions {
  level?: LogLevel // Default: 'error'
  prefix?: string // Default: '[nexus]'
  customLogger?: Logger // Use this logger directly if provided
}

const logger = createLogger({ level: 'debug', prefix: '[app]' })

Custom Logger Injection

Verbosity is controlled with the logLevel setting; a custom logger (Winston, Pino, etc.) can be injected for production:

const broker = createBroker({
  name: 'production-broker',
  contract,
  settings: {
    logger: {
      error: (...args) => myLogger.error(args.join(' ')),
      warn: (...args) => myLogger.warn(args.join(' ')),
      log: (...args) => myLogger.info(args.join(' ')),
      info: (...args) => myLogger.info(args.join(' ')),
      debug: (...args) => myLogger.debug(args.join(' ')),
      setLogLevel: () => {},
      getLogLevel: () => 'info',
    },
  },
})

Channels inherit the broker's logger; it is exposed via broker.logger.


Security Model

Nexus provides a multi-layered security approach:

Layer 1: Origin Filtering

Basic origin-based access control, applied to every inbound message before routing. A non-empty whitelist takes precedence over the blacklist:

const broker = createBroker({
  settings: {
    whitelist: ['https://trusted.com'],
    blacklist: ['https://malicious.com'],
  },
})

During the handshake each side additionally pins the counterpart's concrete origin; inbound frames from any other origin are dropped and surfaced as invalid.

Layer 2: Security Policy

Custom programmatic validation, applied while handling REQUEST and ACCEPT. A rejected request is answered with DENY_CONNECTION:

broker.setSecurityPolicy((event: MessageEvent) => {
  return event.origin.endsWith('.mycompany.com')
})

Layer 3: Contract Validation

Contracts gate the handshake (structure validation, the required-actions check, and any contractCompat rule) and filter product traffic: inbound messages are validated for envelope shape and dropped when their type is not in the channel's accepted list. Per-action schema fields travel with the contract but nexus does not evaluate them against message payloads: payload-schema enforcement is left to the consuming layer (@hyperfrontend/features validates payloads against the sender's emitted and the receiver's accepted schemas on both ends):

const contract: IChannelContract = {
  emitted: [
    {
      type: 'USER_DATA',
      schema: {
        type: 'object',
        properties: {
          userId: { type: 'string' },
          email: { type: 'string', format: 'email' },
        },
        required: ['userId'],
      },
    },
  ],
  accepted: [{ type: 'ACK' }],
}

Layer 4: Transport Security (Optional)

A sealed session envelope via @hyperfrontend/network-protocol:

ProtocolKey scheduleDefeats
nonePassthrough, no envelopeNothing: trusted environments only
v3Ephemeral P-256 agreement per session, HKDF-SHA256 into two direction-bound AES-GCM-256 keysScripts that can only listen: a passive observer of message events can neither read nor forge frames
v4The v3 agreement with a PBKDF2-SHA256 stretch of the pre-shared key mixed into the key material, once per sessionAny script without the key: without it no frame can be read or accepted, and a key mismatch never confirms

Neither protocol hides the hello (public keys and nonces are public by design), and v3 does not authenticate who the counterpart is: any script that can post to a peer's window with a genuine source can complete a v3 handshake as that peer. The cost is one key agreement plus one HKDF per session (plus one 600k-iteration PBKDF2 for v4), then one AES-GCM operation per message in each direction.

Security Negotiation Flow

Negotiation is registry-sourced: each broker holds a protocol registry, filled via broker.registerProtocol(version, provider) or the settings.security.protocols bag ({ v3?, v4? }), and getSupportedProtocols() lists v4, then v3, then other registered identifiers, then none. A channel opts in with security: { protocol: ... } and from then on accepts that protocol or plaintext and nothing else, on both sides: its REQUEST advertises [protocol, 'none'], and a counterpart naming any other protocol is treated as offering plaintext. The responder picks the first initiator preference its own registry supports (falling back to 'none'), answers it in ACCEPT_CONNECTION, and the initiator confirms the outcome in OPEN_CONNECTION. An ACCEPT must answer the process this side has pending and an OPEN must match the accept this side made; anything else is dropped with an invalid event.

The transport is attached as each side composes its own handshake answer: the responder when it composes ACCEPT (or, for a request that arrived before connect(), when connect() composes it), the initiator when it handles ACCEPT, before OPEN leaves. Once that frame is posted the transport starts: it posts the session hello and retries it every requestRetryMs (500 ms default) until the counterpart confirms. Product traffic sent before the session is keyed waits inside the seal stage, so sends queued before the handshake still leave sealed. Once keyed, each side sends a sealed SECURITY_CONFIRMED control action; the first inbound frame that authenticates (that action or any product frame) confirms the counterpart and fires security-ready { protocol }:

The confirmation deadline is connectTimeoutMs (10 s default), armed when the transport starts. If nothing authenticates in time, the channel fires security-error with code 'security-unconfirmed' and closes silently with close { notify: false, reason: 'security-unconfirmed' }: no CLOSE frame travels, because nothing but the handshake may cross in plaintext once a transport is attached and the session keys were never confirmed. A session whose material cannot key ('invalid-session') ends the same way at once. A responder still awaiting OPEN when its session fails cancels the handshake instead (CANCEL to the counterpart, a local deny with reason: 'security-unavailable'). Every dropped frame in either direction (replays, forgeries, malformed frames, seal failures) is reported through security-error with the wire protocol's code, and a hello that differs from the one keying the session is reported as 'hello-rejected'; the session itself is one-shot and never rekeyed.

Wire frames arrive as Uint8Array payloads. The broker resolves them by source window, enforces the channel's pinned origin, and hands them to the channel's transport: a hello keys the session, anything else is opened and the transported action is dispatched into the same handler map as a plaintext action, with the counterpart window as its source. The plaintext gate guards the other entry: once a channel has a transport, only the six handshake actions may arrive in plaintext, and any other plaintext action (a bypass attempt, or a counterpart that lost its transport) is dropped with an invalid event before any handler sees it. CLOSE, CLOSE_ACK, DESTROY, NEW_MESSAGE and SECURITY_CONFIRMED therefore always travel sealed on a secured channel.

Fail-Open and Fail-Closed Modes

Negotiation fails open by default: when the handshake cannot deliver a sealed transport (the counterpart predates security, offers no common protocol, selects a protocol other than the one asked for, or the negotiated provider is missing), the channel falls back to plaintext with a warning. Setting security: { protocol: ..., mode: 'fail-closed' } refuses that outcome instead: the connection is denied before it opens, with a deny event carrying reason: 'security-unavailable' (the responder denies at REQUEST time, or when connect() finds no working provider for a scheduled answer; the initiator aborts at ACCEPT time via CANCEL plus a local deny; the responder refuses a plaintext OPEN confirmation the same way). A session that negotiates but is never confirmed is a different failure and ends the same way in both modes: the silent close with reason: 'security-unconfirmed' described above.

Security Transport Architecture

SecurityTransport is { send, receive, start, stop, resume, dispose, getProtocol }. createSecurityTransport(config) builds one from a SecurityTransportConfig: the protocol and provider, the counterpart window and pinned-origin accessor, the two endpoint ids and this side's role, helloRetryMs and confirmTimeoutMs (defaulting to the request retry interval and the connect timeout), and the onAction, onError, onConfirmed and onFailed callbacks. For 'none' it returns the passthrough transport, which has no session to start.

Configuration Examples

The registered provider satisfies the SecurityProvider shape: a per-channel wire-pipeline factory plus the protocol instance factory. @hyperfrontend/network-protocol's createChannel and createProtocol exports satisfy it directly (createProtocol(logger) from the v3 entry; createProtocol(logger, sharedKey) from the v4 entry, which throws for a key shorter than 16 characters), and createSecurityTransport plus the SecurityTransport/SecurityProvider types keep the seam public for other implementations. The pre-shared key lives in the v4 provider, not in the channel settings:

import { createChannel as createWireChannel } from '@hyperfrontend/network-protocol/browser/channel'
import { createProtocol as createV4Protocol } from '@hyperfrontend/network-protocol/browser/v4'

// Register a provider at broker level
broker.registerProtocol('v4', {
  createChannel: createWireChannel,
  protocolProvider: createV4Protocol(broker.logger, 'a-pre-shared-key-of-sixteen-or-more'),
})

// Opt a channel into negotiation
const channel = broker.addChannel('secure', targetWindow, {
  security: {
    protocol: 'v4',
    mode: 'fail-closed',
  },
})

// Protocol registry API
broker.hasProtocol('v4') // true
broker.getSupportedProtocols() // ['v4', 'none']
broker.unregisterProtocol('v4') // Remove provider

Internal Dependencies

Hyperfrontend Libraries

Optional Integration


Integration Points

With Other Hyperfrontend Libraries

LibraryIntegration
@hyperfrontend/network-protocolOptional transport security
@hyperfrontend/loggingLogging infrastructure
@hyperfrontend/random-generator-utilsUUID generation
@hyperfrontend/json-utilsJSON Schema validation of protocol shapes
@hyperfrontend/immutable-api-utilsImmutable built-in wrappers
@hyperfrontend/data-utilsType inspection for deep-freeze and guards

With External Systems

  • Micro-frontend frameworks: Module federation, single-spa
  • Web workers: Worker-to-main thread communication
  • Service workers: Offline-capable messaging
  • Electron: Main-renderer process communication

Public API Surface

Exports from @hyperfrontend/nexus

// Core factories
export { createBroker } from './broker/factory'
export { createChannel } from './channel/factory'
export { mergeContracts } from './setup/merge-contracts'
export { createSecurityTransport } from './security/transport/factory'
export { broker as defaultBroker } from './singleton'
export { DEFAULT_CONTRACT } from './constants/default-contract'

// Broker types
export type { BrokerHandle, BrokerConfig, BrokerSettings, BrokerState, SecurityPolicy }

// Channel types
export type { ChannelHandle, ChannelJSON, IChannelSettings, IChannelConfig }

// Contract types
export type { IChannelContract, IActionDescription }
export type { ContractCompat, ContractCompatibility, ContractCompatible, ContractIncompatible }

// Message types
export type { IMessage, MessageEnvelope }

// Event types
export type { ChannelEvent, EventData, OpenEventData, CloseEventData, CloseReason, CancelEventData, DenyEventData, DenyReason }
export type { InvalidEventData, SecurityReadyEventData, SecurityErrorEventData }
export type { OpenEventHandler, CloseEventHandler, CancelEventHandler, DenyEventHandler, InvalidEventHandler }

// Action types
export type { IAction, ActionType }

// Security types: the negotiation slots, the transport seam, and the wire-pipeline mirrors a provider implements
export type { SecurityProtocolVersion, SecurityNegotiationRequest, SecurityNegotiationResponse, SecurityConfirmation }
export type { SecurityErrorCode, SecurityTransportError, SecurityProvider, SecurityTransport, SecurityTransportConfig }
export type { SecurityPacket, SecurityPacketData, SecurityPacketDrop, SecuritySendPacket, SecurityReceivePacket }
export type { SecuritySession, SecuritySessionRole, SecurityHelloOutcome, SecurityWireProtocol, SecurityProtocolProvider }
export type { SecurityWireChannel, SecurityChannelOptions, SecurityChannelFactory }
export type { SecurityProtocolProviders, BrokerSecurityConfig, ChannelSecuritySettings }

// Filter utilities
export { openFilter, closeFilter, cancelFilter, denyFilter, invalidFilter, createEventFilter }
export { byType, compose, createMessageFilter }
export type { MessageFilter, MessagePredicate, MessageHandler, EventHandler }

// Logging
export { createLogger, logAction, logEvent }
export type { Logger, LogLevel, NexusLoggerOptions }

Links