@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 chainingonce
Registers a one-time event listener returns: This instance for chainingremoveAllListeners
Removes all listeners for a specific event type, or all listeners if no type specified returns: This instance for chainingstart
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
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)
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
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 sendforwardMessage
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 failssendConnectionRequest
Sends a connection requestparams.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 sendrejectConnectionRequest
Rejects a connection request returns: Message ID throws: Error if rejection fails to sendcancelConnectionRequest
Cancels a previously sent connection request returns: Message ID throws: Error if cancellation fails to sendgetActiveTransports
Gets the list of active transports returns: Array of active transport type namesenableTransport
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 disableisBluetoothEnabled
Checks if Bluetooth is enabled on the device returns: True if Bluetooth is enabled, false otherwiserequestEnableBluetooth
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 otherwisegetTopology
Gets the current network topology returns: Network topology snapshot including nodes, links, and stats throws: Error if topology retrieval failsgetMessageStats
Gets message delivery statistics returns: Array of message delivery statistics throws: Error if stats retrieval failsgetDeliverySuccessRate
Gets the delivery success rate returns: Success rate as a number between 0 and 1 throws: Error if retrieval failsgetMedianLatency
Gets the median message delivery latency returns: Median latency in milliseconds, or null if no data available throws: Error if retrieval failsgetMedianHops
Gets the median hop count for delivered messages returns: Median hop count, or null if no data available throws: Error if retrieval failssendMedia
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 progresssendFile
Sends a generic file to a recipient (convenience for sendMedia with ContentType.File). returns: File ID for tracking progresssendImage
Sends an image to a recipient. returns: File ID for tracking progresssendVoiceNote
Sends a voice note to a recipient. returns: File ID for tracking progresssendVideoNote
Sends a video note to a recipient. returns: File ID for tracking progresssendVideo
Sends a video to a recipient. returns: File ID for tracking progressgetFileProgress
Gets the progress of a file transfer returns: File progress information, or null if not foundcancelFileTransfer
Cancels an active file transfer returns: True if cancelled, false if not foundreceiveMessage
Polls for the next received message returns: Message object if available, null otherwise throws: Error if polling failspause
Pauses the protocol (for background mode) throws: Error if protocol is not running or fails to pauseresume
Resumes the protocol from pause throws: Error if protocol is not paused or fails to resumegetState
Gets the current protocol state returns: Protocol state (Stopped, Running, or Paused) throws: Error if retrieval failssetBatteryLevel
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. SeesetBatteryLevel 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 onegetIsCharging
Gets the last reported charging state (false if none reported).setRelayPriority
Sets the relay priority. A shorthand for updating onlyrelayPriority; 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 priorityupdateRelayConfig
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: seesetBatteryState.
getRelayConfig
Gets the current relay configuration.isRelay
Checks if this device is currently acting as a relay returns: True if device is a relaygetBLePeerCount
Gets the current number of discovered BLE peers returns: Number of BLE peers currently trackedgetBleDiagnostics
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.
getTransportMetrics
Gets detailed metrics for a specific transport returns: Transport metrics or null if not availableenableTelemetry
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 theapiKey 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, ornull 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; useflushTelemetry 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. Resolvesnull 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 failsreleaseTransportLock
Releases the transport lock and lets DORS make decisions againupdateDorsConfig
Updates DORS configuration at runtime. Omitted fields keep their current values: the same partial-update contract asupdateRelayConfig. 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 configurationupdateAckConfig
Updates ACK configuration at runtime throws: Error if update failsupdateRetryConfig
Updates retry configuration at runtime throws: Error if update failsupdateDedupConfig
Updates deduplication configuration at runtime throws: Error if update failsgetDedupStats
Gets deduplicator statistics for monitoring returns: Deduplication statisticsgetMeshRelayStats
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 isawaitingTransmission, 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 tocreate(): 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’swipeAll() 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 ACKsgetRetryQueueSize
Gets the current retry queue size returns: Number of messages in retry queueshouldEscalateToWifi
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 recommendedprocessFileChunk
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 provedsenderId.
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 provedrecipientId; 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 unprovenpeerId 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 forpeerId 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 eitherinternetConfirmSent(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 WebSocketsend() 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 WebSocketsend() 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 bystart() 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 usegetIdentityPublicKey
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 initializedderiveAddress
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 beforecreate(), 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. Requirestransports.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 requirescoldContactEnabled). 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 afterstart().InvalidArgument: not a claimable username (empty, over 64 bytes once normalized, carrying a control or format character, or address-shaped).
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: UsederiveAddress. 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 initializedverifySignature
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 inputmlsGenerateKeyPackage
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 failsmlsGetOrCreateKeyPackage
Gets an existing key package or creates a new one. returns: Key package throws: Error if operation failsmlsGetPendingKeyPackages
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 withmlsGenerateKeyPackage.
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 failsmlsImportKeyPackage
Imports another user’s key package. Required before you can send encrypted messages to them. throws: Error if import failsmlsHasSession
Checks if an MLS session exists with another user. returns: True if session existshasPendingKeyPackage
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 availablegetEstablishmentState
Returns the current secure-session establishment state for a peer. Useful for retry/UI flows when operations fail withSessionNotReady.
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
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 usingestablishSecureSession 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 failsmlsEncryptForUser
Encrypts a message for another user. Creates a session automatically if one doesn’t exist. returns: Encrypted message throws: Error if encryption failsmlsDecryptFromUser
Decrypts a message from another user. returns: Decrypted plaintext as bytes, or null if decryption failsmlsDecrypt
Decrypts any MLS message (1:1 or group). returns: Decrypted plaintext as bytes, or null if decryption failsmlsListSessions
Lists all active MLS sessions. returns: Array of user IDs with active sessionsmlsDeleteSession
Deletes an MLS session with another user. throws: Error if deletion failsmlsProcessWelcome
Processes a Welcome message (auto-detects session vs group). returns: Session or group info throws: Error if processing failsmeshCreateGroup
Creates a new MLS group with full protocol integration. Emits GroupCreated event. Use inviteToGroup() to add members. returns: Group infomeshInviteToGroup
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 IDsmeshForwardMessageToGroup
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 IDsmeshRemoveFromGroup
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 IDsmeshGetGroupInfo
Gets information about an MLS group. returns: Group info or null if not foundmeshGroupRichReadiness
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 asgroup_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-sendCreateGroup: 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 beforeCreateGroupInviteLink 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 -> rolemeshRenameGroup
Renames an MLS group (admin only). Broadcasts the rename to all group members. throws: Error if not admin or group not founddestroy
Destroys the protocol instance and cleans up resourceswipePersistedState
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, afterdestroy(). 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 IDcheckInternetPresence
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
CheckPresenceframe 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_updatedevent withsource: 'internet'(includinglast_seen_mswhen 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 nopresence_updatedfollows. Andtruemeans 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 gatesendRawServerCommand 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 questionisInternetReady 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:
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 IDsendReadReceipt
Sends a read receipt to a peer. returns: Message IDblockUser
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 IDsisUserBlocked
Checks if a specific user is blocked. returns: true if the user is blockedDataStore
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. UseremoveDoc 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 ordinarydata_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. UsefetchAttachment 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. PreferdocJson 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 withmapSet, which takes the value
as an object and encodes it for you:
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 asdata_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’sdata_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 removeddiscoverServices
Broadcasts a service discovery query to the mesh. Responses arrive asynchronously asservice_discovered events.
returns: Query ID for correlating responses
sendServiceRequest
Sends a service request to a specific provider peer. The response arrives as aservice_response_received event.
returns: Request ID for correlating the response
respondToServiceRequest
Responds to a service request from another peer. Call this after receiving aservice_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 andProtocolConfig.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.
