Skip to main content
This reference is generated from the published @offline-protocol/mesh-sdk@0.28.0 TypeScript source. Method parameters and return types match that package. Use the workflow guides for complete integration steps. Register event listeners before starting the protocol. Returned message, query and request IDs identify pending work; they are not proof of application acceptance.

OfflineProtocol

on

Registers an event listener One-shot events registered for late are replayed. An event that reached the SDK before any listener for it existed is held and delivered to the first listener that registers: but only for the one-shot tags (internet_session_superseded, mesh_stopped_by_user), which nothing else ever restates. Everything else is dropped, as it should be: a periodic event replayed after the fact would report a state that has since changed. That window is not an edge case, it is the default. The SDK subscribes to the native emitter inside its own constructor, so between new OfflineProtocol(...) and your first on(...) there is a stretch in which events arrive with nothing registered: and Android’s native redelivery of held one-shot events fires on exactly that constructor-time subscribe, landing squarely inside it. Registering synchronously right after construction keeps the window at zero and is still the right habit; this hold is what makes an await in between survivable. Replay is asynchronous (a microtask), so a handler never runs before the on(...) call that registered it has returned, and every listener registered in the same tick, including an 'all' listener added after a specific one, receives it. Delivery stays at-least-once: these events are state, not edges, and handlers must be idempotent. See docs/react-native-integration.md §6.1. returns: This instance for chaining

off

Removes an event listener returns: This instance for chaining

once

Registers a one-time event listener returns: This instance for chaining

removeAllListeners

Removes all listeners for a specific event type, or all listeners if no type specified returns: This instance for chaining

start

Starts the protocol Automatic BLE Management: When called, this method automatically starts BLE operations if BLE is enabled:
  • Starts scanning for nearby devices advertising the Offline Protocol service
  • Starts advertising this device so others can discover it
  • Begins polling for fragments to send
  • Handles incoming fragments from peers
Automatic MLS Initialization: If encryption is enabled (default: true), MLS is automatically initialized with platform-specific secure storage (iOS Keychain / Android EncryptedSharedPreferences). To disable auto-initialization, set encryption.enabled: false in the config. Permissions Required:
  • iOS: Bluetooth permissions (NSBluetoothAlwaysUsageDescription in Info.plist)
  • Android: BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT (Android 12+) or BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION (Android 11 and below)
throws: Error if protocol is already started or fails to start

stop

Stops the protocol Automatic BLE Management: When called, this method automatically stops all BLE operations:
  • Stops scanning for devices
  • Stops advertising
  • Disconnects from all connected peers
  • Cleans up BLE resources
throws: Error if protocol is not started or fails to stop

emitTestEvent

Emits a test event to verify the event system is working This is a debugging method that emits a network_metrics event with all zeros. Use this to verify that events are being delivered from Rust through the native bridge to JavaScript.

sendMessage

Sends a message returns: Message ID throws: Error if message fails to send

forwardMessage

Forwards a message to a new recipient with original sender attribution. Creates a new message with the original content and attaches forwarding metadata tracking the original sender, message ID, timestamp, and forward count. returns: New message ID throws: Error if forwarding fails

sendConnectionRequest

Sends a connection request params.recipient must be the target’s canonical address (off1…): the value they derived from their own identity key, which is also what neighbor_discovered reports as peer_id. The returned message id is the correlation key for the request’s outcome events: connection_request_undeliverable (recipient offline or retry budget exhausted), message_delivered (reached the recipient’s device), and message_failed (generic retry exhaustion, fires alongside the typed event). The recipient’s answer arrives as connection_accepted / connection_rejected, which correlate by peer id (accepted_by / rejected_by), not by message id. returns: Message ID throws: Error if request fails to send

acceptConnectionRequest

Accepts a connection request returns: Message ID throws: Error if acceptance fails to send

rejectConnectionRequest

Rejects a connection request returns: Message ID throws: Error if rejection fails to send

cancelConnectionRequest

Cancels a previously sent connection request returns: Message ID throws: Error if cancellation fails to send

getActiveTransports

Gets the list of active transports returns: Array of active transport type names

enableTransport

Enables a transport with optional configuration Re-enabling 'internet' is also the recovery from a relay supersede: it clears the transport’s latch, so any internet_session_superseded this instance is still holding for a late listener is dropped here. Held, it would tell an app with a freshly reconnected relay socket that it is connected elsewhere, with nothing to correct it. Mirrors the same discard on the native side. throws: Error if transport fails to enable

disableTransport

Disables a transport throws: Error if transport fails to disable

isBluetoothEnabled

Checks if Bluetooth is enabled on the device returns: True if Bluetooth is enabled, false otherwise

requestEnableBluetooth

Requests the user to enable Bluetooth On Android, this shows a system dialog to enable Bluetooth. On iOS, this returns false as iOS doesn’t allow programmatic Bluetooth enabling. returns: True if Bluetooth was enabled, false otherwise

getTopology

Gets the current network topology returns: Network topology snapshot including nodes, links, and stats throws: Error if topology retrieval fails

getMessageStats

Gets message delivery statistics returns: Array of message delivery statistics throws: Error if stats retrieval fails

getDeliverySuccessRate

Gets the delivery success rate returns: Success rate as a number between 0 and 1 throws: Error if retrieval fails

getMedianLatency

Gets the median message delivery latency returns: Median latency in milliseconds, or null if no data available throws: Error if retrieval fails

getMedianHops

Gets the median hop count for delivered messages returns: Median hop count, or null if no data available throws: Error if retrieval fails

sendMedia

Sends a media attachment (image, video, audio, file, etc.) to a recipient. The platform reads the file and passes the raw bytes as a base64 string. The SDK chunks the data and sends each chunk via internet-preferred transport. returns: File ID for tracking progress

