Noise and synchronization
The SDK implements the exact deployed
Noise_NK_25519_ChaChaPoly_SHA256 profile and bounded NIP-77 Negentropy state.
It never opens a socket or owns a database.
Noise handshake and transport
Create one handshake with the relay’s authenticated static public key. Send
messageOne as one binary WebSocket frame, pass the relay’s complete message
two to complete, then use the returned transport for ordered binary frames.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
let handshake = NoiseClientHandshake::new(relay_static_key, true)?;
socket.send_binary(handshake.message_one());
let transport = handshake.complete(message_two)?;
for frame in transport.encrypt(application_bytes)? {
socket.send_binary(frame);
}
if let Some(message) = transport.decrypt(received_frame)? {
handle_complete_message(message);
}
transport.shutdown();
val transport = NoiseClientHandshake(relayStaticKey).use { handshake ->
socket.send(handshake.messageOne)
handshake.complete(receiveBinaryFrame())
}
transport.use {
it.encrypt(applicationBytes).forEach(socket::send)
}
Call encrypt for one logical message and send every returned frame in order.
decrypt returns null until a chunked message is complete.
Keep the handshake, WebSocket, and transport inside one serialized coroutine owner. Close the transport on account loss, socket replacement, or protocol failure; do not reuse it after reconnect.
let handshake = try NoiseClientHandshake(
relayStaticPublicKey: relayStaticKey
)
try await socket.send(.data(handshake.messageOne))
let transport = try handshake.complete(messageTwo: await receiveData())
The actor sends every result from transport.encrypt in order and waits for a
non-nil result from decrypt before decoding an application message.
const handshake = new NoiseClientHandshake(relayStaticKey);
socket.send(handshake.messageOne);
const transport = handshake.complete(messageTwo);
for (const frame of transport.encrypt(applicationBytes)) socket.send(frame);
const complete = transport.decrypt(receivedFrame);
transport.shutdown();
Perform synchronous crypto in a Worker when messages are large.
Authentication failure closes this session. Never retry with the same handshake state, reveal detailed cryptographic errors, or concatenate transport frames outside the negotiated chunking rules.
Negentropy
Take one bounded, sorted snapshot of (createdAt, eventId) pairs from the host
database, then construct a client. Send initiate(). For each relay response,
apply the have/need IDs to host queries, send outgoing when present, and
finish when complete is true.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
let client = NegentropyClient::new(items, 60_000)?;
send(client.initiate()?);
let step = client.reconcile(response)?;
NegentropyClient(items).use { client ->
send(client.initiate())
val step = client.reconcile(receive())
}
Read the snapshot in a Room transaction, release the transaction, then reconcile. Query or mutate Room only in response to returned IDs; the client never holds a database handle.
let client = try NegentropyClient(items: items)
send(try client.initiate())
let step = try client.reconcile(response)
const client = new NegentropyClient(items);
send(client.initiate());
const step = client.reconcile(response);
client.close();
The snapshot and frame limits are denial-of-service boundaries. Split the application’s sync scope instead of raising them without a measured, interoperable protocol decision.