Specification

Esiur Protocol (EP) Specification

πŸ“„ Version 1.0 β€” 2026-07-22 πŸ”Œ TCP / WebSocket πŸ”’ Default port: 10518 πŸ”— URI scheme: ep:// πŸ“œ MIT License
Every claim in this document is verified directly against the reference implementations β€” esiur-dotnet and esiur-ts β€” including cross-checked byte-level test vectors shared between both codebases. Where the two implementations currently disagree in behavior, it's called out explicitly in Β§8 rather than papered over.

1. Overview

The Esiur Protocol (EP) is a compact binary application-layer protocol for distributed object computing. It enables first-class sharing of object instances across a TCP or WebSocket connection: property values synchronize in real time, remote methods are called as if local, and events propagate transparently to every subscriber.

EP is fully self-describing: a client can discover the type schema of any resource at runtime (a TypeDef query) and generate strongly-typed proxies without out-of-band API documents. Actively maintained implementations exist for .NET and TypeScript, sharing wire concepts, packet layout, and command names byte-for-byte.

Key design properties:

2. Transport & URI scheme

TCP

EP runs natively over TCP. EpServer defaults to TCP port 10518 in both reference implementations; a bare ep://host URL (no explicit port) resolves to the same default. The server accepts incoming connections and immediately begins the Authentication Phase (Β§4).

WebSocket

EP also runs as a WebSocket sub-protocol, identifier EP (uppercase, compared case-sensitively/ordinally by both implementations). After the WebSocket handshake, the binary framing is identical to the TCP wire format: each WebSocket message carries exactly one EP packet.

URI scheme

Resources are addressed with an ep:// URL:

ep://<host>[:port]/<store>/<resource-path>

Examples
  ep://server.example.com/sys/thermostat
  ep://localhost:10518/sys/counter
  ep://iot-hub.local/sensors/temperature

The path maps directly to a resource in the server's store hierarchy, analogous to a filesystem path. Omitting the port uses 10518. A bare ep://host[:port] with no path resolves to the connection itself.

eps:// is transport-specific today, not universal. Over WebSocket, the TypeScript client maps eps:// to wss:// (TLS-protected WebSocket) β€” this is implemented and working. The .NET client's URL resolver does not currently register a distinct handler for an eps/EPS scheme, and neither implementation currently wraps raw TCP connections in TLS automatically. Confidentiality on a raw TCP connection today relies entirely on the application-layer session encryption negotiated during the auth handshake (Β§4.3), not on transport-layer TLS. See Β§8 and Β§9.

3. Connection lifecycle

Client                                        Server
  β”‚                                               β”‚
  │────────── TCP / WebSocket connect ───────────▢│
  β”‚                                               β”‚
  │◀─────── Auth: Initialize ─────────────────────│  server sends first
  β”‚                                               β”‚
  │──────── Auth: Action ─────────────────────────▢  zero or more rounds,
  │◀─────── Auth: Acknowledge ────────────────────│  protocol-dependent
  β”‚                                               β”‚
  │◀─────── Auth: SessionEstablished ─────────────│
  β”‚                                               β”‚
  │◀════════════ EP session packets ═════════════▢│
  β”‚   Request / Reply / Notification              β”‚
  β”‚                                               β”‚
  │────────── connection close ──────────────────▢│

Once the session is established, the connection is fully bidirectional β€” either peer may send Requests, Replies, or Notifications at any time. A resource attachment stays live until the connection closes or the client explicitly detaches; on reconnect, a client can reattach and resynchronize rather than re-fetch full state from scratch.

4. Authentication phase

Before any EP session packet is transmitted, both sides exchange one or more auth packets, a distinct binary format from session packets. The server always sends the first packet (Initialize). Even a connection configured for anonymous access completes this phase β€” it just negotiates AuthenticationMode.None with EncryptionMode.None in a single round trip.

4.1 Auth packet header byte

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
Command2 bits hasTDU1 bit reserved1 bit AuthMode2 bits β€” Initialize only EncMode2 bits β€” Initialize only

For every command other than Initialize, bits 4–0 (with bit 5 masked off) are instead read as a single EpAuthPacketMethod byte (Β§4.3) β€” the low 5 bits given above only apply to the very first, server-sent Initialize packet. hasTDU (bit 5) β€” when set, a TDU payload (Β§5) follows, encoding a key–value map of handshake parameters (Β§4.4).