sendFile

Sends a generic file to a recipient (convenience for sendMedia with ContentType.File). returns: File ID for tracking progress

sendImage

Sends an image to a recipient. returns: File ID for tracking progress

sendVoiceNote

Sends a voice note to a recipient. returns: File ID for tracking progress

sendVideoNote

Sends a video note to a recipient. returns: File ID for tracking progress

sendVideo

Sends a video to a recipient. returns: File ID for tracking progress

getFileProgress

Gets the progress of a file transfer returns: File progress information, or null if not found

cancelFileTransfer

Cancels an active file transfer returns: True if cancelled, false if not found

receiveMessage

Polls for the next received message returns: Message object if available, null otherwise throws: Error if polling fails

pause

Pauses the protocol (for background mode) throws: Error if protocol is not running or fails to pause

resume

Resumes the protocol from pause throws: Error if protocol is not paused or fails to resume

getState

Gets the current protocol state returns: Protocol state (Stopped, Running, or Paused) throws: Error if retrieval fails

setBatteryLevel

Reports the device’s battery level to the protocol engine. This is the feed for every battery-dependent policy in the SDK: DORS energy scoring, relay promotion/demotion (relay_promoted / relay_demoted), the message-forwarding battery floor, and the telemetry device-capability snapshot. No transport can observe the host’s battery, so until this is called each of those policies runs in its unknown-level branch. Call it on start and on each platform battery notification. Prefer setBatteryState where charging state is available: a charging device is deliberately excused the soft relay battery floor, so reporting the level alone strips relay duty from plugged-in devices that should keep it.

setBatteryState

Reports the device’s battery level and charging state to the protocol engine. See setBatteryLevel for what depends on it.

getBatteryLevel

Gets the last reported battery level returns: Battery level (0-100) or null if the host has not reported one

getIsCharging

Gets the last reported charging state (false if none reported).

setRelayPriority

Sets the relay priority. A shorthand for updating only relayPriority; see updateRelayConfig for the rest. The legacy 'low' | 'medium' | 'high' spelling is still accepted and maps to never / auto / always. throws: Error if setting fails

getRelayPriority

Gets the current relay priority

updateRelayConfig

Updates the relay configuration at runtime. Governs whether this device carries other people’s traffic and under what conditions it takes the relay role. Applies to the next role evaluation and the next forwarding decision: no restart needed. Omitted fields keep their current values. The battery-dependent parts need a battery feed to do anything: see setBatteryState.

getRelayConfig

Gets the current relay configuration.

isRelay

Checks if this device is currently acting as a relay returns: True if device is a relay

getBLePeerCount

Gets the current number of discovered BLE peers returns: Number of BLE peers currently tracked

getBleDiagnostics

Gets the BLE diagnostic counters. These are the rollout alarm for the self-certifying-address migration. Each counter records a frame that took a degraded path rather than failing outright, so none of them show up as a delivery error: a fleet can be quietly falling back on every send while its success metrics look healthy. Read them together and watch the trend, not the absolute value: small counts are normal, sustained growth after a release means peers disagree about identity or MTU.
  • fragmentFallbacks: a frame for a directly-connected peer had to be broadcast to every peer instead of addressed to one. Rising means recipients are not being recognised as the connected peer they are.
  • recipientNotAmongPeers: a send named a peer that is connected over BLE but not under that id. This is the sharpest identity-mismatch signal: it is what a peer announcing one id while framing another looks like from the sender’s side.
  • undersizedMtuReports: a peer reported an MTU too small to carry a fragment header, so a conservative default was used.
All three read zero when BLE is not enabled or not yet started. returns: The three counters as of now

getTransportMetrics

Gets detailed metrics for a specific transport returns: Transport metrics or null if not available

enableTelemetry

Enables telemetry: the SDK collects, batches and uploads accepted events to the Offline Protocol ingest itself, on a background thread inside the native library. Nothing crosses the bridge per event and nothing reaches JavaScript; the only inputs are the apiKey and appId the developer portal issued. The native module fills in the platform (os, osMajor) and owns the application lifecycle: entering the background closes the telemetry session (a summary, then a flush inside an OS background task on iOS), and returning to the foreground opens the next one. There is no lifecycle call for the app to make. Batches are durable across a restart once initializeMls has run (the queue lives on the protocol-state store); before that they are held in memory. Calling this again replaces the running pipe after its final flush. Rejects with code TelemetryConfigInvalid naming the refused field when the configuration is unusable (an empty key, a non-ASCII app id, a zero batch size). What leaves the device, when, and how to switch it off are documented in docs/telemetry.md; docs/privacy.md has the store disclosures.

disableTelemetry

Disables telemetry after one final flush of up to three seconds. The emit path goes back to its telemetry-off cost. Idempotent.

flushTelemetry

Asks the uploader to send what is queued now. Returns immediately; the upload happens on the native background thread.

telemetryStats

The telemetry pipe’s counters, or null while telemetry is not enabled. acceptedEvents is what the ingest reported accepting, which is what an invoice is reconciled against; dropped counts events lost to the ring buffer, the durable queue’s caps, the six-day expiry, a permanent rejection, or everything queued or collected after the ingest reported the application’s telemetry toggle off; lastError is the most recent send failure, and clears once a batch is accepted, so it reports the current state rather than the high-water mark of a recovered outage.

endTelemetrySession

Closes the current telemetry session explicitly: a summary, a flush of up to three seconds, then a fresh session id. The lifecycle-driven boundary still applies; calling both is safe because each summary reports only what the previous one did not. A no-op while collection is off; use flushTelemetry to push a queued backlog without collecting.

setTelemetryEnabled

