@hyperfrontend/network-protocol/node/v4v4
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
onDropwith codeauthentication-failedand nothing is delivered. - The browser counterpart lives at
/browser/v4and 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')createV4ProtocolFactory(crypto: SessionCrypto): (logger: Logger, sharedKey: string) => ProtocolProvider
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
| Name | Type | Description |
|---|---|---|
§crypto | SessionCrypto | The platform primitives |
Returns
(logger: Logger, sharedKey: string) => ProtocolProvidercreateProtocol(logger, sharedKey), which returns the provider a channel binds to a sessionExample
Composing v4 in a browser entry
export const createProtocol = createV4ProtocolFactory(crypto)
const protocolProvider = createProtocol(logger, sharedKey)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