@hyperfrontend/network-protocol/node/v3v3
Node.js-side v3 protocol: a session-keyed envelope with no shared secret, wired to the Node.js crypto module.
Overview
createProtocol(logger) returns a ProtocolProvider. Bound to a session by createChannel, each instance mints a random 32-byte nonce and an ephemeral P-256 key pair and advertises them in a 99-byte hello frame (channel.hello()); the peer's hello goes to channel.acceptHello(frame), after which two AES-GCM-256 keys, one per direction, are derived from the agreement with HKDF-SHA256 under info strings that name the protocol and both identities. The first hello keys the session, a byte-for-byte repeat of it is a duplicate, anything else is rejected, and a live session is never rekeyed.
Every sealed frame carries its counter in the clear as the nonce and authenticates its ten-byte header; a frame whose counter is not above the last accepted one is rejected before decryption, and a frame from any other session fails to authenticate.
V3 is the protocol's definition, { id: 'v3', version: 3 }: the identifier a session names and the byte every frame starts with.
What v3 protects against
A party that can only listen to the hello exchange and the traffic cannot read or forge frames. A party that can post its own hello before the peer's arrives can complete a v3 handshake as that peer, because nothing authenticates who said hello. Choose /node/v4 when the counterpart must be authenticated. Neither protocol hides the hello: nonces and public keys are public by design.
Usage
import { parentPort } from 'node:worker_threads'
import { createProtocol } from '@hyperfrontend/network-protocol/node/v3'
import { createChannel } from '@hyperfrontend/network-protocol/node/channel'
import { createLogger } from '@hyperfrontend/logging'
const channel = createChannel('main-to-worker', {
send: (frame) => parentPort.postMessage(frame, [frame.buffer]),
receive: (packet) => handle(packet.data.message),
protocolProvider: createProtocol(createLogger({ level: 'info' })),
session: { protocol: 'v3', role: 'responder', localId, peerId },
})
parentPort.postMessage(await channel.hello())
parentPort.on('message', (frame: Uint8Array) => (channel.isHello(frame) ? channel.acceptHello(frame) : channel.receive(frame)))
Notes
- Session setup costs one ECDH agreement plus one HKDF expansion per direction; each frame costs one AES-GCM operation. Keys are derived once, on the first seal or open after the peer's hello is accepted, and product traffic sent before then waits inside the seal stage.
- A rejected frame reaches
onDropwith aProtocolErrorascause;getProtocolErrorCode(drop.cause)from/securitynames the reason (unsupported-version,replayed,authentication-failed,malformed,counter-exhausted, orinvalid-session). - The browser counterpart lives at
/browser/v3and produces identical frames.
API Reference§
ƒ Functions
Returns
ProtocolProviderStoreExample
Creating and using a protocol provider store
const store = createProtocolProviderStore()
store.add('websocket', myProtocolProvider)
const provider = store.getByName('websocket')v3 keys each session from an ephemeral key agreement alone: a party that only listens to the hello exchange and the traffic cannot read it, while a party that can post its own hello to a window before the peer's arrives can stand in for the peer. It authenticates frames, rejects replays, and binds traffic to one session, but does not authenticate the peer.
Parameters
| Name | Type | Description |
|---|---|---|
§crypto | SessionCrypto | The platform primitives |
Returns
(logger: Logger) => ProtocolProvidercreateProtocol(logger), which returns the provider a channel binds to a sessionExample
Composing v3 in a browser entry
export const createProtocol = createV3ProtocolFactory(crypto)
const protocolProvider = createProtocol(logger)Parameters
| Name | Type | Description |
|---|---|---|
§name | string | The name to validate |
Returns
booleanExample
Validating protocol names
isValidName('websocket')
// => true
isValidName('')
// => falseParameters
| Name | Type | Description |
|---|---|---|
§protocol | unknown | The protocol object to validate |
Returns
ValidProtocolResultExample
Validating a protocol object
const result = isValidProtocol(myProtocol)
// => { seal: true, open: true, hello: true, isHello: true, acceptHello: true, send: true, receive: true, getLogger: true }
const invalid = isValidProtocol({})
// => { seal: false, open: undefined, ... }Parameters
| Name | Type | Description |
|---|---|---|
§protocolProvider | unknown | The value to validate as a protocol provider |
Returns
booleanExample
Validating a protocol provider function
isValidProtocolProvider(() => protocol)
// => true
isValidProtocolProvider('not-a-function')
// => falseParameters
| Name | Type | Description |
|---|---|---|
§receive | unknown | The value to validate as a receive function |
Returns
booleanExample
Validating a receive function
isValidReceiveFn((packet) => console.log(packet))
// => true
isValidReceiveFn(null)
// => falseParameters
| Name | Type | Description |
|---|---|---|
§send | unknown | The value to validate as a send function |
Returns
booleanExample
Validating a send function
isValidSendFn((packet) => websocket.send(packet))
// => true
isValidSendFn('not-a-function')
// => false◈ Interfaces
Properties
Properties
readonly add:(name: string, protocolProvider: ProtocolProvider<T>) => voidProperties
readonly expandKey:(ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, usages: unknown) => Promise<CryptoKey>readonly open:(key: CryptoKey, nonce: Uint8Array, additionalData: Uint8Array, sealed: Uint8Array) => Promise<Uint8Array<ArrayBufferLike>>readonly seal:(key: CryptoKey, nonce: Uint8Array, additionalData: Uint8Array, plaintext: Uint8Array) => Promise<Uint8Array<ArrayBufferLike>>readonly stretchPassword:(password: string, salt: Uint8Array, options?: StretchOptions) => Promise<Uint8Array<ArrayBufferLike>>◆ Types
type ValidProtocolResult = mapped