Stops or resumes new collection without tearing the pipe down. Off, every emit costs one atomic load and nothing is buffered, and no session summary is emitted. The lifecycle transitions are still tracked, so the first foreground after collection resumes rotates the session as usual. Records queued before the switch continue to drain: this is a collection control, not an immediate network stop or a deletion. It is the runtime opt-out for a user setting.

telemetryInstallId

Returns a stable, opaque per-install telemetry identifier (32 hex characters), derived from the SDK-managed persistent scrub secret. The secret itself never crosses the bridge and cannot be recovered from the id. Resolves null until the persistent secret is available, which is after initializeMls has wired secure storage. Stamped on telemetry batches only when TelemetryConfig.includeDeviceId is set; see docs/privacy.md for the disclosure that setting carries.

forceTransport

Forces the protocol to use a specific transport (overrides DORS) throws: Error if forcing fails

releaseTransportLock

Releases the transport lock and lets DORS make decisions again

updateDorsConfig

Updates DORS configuration at runtime. Omitted fields keep their current values: the same partial-update contract as updateRelayConfig. Every field must therefore be expressible here: a field this signature omits cannot be set, and (before the bridges merged from the live config) was silently reset by any update that changed something else. throws: Error if update fails

getDorsConfig

Gets the current DORS configuration returns: DORS configuration

updateAckConfig

Updates ACK configuration at runtime throws: Error if update fails

updateRetryConfig

Updates retry configuration at runtime throws: Error if update fails

updateDedupConfig

Updates deduplication configuration at runtime throws: Error if update fails

getDedupStats

Gets deduplicator statistics for monitoring returns: Deduplication statistics

getMeshRelayStats

Reports how much traffic this device is carrying for other people. Counters are cumulative for the lifetime of this instance and never reset, so a rate is a difference between two reads. The exception is awaitingTransmission, a gauge that goes down as well as up. forwarded is the contribution figure to show a user; transmissions is the one the per-second budget bounds, since it counts each link separately and includes this device’s own sends. Two readings worth knowing: rateDeferred rising means forwarding is hitting its ceiling and those frames are delayed rather than dropped, and coveredByANeighbor is the mesh working as intended: a neighbor was heard carrying the frame first, so this device stood down. For whether back-pressure is actually costing anything, read refusedQueueFull and abandonedOverdue. Those are the two that count frames genuinely lost, and a device shedding traffic can otherwise show nothing but healthy-looking deferrals. Note that a device with a working relay connection forwards nothing: the mesh is only offered frames no other carrier can deliver. Zero counters on an online device are the honest answer, not a fault. returns: Cumulative mesh forwarding counters

getMeshRelayTunables

Reports the mesh forwarding tunables actually in force. Read from the governor in the Rust core, so this is what forwarding decisions really use rather than an echo of what was passed to create(): including every default this app never set. Every field is present. Nothing here needs a ?? fallback, and writing one would be inventing a second copy of a default that can drift. returns: The mesh forwarding tunables in force

getCustodyStats

What this device holds for its neighbours and has done as a depositor (docs/spec/custody.md), read through to the Rust core. held and heldBytes are gauges; everything else is cumulative. Every refusal reason in the acceptance table has a counter, so an operator who enabled custody and sees nothing held can tell “off” from “nobody asked” from “everyone refused”. returns: Custody counters

eraseCustody

Drops every held frame and resets the custody counters. Callable on its own; the data layer’s wipeAll() erases custody in the core as well, because a custody store that survived a logout would hold other people’s traffic past the point the user asked for erasure. Fails only when a record could not be deleted, after attempting every one.

getPendingAckCount

Gets the number of pending ACKs waiting for confirmation returns: Number of pending ACKs

getRetryQueueSize

Gets the current retry queue size returns: Number of messages in retry queue

shouldEscalateToWifi

Checks if DORS recommends escalating to WiFi. Use this to query whether the protocol should switch from BLE to WiFi Direct. returns: True if escalation to WiFi is recommended

processFileChunk

Processes a file chunk. Use this for custom file transfer handling. Native bridge hook. Use only when implementing a transport bridge.

finalizeFile

Finalizes a file transfer. Call this after all chunks have been processed.

wifiDirectStatusChanged

Notifies the protocol that the platform’s stream layer is up or down. Up means streams can be established, not that a peer is reachable. The slot counts as an available carrier only once a stream has proved a peer, and it queues a message only toward an address a stream has proved, so an up layer with no announced peer is not a carrier at all. Down clears every announced link. Native bridge hook. Use only when implementing a transport bridge.

wifiDirectMessageReceived

Hands the protocol one frame body that arrived on the stream whose preamble proved senderId. senderId is the address verifyIdentityAssertion derived from that stream’s first frame, and nothing else: the core attributes the body to it and matches Message.sender against it, so a body handed up under a socket address, a device name, or an address merely read from a discovery record is refused as unattributed. data is the body after the length prefix, exactly one message. Native bridge hook. Use only when implementing a transport bridge.

wifiDirectGetNextMessage

Gets the next outgoing body and the proved address of the stream to write it on. The caller frames it (u32 big-endian length, then the body) and writes it to the stream whose preamble proved recipientId; nothing is ever returned for an address no stream proved. returns: The body and its stream’s address, or null if the queue is empty Native bridge hook. Use only when implementing a transport bridge.

wifiDirectPeerConnected

Announces a stream whose preamble verified, under the address it proved. Call it once per announced stream with the derived address. An unproven peerId here is entered into the core’s capacity-bounded known_peers, evicting genuine neighbours, and starts an automatic key exchange toward a peer that cannot answer it, which is why nothing but the verifier’s result may be passed. A receiver holds one announced stream per address: a second stream proving an address already announced is refused or supersedes the first, and either way the core sees one announcement and, later, one loss. Native bridge hook. Use only when implementing a transport bridge.

wifiDirectPeerDisconnected