4.2 Auth commands (bits 7–6)

ValueNameDirectionDescription
0x0InitializeServer β†’ ClientOpens authentication; announces supported modes and algorithms
0x1AcknowledgeBothAccept, deny, or redirect an authentication step
0x2ActionClient β†’ ServerA handshake step β€” credentials or key material
0x3EventServer β†’ ClientSession established, an error, or an indication

AuthenticationMode (bits 3–2 of an Initialize packet):

ValueNameDescription
0x0NoneNo identity required from either party
0x1InitializerIdentityThe connecting client must authenticate
0x2ResponderIdentityThe server must present its identity to the client
0x3DualIdentityMutual authentication β€” both parties must prove identity

EncryptionMode (bits 1–0 of an Initialize packet):

ValueNameDescription
0x0NoneSession data transmitted without application-layer encryption
0x1EncryptWithSessionKeySymmetric session key (AES-256-GCM) derived during the handshake
0x2EncryptWithSessionKeyAndAddressSession key additionally bound to the connection's source address

4.3 EpAuthPacketMethod (Acknowledge / Action / Event byte)

For any command other than Initialize, the byte identifies the specific handshake step:

ValueNameCommandDescription
0x40DeniedAcknowledgeAuth denied β€” terminate connection
0x41NotSupportedAcknowledgeRequested method not supported β€” terminate
0x42TrySupportedAcknowledgeTry an alternative, supported method
0x43RetryAcknowledgeRetry with different parameters
0x44ProceedToHandshakeAcknowledgeProceed to the key-exchange step
0x45ProceedToFinalHandshakeAcknowledgeProceed to the final handshake step
0x46ProceedToEstablishSessionAcknowledgeDerive session keys
0x47SessionEstablishedAcknowledgeSession ready β€” switch to EP session packets
0x80HandshakeActionClient sends key material / credentials
0x81FinalHandshakeActionClient sends final confirmation
0x82KeyRotationActionRequest mid-session encryption-key rotation (.NET only β€” see Β§8)
0xC0EstablishedEventAuth flow complete
0xC1ErrorTerminateEventFatal auth error β€” connection will close
0xC2ErrorMustEncryptEventEncryption is required for this resource
0xC3ErrorRetryEventTransient error β€” client may retry
0xC8IndicationEstablishedEventInformational: (re-)established indication
0xC9KeyRotationEstablishedEventKey rotation completed (.NET only β€” see Β§8)
0xD0IAuthPlainEventIdentity assertion, unhashed
0xD1IAuthHashedEventIdentity assertion, hashed
0xD2IAuthEncryptedEventIdentity assertion, encrypted

4.4 Auth TDU header keys

The TDU payload of an auth packet is a keyed map. Integer keys 0–17 are defined:

#Key#Key
0Version9SoftwareIdentity
1Domain10Referrer
2SupportedAuthentications11Time
3SupportedHashAlgorithms12IPAddress
4SupportedCiphers13Identity
5SupportedCompression14AuthenticationProtocol
6SupportedMultiFactorAuthentications15AuthenticationData
7CipherType16ErrorMessage
8CipherKey17CipherNonce

5. TDU (Transmission Data Unit) encoding

Every value carried inside a session or auth packet β€” arguments, return values, property contents, handshake parameters β€” is encoded as a self-describing TDU. Decoders never need out-of-band schema information: each TDU carries its own type tag and, for variable-length types, its own length.

5.1 Identifier byte

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
Class2 bits Fixed: exponent / Var-length: size-field count3 bits Sub-type3 bits
Bits 7–6ClassBits 5–3 meaning
00FixedExponent β€” payload width = 0 if exponent is 0, else 1 << (exponentβˆ’1) bytes.
01DynamicLength-field byte count (0–7, see Β§5.3) β€” how many big-endian length bytes follow the identifier.
10TypedSame length-field encoding as Dynamic. Carries a type reference (record/enum/tuple).
11ExtensionSame length-field encoding as Dynamic. Protocol-level markers.

For Dynamic, Typed, and Extension classes, the identifier recovered by a parser is headerByte & 0xC7 (class bits + sub-type bits only β€” the length-field-count bits are stripped once the length has been read). The length bytes themselves are big-endian (most-significant byte first):

