@hyperfrontend/network-protocol/node/v4

v4

Node.js-side v4 protocol: the session-keyed envelope with a shared secret mixed in, wired to the Node.js crypto module.

Overview

createProtocol(logger, sharedKey) returns a ProtocolProvider used exactly like /node/v3's: the same hello exchange, the same frames, the same replay rule. The difference is the key schedule: the shared key is stretched once per session with PBKDF2-SHA256 (600,000 iterations, salted with both hellos' nonces) and appended to the ECDH shared secret before the HKDF-SHA256 expansion, so the session is bound to the key. Without it a party can neither read frames nor produce frames the counterpart accepts, a key mismatch is detected because no frame ever authenticates, and a key that leaks later does not expose earlier sessions, whose ephemeral agreements are gone.

V4 is the protocol's definition, { id: 'v4', version: 4 }: the identifier a session names and the byte every frame starts with.

The shared key

The key must be a string of at least MIN_SHARED_KEY_LENGTH (16) characters; isValidSharedKey(value) checks exactly that, and createProtocol throws for a shorter key. The guarantee holds for a generated key of 128 bits or more (for example 32 hex characters from a secure random source). A party that can run a hello exchange against this side can test key guesses offline afterwards, so a human-chosen passphrase is not a substitute.

Usage

import { parentPort } from 'node:worker_threads'
import { createProtocol } from '@hyperfrontend/network-protocol/node/v4'
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' }), sharedKey),
  session: { protocol: 'v4', 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, one PBKDF2-SHA256 stretch, and one HKDF expansion per direction; each frame costs one AES-GCM operation. The stretch is paid once per session, not per message, so it lands on the handshake rather than on traffic.
  • A side holding a different key derives different keys: every frame from it is reported through onDrop with code authentication-failed and nothing is delivered.
  • The browser counterpart lives at /browser/v4 and produces identical frames.

API Reference§

ƒ Functions

§function

createProtocolProviderStore(): ProtocolProviderStore

Creates a store for managing protocol provider registrations. Provides methods to register, retrieve, and list protocol providers.

Returns

ProtocolProviderStore
A ProtocolProviderStore with methods for managing protocol providers

Example

Creating and using a protocol provider store

const store = createProtocolProviderStore()
store.add('websocket', myProtocolProvider)
const provider = store.getByName('websocket')
§function

createV4ProtocolFactory(crypto: SessionCrypto): (logger: Logger, sharedKey: string) => ProtocolProvider

Creates the v4 protocol factory for a platform.
v4 keys each session from the same ephemeral key agreement as v3 with the shared key stretched once and mixed in, so a party without the key cannot complete the agreement: product traffic is confidential and authentic against anyone who lacks the key, and a key that leaks later does not expose earlier sessions. A party that can run a hello exchange against this side can test key guesses offline afterwards, which is why the key must be generated, not chosen.

Parameters

NameTypeDescription
§crypto
SessionCrypto
The platform primitives

Returns

(logger: Logger, sharedKey: string) => ProtocolProvider
createProtocol(logger, sharedKey), which returns the provider a channel binds to a session

Example

Composing v4 in a browser entry

export const createProtocol = createV4ProtocolFactory(crypto)
const protocolProvider = createProtocol(logger, sharedKey)
§function

isValidName(name: string): boolean

Validates whether the provided name is valid for protocol registration. The name must be a non-empty string.

Parameters

NameTypeDescription
§name
string
The name to validate

Returns

boolean
True if the name is a non-empty string, false otherwise

Example

Validating protocol names

isValidName('websocket')
// => true

isValidName('')
// => false
§function

isValidProtocol(protocol: unknown): ValidProtocolResult

Validates whether a protocol object contains all required function properties, stopping at the first that is missing or not a function.

Parameters

NameTypeDescription
§protocol
unknown
The protocol object to validate

Returns

ValidProtocolResult
An object mapping each protocol property to its validation status (true if valid, false if invalid, undefined if not yet checked)

Example

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, ... }
§function

isValidProtocolProvider(protocolProvider: unknown): boolean

Validates whether the provided value is a valid protocol provider. A protocol provider must be a function that creates protocol instances.

Parameters