Reports that the announced stream for peerId ended. Only for a stream that was announced: a stream that never proved a peer has nothing to report, and reporting it would tell the core a peer it may still hold over another stream is gone. Native bridge hook. Use only when implementing a transport bridge.

internetStatusChanged

Notifies the protocol of internet connection state change. Native bridge hook. Use only when implementing a transport bridge.

internetMessageReceived

Handles an incoming internet message. Native bridge hook. Use only when implementing a transport bridge.

internetGetNextMessage

Gets the next outgoing internet message. After sending over the wire, you must call either internetConfirmSent(messageId) or internetSendFailed(messageId). returns: Message to send (with messageId) or null if queue is empty Native bridge hook. Use only when implementing a transport bridge.

internetConfirmSent

Confirms that a message was successfully sent over the wire (e.g., WebSocket). Call this after the WebSocket send() completes successfully. This feeds real delivery data into transport metrics for DORS routing. Native bridge hook. Use only when implementing a transport bridge.

internetSendFailed

Reports that a message failed to send over the wire. Call this when the WebSocket send() fails or the connection drops. Native bridge hook. Use only when implementing a transport bridge.

initializeMlsWithSecureStorage

Initializes MLS with built-in secure storage. Uses iOS Keychain or Android EncryptedSharedPreferences. Note: This is called automatically by start() when encryption.enabled is true (default). You only need to call this manually if you disabled encryption initially and want to enable it later. throws: Error if initialization fails

isMlsInitialized

Checks if MLS is initialized. returns: True if MLS is ready for use

getIdentityPublicKey

Gets the identity public key (Ed25519, 32 bytes). This is the public key derived from the MLS credential and can be shared with others for identity verification and secure communication. returns: The public key as an array of bytes throws: Error if MLS is not initialized

deriveAddress

Derives the canonical self-certifying address of an Ed25519 identity key: off1… (44 characters), the bech32m encoding of 0x01 || SHA-256(publicKey)[0:20]. The address is a hash of the key, so it authenticates itself: a peer claiming an address is checked by re-deriving it from the key they present. The same key always yields the same address, and every address has exactly one valid string form. Needs no protocol instance: safe to call before create(), e.g. to verify an invite or QR code. returns: The derived off1… address throws: If publicKey is not 32 bytes

parseInvite

Decodes and verifies an invite blob. Needs no protocol instance: safe to call before create(), which is the whole point: a scanner verifies a QR code before deciding to act on it. Verification is mandatory and total. The address must be the one its public key derives to, and any signature present must verify under that key, so a resolved InviteInfo is always self-certified. What it does not prove: that the invite came from who you think. An attacker’s own correctly-signed invite is indistinguishable from a legitimate stranger’s: only the out-of-band context (this QR was on this person’s screen) carries that. returns: The verified invite throws: If the blob is malformed, the address is not the key’s, or a signature does not verify. Every case means refuse, not warn.

createInvite

Builds an invite blob for this identity. The result is opaque base64url. Apps own the container; the recommended form is one parameter, <app-scheme>://connect?c=<blob>, so it composes with an existing scheme and route. Sign it when the invite may travel without its issuer, a link forwarded through a third party, because the signature binds the petname to the key, so a forwarded invite cannot save Alice’s key under the name “Bob”. Leave it unsigned for a QR shown phone to phone: the physical channel already authenticates it, and an app that lets the user confirm the name has made the user the authority over it. Signing costs about 90 characters. Carries no key package by design (an MLS init key is single-use and a QR code is static, so pairing them guarantees a collision as soon as two people scan the same code) and no expiry (a printed QR that stops working is a bug).

resolveUsername

Returns true if this call started the lookup, false if one was already running; exactly one username_resolved follows either way. Requires transports.nostr.usernameDiscoveryEnabled. Resolves true if this call started the lookup and false if it joined one already in flight. Both mean an answer is coming: exactly one username_resolved event follows either way, so awaiting that event after either result is safe. Every case where no event will ever arrive rejects instead, so a false can never leave a spinner running forever. The rejection code says which, and they call for different handling:
  • InvalidConfiguration: discovery is off (it also requires coldContactEnabled). Retrying unchanged can never succeed.
  • InvalidState: too many lookups in flight. Transient; retry shortly.
  • NotStarted: the protocol is not running, so nothing would pump relay traffic or sweep the deadline for this lookup. Transient; retry after start().
  • InvalidArgument: not a claimable username (empty, over 64 bytes once normalized, carrying a control or format character, or address-shaped).
Subscribe to username_resolved before calling this. The answer is a single event with no replay, so a listener attached afterwards can miss it. The answer carries every verified claim. There is deliberately no “best” claim and no ordering: anyone may publish any name, so what comes back is a set of assertions for a human to arbitrate, not a lookup result. Do not auto-select. Taking the first entry turns a non-authoritative directory into an authoritative-looking one: the user then believes the name was verified when only a key ever was. Present the claims, have the user confirm out of band, and store the address, never the name: a name can be re-claimed by anyone tomorrow, an address is self-certifying. returns: true if this call started the lookup, false if it joined one throws: InvalidConfiguration if discovery is off, InvalidState if too many lookups are in flight, NotStarted if the protocol is not running, InvalidArgument if the name is not claimable

localAddress

This device’s own address (off1…), or null before startup completes. Derived from the identity key held in this profile’s storage: the app does not choose it, and it is stable across restarts of the same profile. This is the string to show the user, put in an invite or QR code, and what peers pass as the recipient to reach this device. null until MLS is initialized (which start() does), because the key that defines it lives in storage that is not open before then. The identity_ready event carries the same value at the moment it is known.

deriveUserIdFromPublicKey