Bits 5–3OR-maskLength-field sizeMax payload
0000x00none β€” zero-length value0 B
0010x081 byte255 B
0100x102 bytes64 KB
0110x183 bytes16 MB
1000x204 bytes4 GB
101–1110x28–0x385–7 bytesup to 72 PB

5.2 Fixed class (0x00–0x2F)

WidthIdentifiers
0 bytes0x00 Null, 0x01 False, 0x02 True, 0x03 NotModified, 0x04 Infinity
1 byte0x08 UInt8, 0x09 Int8, 0x0A Char8, 0x0B LocalResource8, 0x0C RemoteResource8, 0x0D LocalProcedure8, 0x0E RemoteProcedure8
2 bytes0x10 UInt16, 0x11 Int16, 0x12 Char16, 0x13 LocalResource16, 0x14 RemoteResource16, 0x15 LocalProcedure16, 0x16 RemoteProcedure16
4 bytes0x18 UInt32, 0x19 Int32, 0x1A Float32, 0x1B LocalResource32, 0x1C RemoteResource32, 0x1D LocalProcedure32, 0x1E RemoteProcedure32
8 bytes0x20 UInt64, 0x21 Int64, 0x22 Float64, 0x23 DateTime (ticks, signed 64-bit)
16 bytes0x28 UInt128, 0x29 Int128, 0x2A Decimal128, 0x2B UUID (RFC 4122, 16 raw bytes)

Multi-byte Fixed-class payload contents (integers, floats, DateTime ticks) are little-endian β€” see Β§5.5 for verified examples. 128-bit integers are two little-endian 64-bit halves, low word first.

5.3 Dynamic class (0x40–0x47)

Base IDNameDescription
0x40RawDataOpaque byte array
0x41StringUTF-8 text
0x42ListOrdered list of heterogeneous TDUs
0x43ResourceListList of resource references
0x44RecordListList of typed records
0x45ResourceLinkResource path, as a string
0x46MapKey–value map
0x47MapListOrdered list of maps

5.4 Typed & Extension classes

IdentifierClassNameDescription
0x80TypedTypedRecord/enum/tuple value; embeds a type-reference (TRU) block before its content
0x81TypedTypeDefA full type definition, returned in answer to a TypeDef query
0x82TypedTRUA standalone type-reference unit
0xC0ExtensionTypeContinuationMulti-part type reference β€” continuation fragment
0xC1ExtensionTypeOfTargetUse the enclosing resource's own type as the type reference

5.5 Verified wire examples

Each of these is a complete TDU (identifier byte, any length bytes, payload), taken from byte-for-byte matching test vectors shared between the .NET and TypeScript test suites:

49 02 48 69 β€” String "Hi": id 0x41|0x08 (1-byte length), length 2, UTF-8 "Hi" 19 40 9c 00 00 β€” Int32 40000: id 0x19, payload 40 9c 00 00 (little-endian) 21 00 f2 05 2a 01 00 00 00 β€” Int64 5,000,000,000: id 0x21, 8-byte little-endian payload

The Int32/Int64 examples above intentionally show a multi-byte payload: note the least-significant byte comes first. This is the opposite byte order from the TDU length-prefix field (Β§5.1), which is big-endian β€” the two fields are independent and use different conventions.

6. EP packet format

After the authentication phase completes, all traffic uses EP session packets. Every packet begins with a single header byte; Request and Reply packets additionally carry a 4-byte callback ID. Payloads, when present, are TDU-encoded (Β§5).

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
Method2 bits hasTDU1 bit Command5 bits
Bits 7–6MethodCallback ID?Notes
00NotificationNo β€” 1-byte minimum packetUnsolicited; no reply expected
01RequestYes β€” 4-byte little-endian uint325-byte minimum packet
10ReplyYes β€” echoes the Request's callback ID5-byte minimum packet
11ExtensionNoReserved β€” parsed by both implementations, emitted by neither (see Β§8)
Notification (1+ bytes) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 00 hasTDU cmd:5 β”‚ β”‚ TDU (if set) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Request / Reply (5+ bytes) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 01/10 hasTDU cmd:5 β”‚ β”‚ callback id, u32 LE β”‚ β”‚ TDU (if set) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Verified example β€” a KeepAlive request (no payload) with callback ID 12:

58 0c 00 00 00 58 = Request(01)<<6 | hasTDU(0)<<5 | KeepAlive(0x18) 0c 00 00 00 = callback id 12, little-endian

7. Commands reference

Request commands

ValueNameGroupDescription
0x00InvokeFunctionInvokeCall a method on a resource instance by index
0x01SetPropertyInvokeSet a resource property by index
0x02SubscribeInvokeSubscribe to property/event change notifications
0x03UnsubscribeInvokeCancel a subscription
0x08TypeDefIdsByNamesInquireResolve numeric type IDs from class name strings
0x09TypeDefByIdInquireFetch a full type definition by numeric ID
0x0ATypeDefByResourceIdInquireFetch the type definition for an attached resource
0x0BQueryInquireSearch resources by path pattern
0x0CLinkTypeDefsInquireGet all type definitions reachable from a resource link
0x0DTokenInquireRequest an authentication token
0x0EGetResourceIdByLinkInquireResolve a resource path to its server-assigned numeric ID
0x10AttachResourceManageAttach to a resource; server returns its full current state
0x11ReattachResourceManageRe-sync after reconnection
0x12DetachResourceManageStop receiving updates for this resource
0x13CreateResourceManageInstantiate a new resource on the server
0x14DeleteResourceManageDestroy a server-side resource
0x15MoveResourceManageRename or relocate a resource
0x18KeepAliveStaticConnection heartbeat β€” no payload
0x19ProcedureCallStaticCall a named server-side procedure
0x1AStaticCallStaticCall a static/class-level method β€” no instance required
0x1BIndirectCallStaticIndirect invocation via a callable reference. Declared, not yet dispatched by the reference server β€” see Β§8.
0x1CPullStreamStaticPull the next chunk from a server-side stream
0x1DTerminateExecutionStaticAbort an in-progress execution
0x1EHaltExecutionStaticPause an in-progress (pausable) execution
0x1FResumeExecutionStaticResume a halted execution

Reply commands carry the Request's callback ID

ValueNameGroupDescription
0x00CompletedSuccessOperation completed; payload is the return value, if any
0x01PropagatedSuccessResult delivered to a downstream subscriber
0x02StreamSuccessOperation returned a stream handle
0x04PermissionErrorErrorRequest denied β€” insufficient permissions
0x05ExecutionErrorErrorException thrown during method execution
0x08ProgressPartialIntermediate progress update; request still pending
0x09ChunkPartialOne chunk of a streamed response; more may follow
0x0AWarningPartialNon-fatal warning; operation continues

Notification commands no callback ID β€” unsolicited

ValueNameGroupDescription
0x00PropertyModifiedInvokeA subscribed property value changed
0x01EventOccurredInvokeA subscribed event was raised
0x08ResourceDestroyedManageAn attached resource was deleted on the server
0x09ResourceReassignedManageAn attached resource was assigned a new server ID
0x0AResourceMovedManageAn attached resource was renamed or relocated
0x0BSystemFailureManageSystem-level failure affecting the connection

8. Implementation notes

EP is specified so that independent implementations interoperate over the wire without a translation layer, and the two reference implementations agree on every packet, command, and TDU value covered above. As of this writing, a few areas are implemented in one reference implementation but not (yet) the other:

Area.NETTypeScript
WebSocket subprotocol enforcement on acceptRequired β€” rejects the upgrade with HTTP 400 if the client didn't request EPNot currently validated β€” EpServer accepts the upgrade regardless of the requested subprotocol
eps:// URI schemeNot currently registered as a distinct handlerMaps to wss:// (TLS-protected WebSocket)
Mid-session key rotation (KeyRotation / KeyRotationEstablished, Β§4.3)Implemented, used by the PPAP authentication providerNot yet implemented
Untrusted-input TDU length guardsConfigurable maximum payload/allocation limits (WarehouseConfiguration)No equivalent limit yet at the TDU-parsing layer
EpPacketMethod.Extension (session packets, Β§6) and IndirectCall (Β§7)Parsed if received; not currently emitted or dispatchedNot currently emitted

None of this affects wire compatibility for the features both implementations do support today β€” it's listed here so implementers building a third party don't assume behavior that isn't universal yet.

9. Security considerations

AuthenticationMode.None deployments should be restricted to trusted, private networks. Combined with EncryptionMode.None, a connection has no identity verification and no application-layer confidentiality at all.

10. References