@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
SessionCryptoThe 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
stringThe 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
unknownThe 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
unknownThe 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
unknownThe 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
unknownThe 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
unknownThe 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:stringUnique identifier for this entry
§readonly name:stringHuman-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>) => voidRegisters a new provider with the given name
§readonly clear:() => voidRemoves all providers
§readonly existsById:(id: string) => booleanChecks if a provider exists by ID
§readonly existsByName:(name: string) => booleanChecks 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:unknownList of all registered provider entries
§readonly removeById:(id: string[]) => voidRemoves providers by ID
§readonly removeByName:(name: string[]) => voidRemoves 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) => Uint8ArrayCryptographically 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) => stringUTF-8 bytes to text
§readonly utf8Encode:(text: string) => Uint8ArrayUTF-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