Derives a deterministic user ID from a public key. deprecated: Use deriveAddress. This returns the same off1… address, but requires an initialized protocol instance and accepts any input length rather than rejecting keys that are not 32 bytes. returns: The derived address string

signData

Signs arbitrary data with the identity private key (Ed25519). Use this to prove ownership of your identity or to sign messages. returns: The signature as an array of bytes (64 bytes) throws: Error if MLS is not initialized

verifySignature

Verifies a signature against a public key. Use this to verify that data was signed by the owner of a public key. returns: True if the signature is valid throws: Error if verification fails due to invalid input

mlsGenerateKeyPackage

Generates a new MLS key package. Key packages are used by others to establish encrypted sessions with you. returns: Generated key package throws: Error if generation fails

mlsGetOrCreateKeyPackage

Gets an existing key package or creates a new one. returns: Key package throws: Error if operation fails

mlsGetPendingKeyPackages

Lists the key packages this app may publish itself, for example to its own key server. Only packages nobody has spoken for are listed: never one the SDK has already handed to a peer or one standing in its own publication slots, and never one already marked synced. The SDK adds none of its own, so the list is empty until the app mints packages with mlsGenerateKeyPackage. returns: Array of key packages free to publish

mlsMarkKeyPackageSynced

Records that the app has published a key package itself. The package stays on the device so a Welcome built from the published copy still opens. It is no longer listed as pending and is never handed to a peer. It expires with the lifetime it was minted with, and its private key is destroyed after that, whether or not anybody used it. Marking an unknown, expired or already used package does nothing. throws: Error if operation fails

mlsImportKeyPackage

Imports another user’s key package. Required before you can send encrypted messages to them. throws: Error if import fails

mlsHasSession

Checks if an MLS session exists with another user. returns: True if session exists

hasPendingKeyPackage

Checks if a pending key package is available for a peer. Key packages are received automatically when peers are discovered (if auto_key_exchange is enabled). This method checks if we have received the peer’s key package and can establish a session. returns: True if key package is available

getEstablishmentState

Returns the current secure-session establishment state for a peer. Useful for retry/UI flows when operations fail with SessionNotReady.

establishSecureSession

Establishes a secure MLS session with a peer (high-level API). This method handles the complete session establishment flow:
  • If session already exists, returns null
  • If a pending key package is available, imports it, creates session, sends Welcome
  • If no key package is available, throws an error
This is the recommended method for establishing secure sessions as it handles the key package exchange flow automatically. returns: Welcome message if session was created, null if session already exists throws: Error if no key package is available (peer hasn’t completed key exchange)

rekeySession

Rotate the 1:1 session with a peer, advancing post-compromise security. Post-compromise security arrives when a commit rotates a member’s leaf in the ratchet tree, and the SDK originates one on a re-key. Nothing drives a re-key on its own except an epoch desync, so a pair that never forks never rotates unless the application asks. That bites hardest against a leaf node, a lock, a sensor, which never commits at all, so every rotation in such a pair is this side’s to originate. The cadence is yours on purpose: a rotation costs a teardown, a key-package exchange and a re-establish, and what that is worth depends on the deployment rather than on anything the wire says. The peer sees exactly what a desync-driven re-key sends. Queued messages survive, because they are sealed at flush time against whatever session is current then. A rotation that fails changes nothing. The reset is advertised before the local session is torn down, so a rejection leaves the session intact, still usable, and the rate-limit window unspent. Rotate while the peer is reachable and treat a failure as “try again later” rather than as a session to rebuild. returns: true when the rotation was driven; false when the per-peer rate-limit window has not lapsed. False is not a failure: call again later. throws: Error if encryption is not initialized, the peer is blocked, there is no session to rotate (establish one first), or no transport carried the reset.

mlsCreateSession

Creates an MLS session with another user. Returns a Welcome message that must be sent to the other user. Note: Prefer using establishSecureSession which handles the key package flow automatically. This lower-level method requires the peer’s key package to already be imported via mlsImportKeyPackage. returns: Welcome message to send to the other user throws: Error if session creation fails

mlsJoinSession

Joins an MLS session from a Welcome message. returns: Session info throws: Error if joining fails

mlsEncryptForUser

Encrypts a message for another user. Creates a session automatically if one doesn’t exist. returns: Encrypted message throws: Error if encryption fails

mlsDecryptFromUser

Decrypts a message from another user. returns: Decrypted plaintext as bytes, or null if decryption fails

mlsDecrypt

Decrypts any MLS message (1:1 or group). returns: Decrypted plaintext as bytes, or null if decryption fails

mlsListSessions

Lists all active MLS sessions. returns: Array of user IDs with active sessions

mlsDeleteSession

Deletes an MLS session with another user. throws: Error if deletion fails

mlsProcessWelcome

Processes a Welcome message (auto-detects session vs group). returns: Session or group info throws: Error if processing fails

meshCreateGroup

Creates a new MLS group with full protocol integration. Emits GroupCreated event. Use inviteToGroup() to add members. returns: Group info

meshInviteToGroup

Invites a user to an MLS group. Sends MLS Welcome to invitee and Commit to existing members. Emits GroupMemberAdded event on all participants.

meshSendGroupMessage

Sends an MLS-encrypted message to all group members via mesh transport. Handles encryption and fan-out to each member automatically. returns: Array of per-member message IDs

meshForwardMessageToGroup

Forwards a message to all members of a group with forwarding attribution. The message content is encrypted via MLS for the group and fan-out follows the same path as regular group messages. returns: Array of per-member message IDs

meshRemoveFromGroup

Removes a member from an MLS group. Sends removal notification to all members.

meshLeaveGroup

Leaves an MLS group with notification to other members.

meshListGroups

Lists all MLS groups (excluding 1:1 sessions). returns: Array of group IDs

meshGetGroupInfo

