diff --git a/README.md b/README.md index a9696875..fc75ac50 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -45,7 +103,7 @@ 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, }); @@ -53,15 +111,15 @@ const messageId = await protocol.sendMessage({ ### 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 @@ -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 diff --git a/bindings/python/README.md b/bindings/python/README.md index 9250eba6..35f54b42 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -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 @@ -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, @@ -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") @@ -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 @@ -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 @@ -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 @@ -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 @@ -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: @@ -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 @@ -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 @@ -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 @@ -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