Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

[![CI](https://github.com/Offline-Protocol/offline-protocol-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Offline-Protocol/offline-protocol-sdk/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@offline-protocol/mesh-sdk.svg?logo=npm)](https://www.npmjs.com/package/@offline-protocol/mesh-sdk)
[![PyPI](https://img.shields.io/pypi/v/offline-protocol-sdk.svg?logo=pypi)](https://pypi.org/project/offline-protocol-sdk/)
[![crates.io](https://img.shields.io/crates/v/offline-protocol.svg?logo=rust)](https://crates.io/crates/offline-protocol)
[![License](https://img.shields.io/badge/license-AGPL--3.0--only%20or%20Commercial-blue.svg)](#license)
[![Platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#building-the-sdk)

Expand All @@ -17,10 +19,66 @@
- **Group Roles**: Admin/member role management with last-admin safety invariants
- **DORS**: Dynamic Offline Relay Switch for optimal transport selection
- **Reliability**: ACKs, retries, and deduplication built-in
- **Cross-Platform Bindings**: React Native for iOS/Android and Python for macOS/Linux/Windows
- **Cross-Platform Bindings**: React Native for iOS/Android and Python for macOS/Linux/Windows, plus an early native Swift package and Android library

> **What you must implement:** the Rust crates are I/O-free protocol engines for everything that moves a message. They queue, route, encrypt, and select transports, but never open a socket or touch a radio for the protocol itself. A *platform bridge* does the actual I/O: it drains each transport's outbound queue, performs the send, reports the outcome, and injects inbound bytes. The React Native binding ships these bridges for iOS and Android (BLE, WiFi Direct, Internet, Nostr, Reticulum), and the Python binding ships BLE and Internet bridges. If you consume the Rust crates directly via `cargo add`, you write the bridge yourself; the contract is documented in the [`offline-protocol-transport`](crates/offline-protocol-transport/src/lib.rs) crate docs. The one socket the crates do open is the [telemetry pipe](docs/telemetry.md)'s HTTPS upload, and only once an app enables telemetry with a key.

## Install

Every channel carries the version number of the release it came from.

| Platform | Package | Install |
|----------|---------|---------|
| React Native (iOS, Android) | [`@offline-protocol/mesh-sdk`](https://www.npmjs.com/package/@offline-protocol/mesh-sdk) on npm | `npm install @offline-protocol/mesh-sdk` |
| Python (macOS, Linux, Windows) | [`offline-protocol-sdk`](https://pypi.org/project/offline-protocol-sdk/) on PyPI | `pip install offline-protocol-sdk` |
| Rust | [`offline-protocol`](https://crates.io/crates/offline-protocol) and its sibling `offline-protocol-*` crates on crates.io | `cargo add offline-protocol` |
| Swift, iOS (preview) | [`offline-protocol-swift`](https://github.com/Offline-Protocol/offline-protocol-swift), product `OfflineProtocolSDK` | Swift Package Manager, see below |
| Android, Kotlin (preview) | A download on each [GitHub release](https://github.com/Offline-Protocol/offline-protocol-sdk/releases); not on Maven Central yet | See below |

**Python.** Wheels are published for Python 3.10 or later on macOS arm64
(macOS 14 or later), Linux x86_64 and aarch64 (`manylinux_2_34`, so glibc
2.34 or later) and Windows x86_64. There is no wheel for Intel macOS, for musl
Linux such as Alpine, or for an older glibc, and no source distribution, so on
those hosts pip finds nothing to install: [build from
source](#build-python-desktop-bindings) instead. The import name is
`offline_protocol_sdk`.

**Swift (preview).** The package needs no React Native. It is iOS only (iOS
13 or later; no macOS or Mac Catalyst slice):

```swift
dependencies: [
.package(url: "https://github.com/Offline-Protocol/offline-protocol-swift.git", from: "0.28.0")
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "OfflineProtocolSDK", package: "offline-protocol-swift")
])
]
```

It is an early package. Its storage providers are not public yet, so an
application cannot construct them; wiring the transports to the engine is the
application's to write; and the public API is not a decided one, so names in
it may change. The package repository is generated by each release, so issues
and pull requests belong in this repository. See [bindings/swift](bindings/swift/README.md).

**Android (preview).** The library's coordinates are
`com.offlineprotocol:offline-protocol-sdk`, but it is not on Maven Central yet,
so a Gradle dependency on them does not resolve. Each release attaches
`offline-protocol-X.Y.Z-android.zip`, which holds the generated Kotlin bindings
(`uniffi/offline_protocol/offline_protocol.kt`, which call the library through
JNA) and the native libraries for `arm64-v8a`, `armeabi-v7a`, `x86` and
`x86_64`. The transport managers are not in it; the full library is built from
the React Native module's sources as described in
[bindings/kotlin](bindings/kotlin/README.md), and is as early as the Swift
package.

The release also attaches native libraries for Linux (x86_64, aarch64), macOS
(arm64) and Windows (x86_64), and the iOS XCFramework. Contributors, and
anyone on a platform without a published package, use [Building the
SDK](#building-the-sdk).

## Quick Start

### For React Native Apps
Expand All @@ -45,23 +103,23 @@ await protocol.initializeMlsWithSecureStorage();

// Messages are automatically encrypted!
const messageId = await protocol.sendMessage({
recipient: 'recipient456',
recipient: peerAddress, // the peer's off1… address; a username reaches nobody
content: 'Hello!', // Automatically encrypted
priority: MessagePriority.Medium,
});
```

### For Python Desktop Apps

The Python binding supports macOS, Linux, and Windows. Build and install it
from the repository:
The Python binding supports macOS, Linux, and Windows. Install it from PyPI
(the [wheel platforms](#install) are above):

```bash
cd bindings/python
bash scripts/build-desktop.sh
pip install -e .
pip install offline-protocol-sdk
```

On a host without a wheel, [build it from source](#build-python-desktop-bindings).

```python
from offline_protocol_sdk import ProtocolManager
from offline_protocol_sdk.offline_protocol import ProtocolConfig, OverflowPolicy
Expand Down Expand Up @@ -159,6 +217,7 @@ delegate here, so every path produces the whole set. Commit all three together.
```bash
cd bindings/python
bash scripts/build-desktop.sh
pip install -e .
```

The desktop build produces the native `.dylib`, `.so`, or `.dll` for the host
Expand Down
92 changes: 58 additions & 34 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,33 @@ Python bindings for the Offline Protocol SDK: offline-first mesh networking with

> **Upgrading an existing install?** Read [docs/UPGRADING.md](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/UPGRADING.md)
> first — `ProtocolManager` now requires an explicit `state_root`, and this
> release is not safely downgradable.
> release is not safely downgradable. From 0.28.0, an `InternetManager` you
> construct yourself needs the `app_id=` keyword; the one `ProtocolManager`
> builds already has it.

## Quick Start

### 1. Build the native library
### 1. Install

```bash
cd bindings/python
bash scripts/build-desktop.sh
pip install offline-protocol-sdk
```

This compiles the Rust core for your platform and generates the FFI bindings. It
regenerates Swift and Kotlin alongside Python by delegating to the repo-root
`scripts/generate-bindings.sh` — the three are one artifact set off one UDL, so
they are never refreshed apart.
Wheels are published for Python 3.10 or later on:

**Prerequisites:** Rust toolchain, `uniffi-bindgen` (`cargo install uniffi --version 0.30.0 --features cli --locked`).
| Platform | Wheel tag | Requires |
|----------|-----------|----------|
| macOS, Apple Silicon (arm64) | `macosx_14_0_arm64` | macOS 14 or later |
| Linux x86_64 | `manylinux_2_34_x86_64` | glibc 2.34 or later |
| Linux aarch64 | `manylinux_2_34_aarch64` | glibc 2.34 or later |
| Windows x86_64 | `win_amd64` | |

### 2. Install the package
Each wheel carries the native library, so there is nothing to compile. There
is no wheel for Intel macOS, for musl Linux such as Alpine, or for an older
glibc, and no source distribution, so on those hosts pip finds nothing to
install: [build from source](#building-from-source) instead.

```bash
pip install -e .
```

### 3. Use it
### 2. Use it

```python
import asyncio
Expand All @@ -41,7 +43,7 @@ from offline_protocol_sdk.offline_protocol import ProtocolConfig, OverflowPolicy

config = ProtocolConfig(
app_id="my-app",
user_id="alice",
profile="alice",
ble_enabled=False,
wifi_direct_enabled=False,
internet_enabled=True,
Expand All @@ -59,6 +61,9 @@ config = ProtocolConfig(
overflow_policy=OverflowPolicy.DROP_OLDEST,
)

# The peer's address: its pm.local_address. A username reaches nobody.
peer_address = "off1..."

async def main():
# The installer must remove this application-owned directory on uninstall.
state_root = Path("/app/install-owned-data/offline-protocol")
Expand All @@ -70,7 +75,7 @@ async def main():
pm.internet.configure(server_url="ws://relay.example.com")
await pm.internet.start()

msg_id = pm.send_message("bob", "Hello from Python!")
msg_id = pm.send_message(peer_address, "Hello from Python!")
print(f"Sent: {msg_id}")

# Keep running to receive messages
Expand All @@ -79,6 +84,25 @@ async def main():
asyncio.run(main())
```

### Building from source

For a host without a wheel, or to work on the binding itself, build the
native library from a clone of the repository:

```bash
git clone https://github.com/Offline-Protocol/offline-protocol-sdk
cd offline-protocol-sdk/bindings/python
bash scripts/build-desktop.sh
pip install -e .
```

`build-desktop.sh` compiles the Rust core for your platform and generates the
FFI bindings. It regenerates Swift and Kotlin alongside Python by delegating to
the repo-root `scripts/generate-bindings.sh`: the three are one artifact set
off one UDL, so they are never refreshed apart.

**Prerequisites:** Rust toolchain, `uniffi-bindgen` (`cargo install uniffi --version 0.30.0 --features cli --locked`).

### Two hosts without a relay

Set `wifi_direct_enabled=True` and `ProtocolManager` builds a
Expand Down Expand Up @@ -126,7 +150,7 @@ claim by whoever answered on the segment, never a discovery, and the bridge
never registers it with the engine. Registrations that do not fit the record
(a service id over 200 bytes, a capability key DNS-SD cannot carry, a record
over 1300 bytes) are kept on the mesh and not published, with a warning. The
mapping is [docs/spec/dns-sd-mapping.md](../../docs/spec/dns-sd-mapping.md).
mapping is [docs/spec/dns-sd-mapping.md](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/spec/dns-sd-mapping.md).

## Architecture

Expand All @@ -153,8 +177,8 @@ offline_protocol_sdk/
|-----------|---------|-----------|-------|
| Internet/WebSocket | `websockets` | All | Primary transport for desktop |
| BLE | `bleak` | All | Central (scanner) role only; peripheral/GATT server requires `bless` |
| Peer stream (the `wifi_direct` slot) | `asyncio` sockets; `zeroconf` for LAN discovery (optional extra `lan`) | All | `PeerStreamManager`: TCP streams to configured `host:port` peers or hosts found over DNS-SD, each proved by the identity-assertion preamble ([spec](../../docs/spec/stream-framing.md)). Start it after `ProtocolManager.start()`; binds every interface unless `listen_host` narrows it |
| Reticulum (a gateway daemon) | `asyncio` sockets | All | `GatewayManager` as `pm.gateway` when `reticulum_enabled=True`: the [gateway-daemon contract](../../docs/spec/gateway-contract.md) over TCP to a daemon on local IP (`configure(daemon_address="localhost:4242")`, then `await pm.gateway.start()` after `pm.start()`). Attaches with a signed address declaration, settles each send on the gateway's verdict, watches presence. What answers is a daemon built to the contract; this package ships the device half. Apps driving the slot themselves replace the callback via `protocol.set_reticulum_transport_callback(...)` |
| Peer stream (the `wifi_direct` slot) | `asyncio` sockets; `zeroconf` for LAN discovery (optional extra `lan`) | All | `PeerStreamManager`: TCP streams to configured `host:port` peers or hosts found over DNS-SD, each proved by the identity-assertion preamble ([spec](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/spec/stream-framing.md)). Start it after `ProtocolManager.start()`; binds every interface unless `listen_host` narrows it |
| Reticulum (a gateway daemon) | `asyncio` sockets | All | `GatewayManager` as `pm.gateway` when `reticulum_enabled=True`: the [gateway-daemon contract](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/spec/gateway-contract.md) over TCP to a daemon on local IP (`configure(daemon_address="localhost:4242")`, then `await pm.gateway.start()` after `pm.start()`). Attaches with a signed address declaration, settles each send on the gateway's verdict, watches presence. What answers is a daemon built to the contract; this package ships the device half. Apps driving the slot themselves replace the callback via `protocol.set_reticulum_transport_callback(...)` |
| Nostr | Built-in | All | Handled in Rust core (BIP-340 signing); `ProtocolManager` wires a stub callback when `nostr_enabled=True` — apps driving Nostr themselves replace it via `protocol.set_nostr_transport_callback(...)` |

### Secure Storage
Expand Down Expand Up @@ -184,7 +208,7 @@ kwallet) for any deployment where that matters, supply your own

One process can own the engine and serve several local applications at once,
over JSON-RPC 2.0 on a WebSocket. The contract is
[the local API chapter](../../docs/spec/local-api.md); the reference server
[the local API chapter](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/spec/local-api.md); the reference server
ships in this package as `offline_protocol_sdk.local_api` and as the
`offline-protocol-service` command:

Expand Down Expand Up @@ -218,14 +242,14 @@ holds for an application whose client is away, and what it never puts on the
wire, is the chapter's. `--policy policy.json` adds the optional rules
(`spaces`, `denied`); `--tcp PORT --token-file PATH` serves loopback TCP with
a per-launch token instead of the socket. See
[the bridge contract](../../docs/bridges/local-api.md) for what the server
[the bridge contract](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/bridges/local-api.md) for what the server
owes.

The guide is [docs/local-api.md](../../docs/local-api.md). Two clients ship
The guide is [docs/local-api.md](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/local-api.md). Two clients ship
as examples and are run by the test suite against an in-process server:
[`examples/local_api_client.py`](examples/local_api_client.py) (this package's
[`examples/local_api_client.py`](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/bindings/python/examples/local_api_client.py) (this package's
`websockets` dependency, Unix socket or TCP) and
[`examples/local-api/client.mjs`](../../examples/local-api/client.mjs) at the
[`examples/local-api/client.mjs`](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/examples/local-api/client.mjs) at the
repository root (Node 22 or later, no dependencies, TCP with the token).

### Headless hosts: the built-in file stores
Expand Down Expand Up @@ -311,7 +335,7 @@ await pm.start()
`subprocess` or the `spawn` method instead.

What the stores guarantee, and what a copied directory reveals, is in the
[MLS integration guide](../../docs/mls-integration.md#built-in-file-stores).
[MLS integration guide](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/mls-integration.md#built-in-file-stores).

Restartable message-plane state is kept separately by `AppStateStorage`, outside
the credential store. The built-in stores derive an opaque account namespace
Expand Down Expand Up @@ -411,7 +435,7 @@ pm.disable_telemetry() # final flush, then stop; stop() does this too

`flush_telemetry`, `end_telemetry_session`, `set_telemetry_enabled` and
`notify_app_state` complete the surface. What leaves the device, when, and how
to switch it off are in [docs/telemetry.md](../../docs/telemetry.md).
to switch it off are in [docs/telemetry.md](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/telemetry.md).

## Running the Example

Expand All @@ -438,13 +462,13 @@ pip-audit --strict

## Platform Support

| Target | Architecture | Library |
|--------|-------------|---------|
| macOS | Apple Silicon (arm64) | `.dylib` |
| macOS | Intel (x86_64) | `.dylib` |
| Linux | x86_64 | `.so` |
| Linux | aarch64 | `.so` |
| Windows | x86_64 | `.dll` |
| Target | Architecture | Library | Wheel on PyPI |
|--------|-------------|---------|---------------|
| macOS | Apple Silicon (arm64) | `.dylib` | Yes, macOS 14 or later |
| macOS | Intel (x86_64) | `.dylib` | No, [build from source](#building-from-source) |
| Linux | x86_64 | `.so` | Yes, glibc 2.34 or later |
| Linux | aarch64 | `.so` | Yes, glibc 2.34 or later |
| Windows | x86_64 | `.dll` | Yes |

## License

Expand Down
Loading