Gets information about an MLS group. returns: Group info or null if not found

meshGroupRichReadiness

Whether a rich group send right now would seal its extras, and which members hold the gate closed. Point-in-time and advisory: the send path re-evaluates the gate itself: use this to warn before sending instead of learning from a GroupRichExtrasDropped event after the drop. returns: Readiness snapshot ({ ready, unknownMembers })

groupRelaySyncState

The relay-side registration state of a group. Point-in-time: transitions arrive as group_relay_sync_changed events. 'synced' means the relay positively acknowledged the group’s registration on the current connection: relay-dependent server commands for it (CreateGroupInviteLink & co. via sendRawServerCommand) can be issued. 'pending' means a registration is in flight; 'unsynced' means none is (Internet down, relay grouping disabled, or a prior attempt errored / timed out).

requestGroupRelayRegistration

Registers (or re-registers) a group with the relay server on demand: the supported path for making a mesh-created group known to the relay before issuing relay-dependent server commands for it. Never raw-send CreateGroup: it desyncs the SDK’s registration tracking. Fire-and-event: the outcome arrives as group_relay_sync_changed (ensureGroupRegistered wraps the wait). Resolves true when the registration frame was queued (or the group is already synced), false when relay grouping is disabled or the Internet transport is unavailable; rejects when the group is unknown locally.

ensureGroupRegistered

Resolves once the relay holds a positively acknowledged registration for the group: the gate to await before CreateGroupInviteLink and other relay-dependent raw server commands for a mesh-created group. Resolves immediately when the group is already synced; otherwise kicks a registration (when none is in flight) and waits for the group_relay_sync_changed outcome. Rejects on a negative outcome (reason: 'error' | 'ack_timeout' | …), when the registration cannot be sent (Internet down / relay grouping disabled / unknown group), or on timeout. The SDK re-sends an unanswered registration every 30s up to 3 attempts before giving up with ack_timeout (~90s worst case): the default timeout covers that full cycle. A shorter timeout is fine for UI purposes: the SDK keeps retrying in the background and a later group_relay_sync_changed still fires on success.

meshSetMemberRole

Sets a member’s role in an MLS group (admin only). Broadcasts role change to all group members.

meshGetMemberRole

Gets a member’s role in an MLS group. returns: Role string (“admin” or “member”)

meshGetGroupRoles

Gets all member roles in an MLS group. returns: Map of user_id -> role

meshRenameGroup

Renames an MLS group (admin only). Broadcasts the rename to all group members. throws: Error if not admin or group not found

destroy

Destroys the protocol instance and cleans up resources

wipePersistedState

Erases every byte of persisted SDK state for one account: the namespaced secure store (MLS identity, sessions, peer trust records, the Nostr signing secret, the protocol-state record key), the account’s protocol-state directory (outbox, pending queues, block list, media descriptors), and, when this account owns it, or nobody does, the pre-namespace store an upgraded install inherited from. Call this on logout and on username switch, after destroy(). The protocol persists as it works, so wiping underneath a live instance races those writes; the native side rejects the call if the account named here is the one the current instance is running. Without it, an account’s undelivered messages are restored and re-driven on the next launch for the lifetime of the outbox, and on iOS, where the Keychain outlives the app container, its identity and delivery state survive an uninstall and are adopted again after a reinstall. The account is named explicitly because destroy() clears the config the namespace would otherwise be derived from. Pass the same appId and profile the protocol was created with; any other pair names a different account and wipes nothing. Irreversible, and it rotates the account’s MLS and Nostr identities: peers holding a session with it will see a desync on next contact and re-establish from a fresh key package. Safe to call twice: a failed wipe should simply be retried. Applications that supply their own storage providers must erase their own containers: this only knows about the built-in ones.

sendPresenceUpdate

Sends a presence update to a peer. returns: Message ID

checkInternetPresence

Asks the internet relay for a peer’s presence (one-shot CheckPresence).

Contract

  • Always fresh. The SDK never throttles or dedupes manual checks: every accepted call sends a new CheckPresence frame to the relay, regardless of how recently the same peer was queried. (The automatic watch loop’s tick/TTL policy does not apply here.)
  • Fire-and-event. The answer arrives as a presence_updated event with source: 'internet' (including last_seen_ms when the relay knows it) rather than in the returned promise. Every relay answer re-emits the event even when nothing changed: safe to drive a chat-header refresh from. Subscribe before calling; events have no replay.
  • Exceptions. The core suppresses presence for blocked peers and your own user id: for those, this resolves true (the query was sent) but no presence_updated follows. And true means the query reached the socket, not that an answer will arrive: a connection dropped before the relay replies loses the answer (call again).
  • Rate limiting is never bypassed, force or not: the SDK’s client-side limiter mirrors the relay’s per-connection budget, and an over-budget frame would be dropped server-side after a locally “successful” write: strictly worse than deferring.
options.force is for chat open/focus: exactly when the app wants a fresh header, the socket is often still resuming from background. A non-forced call fails fast (false) in that window; a forced call is parked and retried until the transport is authenticated and the limiter admits it (up to ~8s), only then resolving false. On a stopped transport (no reconnect coming) even forced calls fail fast. Forced checks stay one-shot: they never join the SDK’s automatic watch set. returns: true once the socket accepted the query (write-confirmed on iOS, enqueue-confirmed on Android, the closest OkHttp offers); false otherwise, an empty userId (never sent), or not connected+authenticated / rate-limiter-deferred past the force deadline (non-forced: immediately; safe to retry) throws: when the internet transport was never initialized (enable it via transports.internet before calling)

sendRawServerCommand

