Initial Commit
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
# 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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
interface ProtocolOffer {
|
||||
name: string
|
||||
versions: number[]
|
||||
capabilities?: string[]
|
||||
}
|
||||
|
||||
interface ProtocolNegotiationResult {
|
||||
readonly agreed: boolean
|
||||
readonly version: number
|
||||
readonly capabilities: string[]
|
||||
readonly reason?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
1. **Structured `NetworkProtocolError`** with stable `PROTO.*` codes
|
||||
2. **Zod schemas** for `ProtocolDefinition`, `ProtocolOffer`, `ProtocolMessage`
|
||||
3. **Three framing strategies** with length-prefix and delimiter implementations using `@suaveplan/crypto-utils` byte helpers
|
||||
4. **Codec registry** — look up codec by `message.type`; throw `PROTO_CODEC_NOT_FOUND` if missing
|
||||
5. **Protocol negotiation** — version intersection + capability set intersection
|
||||
6. **Message size guard** — throw `PROTO_MESSAGE_TOO_LARGE` before encoding
|
||||
7. **OTel instrumentation** — counter `proto.messages_encoded_total`, `proto.messages_decoded_total`, histogram `proto.message_size_bytes`
|
||||
8. **Property-based tests** — encode/decode round-trip for arbitrary message payloads; framing idempotency
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
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.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Tasks: enterprise-network-protocol
|
||||
|
||||
**Change ID**: enterprise-network-protocol
|
||||
**Package**: `@suaveplan/network-protocol`
|
||||
**Target path**: `universal/network/network-protocol`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/serialize`, `@suaveplan/crypto-utils`
|
||||
|
||||
---
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] Create `universal/network/network-protocol/` with `src/`, `tests/`, and all feature subdirectories.
|
||||
- [x] Write `package.json` at version `0.1.0`: `"sideEffects": false`, exports map, deps per `proposal.md`.
|
||||
- [x] Write `tsconfig.json`, `vitest.config.ts`, scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types and error codes
|
||||
|
||||
- [x] Define all public types in `src/types/common.ts`: `ProtocolDefinition`, `FramingStrategy`, `MessageCodec`, `HandshakeDefinition`, `ProtocolMessage`, `ProtocolOffer`, `ProtocolNegotiationResult`, `SessionState`, `SessionEvent`, `SessionOptions`.
|
||||
- [x] Write `src/types/common.md` (≥200 words, simple + advanced examples).
|
||||
- [x] Define error codes in `src/error-codes.ts`.
|
||||
- [x] Write Zod schemas in `src/types/schemas.ts` + `src/types/schemas.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Framing implementations (`src/framing/`)
|
||||
|
||||
### 3a. Length-prefix framing
|
||||
|
||||
- [x] Implement `encodeLengthPrefix(data, opts)` — prepend 2 or 4-byte length header (big-endian or little-endian) using `@suaveplan/crypto-utils` `i2osp`.
|
||||
- [x] Implement `decodeLengthPrefix(buffer, opts)` — parse header, extract payload bytes.
|
||||
- [x] Write `length-prefix.test.ts` + `length-prefix.md`.
|
||||
|
||||
### 3b. Delimiter framing
|
||||
|
||||
- [x] Implement `encodeDelimiter(data, delimiter)` — append delimiter to message bytes.
|
||||
- [x] Implement `decodeDelimiter(buffer, delimiter)` — split on delimiter bytes.
|
||||
- [x] Write `delimiter.test.ts` + `delimiter.md`.
|
||||
|
||||
### 3c. Fixed-length framing
|
||||
|
||||
- [x] Implement `encodeFixed(data, size)` — pad or throw if `data.length !== size`.
|
||||
- [x] Implement `decodeFixed(buffer, size)` — slice `size` bytes.
|
||||
- [x] Write `fixed.test.ts` + `fixed.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Codec registry (`src/codecs/`)
|
||||
|
||||
- [x] Implement `createCodecRegistry()` — stores `Map<string, MessageCodec<unknown>>`.
|
||||
- [x] Implement `register(codec)` / `get(type)` / `encode(message)` / `decode(type, data)`.
|
||||
- [x] `encode` throws `NetworkProtocolError(PROTO_CODEC_NOT_FOUND)` if codec missing.
|
||||
- [x] Write `codec-registry.test.ts` + `codec-registry.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Protocol session (`src/core/session/`)
|
||||
|
||||
- [x] Implement `ProtocolSession` class:
|
||||
- Stores `framing`, `codecs` from parent protocol.
|
||||
- `encode(message)`: check `maxMessageSize`; look up codec; apply framing.
|
||||
- `decode(data)`: strip framing; look up codec by message type header.
|
||||
- `handshake(signal?)`: call `definition.handshake.clientHello()`, await response, call `verify()`.
|
||||
- Emit lifecycle events via event bus.
|
||||
- [x] Write `session.test.ts` (≤500 lines): encode/decode round-trip, handshake, too-large message throws.
|
||||
- [x] Write `session.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 6. Protocol factory (`src/core/protocol/`)
|
||||
|
||||
- [x] Implement `defineProtocol(definition)`:
|
||||
- Validates definition with Zod.
|
||||
- Returns `Protocol` object.
|
||||
- [x] Implement `protocol.createSession(options?)` — creates `ProtocolSession`, returns it.
|
||||
- [x] Implement `protocol.negotiate(offered, signal?)`:
|
||||
- Compute version intersection; take highest common version.
|
||||
- Compute capability intersection.
|
||||
- Return `ProtocolNegotiationResult`.
|
||||
- [x] Write `protocol.test.ts` + `protocol.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 7. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
- [x] Encode/decode round-trip: for arbitrary `ProtocolMessage` with registered codec, `decode(encode(m))` deep-equals `m`.
|
||||
- [x] Framing round-trip: `decode(encode(payload))` equals payload for all three strategies.
|
||||
- [x] Negotiation symmetry: `negotiate(A, B)` produces same version as `negotiate(B, A)`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Docs and verify
|
||||
|
||||
- [x] All per-file `.md` files per layout in `proposal.md`.
|
||||
- [x] `universal/network/network-protocol/README.md`.
|
||||
- [x] `bunx turbo lint typecheck test test:coverage --filter=@suaveplan/network-protocol` — all green, 100%.
|
||||
- [x] `bun scripts/publish.ts --package @suaveplan/network-protocol`.
|
||||
- [x] Update `CLAUDE.md`. Archive change folder.
|
||||
Reference in New Issue
Block a user