NameTypeDescription
§protocolProvider
unknown
The value to validate as a protocol provider

Returns

boolean
True if the value is a function, false otherwise

Example

Validating a protocol provider function

isValidProtocolProvider(() => protocol)
// => true

isValidProtocolProvider('not-a-function')
// => false
§function

isValidReceiveFn(receive: unknown): boolean

Validates whether the provided value is a valid receive function. The receive function must be callable.

Parameters

NameTypeDescription
§receive
unknown
The value to validate as a receive function

Returns

boolean
True if the value is a function, false otherwise

Example

Validating a receive function

isValidReceiveFn((packet) => console.log(packet))
// => true

isValidReceiveFn(null)
// => false
§function

isValidSendFn(send: unknown): boolean

Validates whether the provided value is a valid send function. The send function must be callable.

Parameters

NameTypeDescription
§send
unknown
The value to validate as a send function

Returns

boolean
True if the value is a function, false otherwise

Example

Validating a send function

isValidSendFn((packet) => websocket.send(packet))
// => true

isValidSendFn('not-a-function')
// => false
§function

isValidSharedKey(sharedKey: unknown): unknown

Checks that a value can serve as a v4 shared key.

Parameters

NameTypeDescription
§sharedKey
unknown
The value to check

Returns

unknown
True for a string of at least 16 characters

Example

Validating a key before creating the provider

isValidSharedKey('k3y-that-is-long-enough')
// => true

Interfaces

§interface

ProtocolProviderEntry

Entry in a protocol provider store, associating a provider with identifiers.

Properties

§readonly id:string
Unique identifier for this entry
§readonly name:string
Human-readable name for this provider
§readonly provider:ProtocolProvider<T>
The protocol provider instance
§interface

ProtocolProviderStore

Store for managing protocol providers with lookup by name or ID.

Properties

§readonly add:(name: string, protocolProvider: ProtocolProvider<T>) => void
Registers a new provider with the given name
§readonly clear:() => void
Removes all providers
§readonly existsById:(id: string) => boolean
Checks if a provider exists by ID
§readonly existsByName:(name: string) => boolean
Checks if a provider exists by name
§readonly getById:(id: string) => ProtocolProvider<T>
Retrieves a provider by ID
§readonly getByName:(name: string) => ProtocolProvider<T>
Retrieves a provider by name
§readonly list:unknown
List of all registered provider entries
§readonly removeById:(id: string[]) => void
Removes providers by ID
§readonly removeByName:(name: string[]) => void
Removes providers by name
§interface

SessionCrypto

The platform primitives a session protocol is composed from

Properties

§readonly createKeyAgreement:() => Promise<KeyAgreementLike>
Mints an ephemeral key agreement
§readonly expandKey:(ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, usages: unknown) => Promise<CryptoKey>
HKDF expansion into a usage-restricted AES-GCM key
§readonly getRandomValues:(byteLength: number) => Uint8Array
Cryptographically secure random bytes
§readonly open:(key: CryptoKey, nonce: Uint8Array, additionalData: Uint8Array, sealed: Uint8Array) => Promise<Uint8Array<ArrayBufferLike>>
AES-GCM open under a key, nonce, and additional data
§readonly seal:(key: CryptoKey, nonce: Uint8Array, additionalData: Uint8Array, plaintext: Uint8Array) => Promise<Uint8Array<ArrayBufferLike>>
AES-GCM seal under a key, nonce, and additional data
§readonly stretchPassword:(password: string, salt: Uint8Array, options?: StretchOptions) => Promise<Uint8Array<ArrayBufferLike>>
PBKDF2 password stretching into raw bits
§readonly utf8Decode:(bytes: Uint8Array) => string
UTF-8 bytes to text
§readonly utf8Encode:(text: string) => Uint8Array
UTF-8 text to bytes

Types

§type

ValidProtocolResult

Result object mapping each protocol property to its validation status.
type ValidProtocolResult = mapped

Variables

§type

createProtocol

§type

MIN_SHARED_KEY_LENGTH

The shortest shared key v4 accepts; the guarantee needs a generated key of 128 bits or more
§type

V4

The v4 protocol's negotiated identifier and frame version byte