Sends a raw, caller-built relay command verbatim over the SDK’s internet socket: the generic server-command channel for relay features that are app concerns rather than SDK APIs (the invite-link lifecycle: CreateGroupInviteLink, JoinGroupViaInvite, AckGroupInviteJoin, …). Responses the SDK doesn’t consume arrive as internet_server_message events carrying the verbatim frame. GroupInfo and UserGroups are also emitted on that channel in addition to their stable typed events, so application-owned extension fields remain lossless. Correlate request/response with your own request_id where the relay supports one. Gate calls on isInternetReady() / the internet_status_changed event rather than probing with a command and retrying on false. For group-scoped commands (CreateGroupInviteLink, …) additionally await ensureGroupRegistered(groupId) first: the relay must know the group. Do not send frame types the SDK itself manages (SendMessage, CreateGroup / member deltas, LeaveGroup, CheckPresence, …): the SDK cannot correlate their answers with its own in-flight state, and a raw CreateGroup/LeaveGroup desyncs the bridge’s registration tracking. Use the typed SDK APIs for those. returns: true once the socket accepted the command (write-confirmed on iOS, enqueue-confirmed on Android: the closest OkHttp offers); false when not connected+authenticated, the JSON is invalid, or the SDK’s client-side rate limiter deferred it (the SDK’s mirror sits slightly under the relay’s 30-burst/10-per-second budget, at 28 burst / 9 per second: safe to retry after a short delay) throws: when the internet transport was never initialized (enable it via transports.internet before calling)

isInternetReady

Whether the SDK’s internet socket is connected AND relay-authenticated: the same gate sendRawServerCommand checks before writing. The positive replacement for app-side relayStatus === 'authenticated' tracking: gate raw sends on this (or on the internet_status_changed event) instead of probing with a command and retrying on false. Point-in-time; transitions arrive as internet_status_changed events. A ready socket can still defer an individual send (client-side rate limiter): sendRawServerCommand returning false while ready means retry after a short delay. Cannot tell you why it is false. An ordinary disconnect (which reconnects itself) and a relay displacement (which never will) both read false here: use isInternetSuperseded to tell them apart. returns: true when connected and authenticated; false otherwise, including when the internet transport was never initialized (never throws)

isInternetSuperseded

Whether the relay displaced this session, another device registered the same identity and took over the relay slot, and the SDK latched the internet transport stopped. This is the question isInternetReady structurally cannot answer. A false from it means “not usable right now” and nothing more: an ordinary disconnect reconnects itself within seconds, while a displaced session will never reconnect on its own, because a blind reconnect would just re-displace the other device in a loop. The two are indistinguishable from readiness alone. True here means the only recovery is a deliberate re-enable:
Complements the internet_session_superseded event rather than replacing it. The event tells an app that is listening at that instant; this answers an app that asks: including one that subscribed later, or whose process was killed and restarted, which no in-memory event hold survives. A foreground reconcile against this is the most robust shape. returns: true while the session is superseded; false otherwise, including when the internet transport was never initialized (never throws)

forceInternetReconnect

Forces an immediate teardown + reconnect + re-authenticate of the SDK’s internet socket, bypassing the exponential reconnect backoff. isInternetReady() is a point-in-time cached flag, not a liveness probe: an OS suspend can kill the TCP connection before a clean WebSocket close, so after a background→foreground transition the socket may be a zombie (dead but still reported ready) or alive-but-deregistered by the relay. Neither is detectable by a ping, and both are healed by the same action: a full reconnect that re-runs the relay authenticate/register handshake. You normally do NOT need to call this on foreground: as of the automatic foreground-heal, both native bridges (iOS applicationWillEnterForeground, Android onHostResume) already force a reconnect themselves after a background stay long enough to have killed the socket (~4s), gated on monotonic background duration: and iOS additionally tears down a zombie on the first stalled write via the write-stall watchdog. Calling this method in addition, on every foreground, would double-reconnect and drop a genuinely-healthy socket, forcing a wasted group re-registration round-trip. Keep it for the cases the automatic heal does not cover: a deliberate user-initiated “reconnect now”, or a stale socket you detect while already foregrounded (e.g. a long idle period with no background transition). If you do drive foreground recovery yourself, debounce and gate on background duration rather than calling on every foreground. Recovery lands in ~1s rather than waiting ~20-30s for zombie ping detection. Emits a transient internet_status_changed down→up. No-op unless the internet transport is running (respects the enable/disable lifecycle). returns: true once the request reached a live internet transport: this means “accepted”, not “reconnected”: it is also true when the transport is initialized but not currently running/starting, in which case the call is a deliberate no-op. false only when the internet transport was never initialized. Never throws.

sendTypingIndicator

Sends a typing indicator to a peer. returns: Message ID

sendReadReceipt

Sends a read receipt to a peer. returns: Message ID

blockUser

Blocks a user. Messages from blocked users are silently dropped at the protocol level.

unblockUser

Unblocks a previously blocked user.

getBlockedUsers

Returns the list of blocked user IDs. returns: Array of blocked user IDs

isUserBlocked

Checks if a specific user is blocked. returns: true if the user is blocked

DataStore

createDoc

Creates a document, or does nothing if it already exists.

deleteDoc

Drops this device’s copy, leaving the peers’ copies alone. Eviction rather than removal: it reclaims what the document occupies here and records nothing about the name, so the next version exchange with a replica that still holds it recreates and refills it. Use removeDoc to remove content from the room.

removeDoc

Removes a document from every replica of its space. A replica holding an edit made concurrently with the removal keeps its copy and hands the document back, contents and all, so an edit beats a removal whichever happened first. That arrives as an ordinary data_changed on a document you removed. Every later call on the name rejects with InvalidState until createDoc brings it back, on this device and on every device that learns of the removal. data_doc_removed is the cue to close whatever has the document open. Removing a name this device does not hold does nothing. Re-using the name afterwards is not fenced off: a replica that kept the old contents past the removal merges them into the new document. Give new content a fresh name.

