6.0 KiB
OpenSpec Change: enterprise-network-protocol
- Change ID: enterprise-network-protocol
- Package:
@suaveplan/network-protocol - Category: universal/network
- Target path:
universal/network/network-protocol - Status: SUPERSEDED-BY-GENESIS-WAVE
- Date: 2026-04-24
Summary
Deliver @suaveplan/network-protocol on the genesis branch from scratch at enterprise quality. The package provides custom application-level protocol definitions: message framing (length-prefix, delimiter, fixed-length), codec registry (encode/decode for message types), protocol versioning and negotiation, session handshake lifecycle, and flow-control primitives. Genesis adds structured errors via @suaveplan/error, Zod schemas for all protocol types, OTel instrumentation, AbortSignal on handshake and negotiation, injectable Clock for session timing, and property-based tests for codec round-trip invariants.
Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/error (workspace:^) |
Base class for NetworkProtocolError; stable error codes |
@suaveplan/types (workspace:^) |
LoggerLike, Clock, EventBusLike |
@suaveplan/events (workspace:^) |
Event bus for session lifecycle events |
@suaveplan/logger (workspace:^) |
Structured logging |
@suaveplan/serialize (workspace:^) |
MessagePack binary serialization |
@suaveplan/crypto-utils (workspace:^) |
Byte utilities (concat, i2osp, os2ip) for framing |
zod (catalog:forms) |
Zod schemas |
@opentelemetry/api (catalog:observability) |
Spans + counters on encode/decode |
tslib (catalog:build) |
Runtime helpers |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).
Public API surface
Core
function defineProtocol(definition: ProtocolDefinition): Protocol
interface Protocol {
readonly name: string
readonly version: number
createSession(options?: SessionOptions): ProtocolSession
negotiate(offered: ProtocolOffer, signal?: AbortSignal): Promise<ProtocolNegotiationResult>
}
interface ProtocolSession {
readonly id: string
readonly state: SessionState
encode(message: ProtocolMessage): Uint8Array
decode(data: Uint8Array): ProtocolMessage
handshake(signal?: AbortSignal): Promise<void>
close(): void
on(event: SessionEvent, handler: (data: unknown) => void): () => void
}
Protocol definition
interface ProtocolDefinition {
name: string
version: number
framing: FramingStrategy
codecs: ReadonlyArray<MessageCodec<unknown>>
handshake?: HandshakeDefinition
maxMessageSize?: number
}
type FramingStrategy =
| { type: 'length-prefix'; lengthBytes: 2 | 4; bigEndian?: boolean }
| { type: 'delimiter'; delimiter: Uint8Array }
| { type: 'fixed'; size: number }
interface MessageCodec<T> {
readonly type: string
readonly version?: number
encode(message: T): Uint8Array
decode(data: Uint8Array): T
schema?: { parse: (data: unknown) => T }
}
interface HandshakeDefinition {
timeout?: number
clientHello(session: ProtocolSession): Uint8Array
serverHello(clientData: Uint8Array): Uint8Array
verify?(data: Uint8Array): boolean
}
Negotiation
interface ProtocolOffer {
name: string
versions: number[]
capabilities?: string[]
}
interface ProtocolNegotiationResult {
readonly agreed: boolean
readonly version: number
readonly capabilities: string[]
readonly reason?: string
}
Types
interface ProtocolMessage {
type: string
version?: number
payload: unknown
id?: string
sessionId?: string
}
type SessionState = 'idle' | 'handshaking' | 'established' | 'closing' | 'closed'
type SessionEvent = 'established' | 'message' | 'close' | 'error'
interface SessionOptions {
logger?: LoggerLike
clock?: Clock
eventBus?: EventBusLike
}
Error codes
type NetworkProtocolErrorCode =
| 'PROTO_FRAMING_ERROR'
| 'PROTO_CODEC_NOT_FOUND'
| 'PROTO_DECODE_FAILED'
| 'PROTO_ENCODE_FAILED'
| 'PROTO_HANDSHAKE_FAILED'
| 'PROTO_VERSION_MISMATCH'
| 'PROTO_MESSAGE_TOO_LARGE'
| 'PROTO_ABORT'
function registerNetworkProtocolErrorCodes(): void
Genesis additions
- Structured
NetworkProtocolErrorwith stablePROTO.*codes - Zod schemas for
ProtocolDefinition,ProtocolOffer,ProtocolMessage - Three framing strategies with length-prefix and delimiter implementations using
@suaveplan/crypto-utilsbyte helpers - Codec registry — look up codec by
message.type; throwPROTO_CODEC_NOT_FOUNDif missing - Protocol negotiation — version intersection + capability set intersection
- Message size guard — throw
PROTO_MESSAGE_TOO_LARGEbefore encoding - OTel instrumentation — counter
proto.messages_encoded_total,proto.messages_decoded_total, histogramproto.message_size_bytes - Property-based tests — encode/decode round-trip for arbitrary message payloads; framing idempotency
File layout
universal/network/network-protocol/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
types/
common.ts
common.md
core/
protocol/
protocol.ts
protocol.md
protocol.types.ts
protocol.test.ts
index.ts
session/
session.ts
session.md
session.test.ts
index.ts
framing/
length-prefix.ts
length-prefix.md
length-prefix.test.ts
delimiter.ts
delimiter.md
delimiter.test.ts
fixed.ts
fixed.md
fixed.test.ts
codecs/
codec-registry.ts
codec-registry.md
codec-registry.test.ts
index.ts
tests/
integration.test.ts
property.test.ts
Impact
Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/logger, @suaveplan/serialize, @suaveplan/crypto-utils must be published.