removeSpace

Removes every document a space holds, from every replica of it.

fetchAttachmentFrom

Asks one member of a group for the bytes behind a reference. A reference replicates to everybody and says nothing about who holds the bytes, so a group fetch names the member; the SDK does not try members in turn, because each miss costs the whole silence timeout. The bytes come back as frames under the group key, so no pairwise session with that member is needed, and they are bounded at 1 MiB. Anything larger travels over a 1:1 session, and the holder is told so at the call. Use fetchAttachment for a 1:1 space.

setInterest

The documents this device wants from its peers in one space. Each pattern is a document name, optionally ending in * to match a prefix. ['*'] is everything and is the default; [] is nothing. At most 32 patterns per space. Toward a peer this is a request that saves the radio. Locally it is a refusal, so a document outside it is never stored however it arrives, which is what makes a narrowing mean something against a peer on an older build. Declare it before OfflineProtocol.start. The native store does not exist until start() opens it, so a call made earlier is held and resolves at once, and start() applies it after storage opens and before the engine starts. That ordering matters: the engine’s start-up exchange offers every held space with the interest in force at that moment, and narrowing afterwards does not delete what the wider exchange pulled in. A later call for the same space replaces a held one. Validation of a held declaration therefore happens in start(): a refused pattern rejects start() with an error naming the space, and the engine is not started, because starting would replicate the space whole. Correct it with another setInterest and call start() again. When the data layer is off (DataDisabled, DataStorageUnavailable) a held declaration is dropped with a warning instead, since nothing is replicated. After start() resolves, a call applies immediately, and widening asks the peers for what it adds. Narrowing never deletes what is already held. Not persisted: declare it at every launch, and again after OfflineProtocol.destroy, which discards anything held.

listDocs

The documents in a space.

listSpaces

Every space that holds at least one document.

mapSet

Sets a key in a map collection.

mapDelete

Removes a key from a map collection.

mapGet

Reads a key from a map collection, or null when it is absent.

listPush

Appends to a list collection.

listDelete

Deletes entries from a list collection.

listLength

The number of entries in a list collection.

textInsert

Inserts into a text collection. position is a character offset, not a byte offset.

textDelete

Deletes characters from a text collection.

textValue

The contents of a text collection.

counterIncrement

Adds to a counter collection. Negative amounts subtract.

counterValue

The value of a counter collection.

docJson

The document’s current state as plain JSON. Half of the escape hatch: an app can always take its data and leave, and this half needs no knowledge of the SDK to read.

exportRaw

The document’s full history, base64-encoded. The other half of the escape hatch. Prefer docJson unless the history itself is what you need.

flush

Persists pending edits to a document and pushes them to the space. Edits are neither stored nor replicated until this runs, so call it on a short throttle while the user is editing (at most about twice a second), and whenever the app must know a change survives a crash. The SDK also flushes on shutdown.

flushAll

flush for every open document.

docSize

The compacted size of a document, in bytes.

wipeAll

Deletes every record the data layer owns. Only needed when documents were pointed at a storage backend the app supplied: wipePersistedState() clears the default provider’s account directory, which a custom backend is not inside. Skipping it there leaves documents behind after the account that made them is gone. Only durable once replication has stopped. A wipe records no removals, because a logout has to leave a custom backend empty, so a peer cannot tell a wiped space from one this device has never seen, and with the engine running and sessions live its next version offer recreates and refills every document, with no error and no event. Logout tears the engine down anyway; call destroy() first if you are wiping for any other reason, and only for as long as it stays stopped: the peer still holds the documents, so they return when replication resumes. This clears the device; removeSpace is what clears the room.

attachmentHash

The address of some bytes, in the spelling a reference uses. Write the result into a document with mapSet, which takes the value as an object and encodes it for you:
Compute it here rather than anywhere else. Two spellings of one address are two addresses: they fetch twice, store twice, and compare unequal while naming identical bytes.

fetchAttachment

Asks the peer a 1:1 space is named after for the bytes behind a reference. Pull rather than push, because a space may reference more bytes than a phone wants over Bluetooth: the decision to spend that is yours, per blob, when somebody opens one. The answer arrives later as data_attachment_received or data_attachment_unavailable, never from this call. Group spaces are refused in this version: a blob rides a transfer to a confirmed 1:1 session, and two group members need not have one.

provideAttachment

Answers a peer’s data_attachment_requested with the bytes. Rejects bytes that do not hash to hash, so a mistake reaches you while you still have the file in hand rather than travelling the whole media path to be refused on the other side.

declineAttachment

Tells a peer their request will not be answered. Answer this way when you no longer hold the bytes. It is a real answer rather than silence: a reference outlives the bytes it names, and without this the asking side cannot tell a peer that lost the file from one that is merely slow, so it shows somebody a spinner that never resolves.

MeshServices

registerService

Registers a local service that this node offers for discovery by other peers.

unregisterService

Unregisters a local service. returns: true if the service was found and removed

discoverServices

Broadcasts a service discovery query to the mesh. Responses arrive asynchronously as service_discovered events. returns: Query ID for correlating responses

sendServiceRequest

Sends a service request to a specific provider peer. The response arrives as a service_response_received event. returns: Request ID for correlating the response

respondToServiceRequest

Responds to a service request from another peer. Call this after receiving a service_request_received event. returns: Message ID of the response

Android mesh wake

Register the Android headless wake task at application entry, before registering the root component. The native wake configuration and Android service permissions must also be configured; see the lifecycle guide.

Removed APIs

The learned-route APIs and ProtocolConfig.path were removed in v0.23.0. Telemetry sink/polling APIs were replaced in v0.26.0. Follow the upgrade guide; do not copy older signatures into a v0.28.0 integration.