openrtc 2.9.8

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
# OpenRTC Rust Core

The `openrtc` crate is the shared realtime engine behind OpenRTC's native and
browser/WASM runtimes.

```toml
[dependencies]
openrtc = "2.5.0"
```

Rust 1.91 or newer is required. The default feature set is intentionally small;
enable only the native transports your host ships:

```toml
openrtc = { version = "2.5.0", features = ["transport-lan"] }
```

It powers:
- the native runtime used by Tauri and other native hosts
- the WASM runtime loaded by the npm package in browsers

## Core Responsibilities
- app-scoped device discovery
- presence publishing and cleanup
- signaling messages and sessions
- room membership
- peer identity resolution
- iroh endpoint lifecycle and connection management
- shared runtime records used by both native and WASM hosts

## Native vs WASM

### Native
Used for:
- Tauri desktop/mobile-style hosts
- headless or CLI-style runners
- advanced endpoint and router integration

Native-only features include:
- endpoint adoption/export
- custom router / no-internal-router mode
- native auth relay helpers
- protocol registry and plugin registration

### WASM
The same runtime logic compiles to WebAssembly for browser hosts.

The npm package loads that WASM build through `new WasmClient(apiKey)` and
keeps assertion exchange, browser key storage, and capability handles in the
provider-neutral TypeScript layer. `setIdentityCredential()` forwards only the
OpenRTC-issued device credential into Rust admission. WASM never accepts an
OpenRTC Firebase project or App Check token. The generated WASM package
does not expose the old `(projectId, appTag)` constructors or a rollback
feature.

## OpenRTC Native Shape

The default constructor accepts only the public developer API key. It validates
and derives the app identity locally, performs no network work, and configures
no Firebase project:

```rust
use openrtc::client::Client;

let client = Client::new(
    "pk_test_0000000000000000000000000000000000000000".into(),
)?;
```

Backend-free pure-Rust prototypes use the high-level, network-idle control
plane rather than implementing HTTP grants or encoding gateway avenue strings:

```rust
use std::sync::Arc;
use openrtc::native::{
    CapabilityOptions,
    ControlPlane,
    DeviceSigner,
};

// `signer` keeps one per-install Ed25519 private key in host secure storage.
let rtc = ControlPlane::anonymous(
    "pk_test_0000000000000000000000000000000000000000",
    Arc::new(signer) as Arc<dyn DeviceSigner>,
)?;
let room = rtc.join_room(
    "match-123",
    "desktop",
    CapabilityOptions::default(),
).await?;
// Install room.signaling() in the runtime, then explicitly retire the avenue.
room.close().await;
```

`join_space`, `join_room`, and `issue_ticket` create no Firebase Auth user and
no monthly-active-principal event. The awaited join issues one bounded
capability plus its avenue grant. A browser request is origin-allowlisted;
originless native HTTP instead requires the same signed install proof and is
bounded by replay, rate, provider-risk, and developer spend ceilings.

Authenticated pure-Rust applications can use `ControlPlane` instead
of implementing grant HTTP calls themselves. The host supplies four narrow,
provider-neutral pieces: an OIDC assertion provider, a secure-storage Ed25519
signer, protected device-certificate storage, and optional enrollment-time
attestation. OpenRTC performs assertion exchange, enrollment, proof signing,
certificate renewal, and one-hour in-place grant refresh. Firebase project
configuration and App Check are never crate inputs.

The assertion provider also owns a monotonic login epoch. Increment it for
login, logout, account switch, or explicit revocation--not for ordinary OAuth
access-token refresh. Implement `subscribe_identity_epoch()` with a Tokio
`watch::Receiver` when the host can provide immediate notifications. OpenRTC
then coalesces assertion exchange/certificate lookup inside one confirmed
epoch and closes the old capability promptly when the epoch changes. A fresh
process still verifies the consumer assertion once before it trusts a stored
device certificate's principal.

Long-running native processes that are constructed before login can install
the returned capability into `SignalingSlot` exactly once and pass a
`CredentialRelay` provider to
`Client::builder`. Replacing an active capability
requires closing and rebuilding it; the slot cannot become a second reconnect
or presence owner.

Capability activation supplies an avenue-scoped
`NativeCoordinationGatewaySignaling` backend whose grant provider owns the
control-plane exchange. Wrap it in `NativeCapabilityHandle`: one handle owns one
devices, space, room, or ticket avenue, refreshes its one-hour OpenRTC grant in
place, and stops when `close()` runs or the handle is dropped. Creating the
handle is still network-idle; the first runtime presence publication activates
its socket. The host keeps consumer authentication, attestation registration,
and its per-install Ed25519 private key in platform-secure storage.

Use `ClientBuilder::new_provider_neutral` only for internal composition where
the already-derived app tag is available. Use the runtime manager and protocol
registry for advanced native transport composition.

## Peer Sessions and Custom Protocols

The Rust core owns one peer-session view per `deviceId`. Host layers should
consume connected peer-session snapshots rather than reason about transport
records directly.

Protocol authors have two supported extension paths:

1. **Session-adjacent stream composition**
   - wait for a connected peer session
   - open `open_bi` / `open_uni` streams
   - define framing and message semantics externally

2. **ALPN protocol plugins**
   - register protocol plugins through `RuntimeManager` / `ProtocolRegistry`
   - use `SessionAdjacent` plugins for runtime-ready hooks without ALPN routing
   - use `Alpn` plugins when the native runtime should register a dedicated ALPN

WASM/browser consumers should prefer the first model. The second is a
Rust-first advanced capability.

### Native protected streams for one ticket

The 2.9.8 source candidate adds `Client::open_peer_scoped_protected_bi` and
`Client::accept_scoped_protected_bi`. These native methods reuse the existing
peer, application encryption, named envelope, and transport generation. They
do not create another endpoint or reconnect owner.

Open a stream with the already admitted scope, such as `ticket:<ticket-id>`.
Write the existing `stream_metadata::encode_envelope` with that same scope in
`metadata.scope`, then write the product's own frames. Pass incoming streams
from the host's single `incoming_streams()` reader to
`accept_scoped_protected_bi`; it consumes only the named envelope and returns
`ScopedIncomingStream` with the protected send/receive halves and channel
metadata. Product handlers still validate their channel, ticket ID, and
application permissions before processing payloads.

The SDK bounds envelope intake to 8 KiB and ten seconds, requires current
admission for the selected scope, and preserves already inspected ciphertext
and unread product bytes. Adding or withdrawing a sibling ticket does not
revoke this stream. Withdrawing its selected scope, changing its authority,
key or confirmed physical generation, or retiring its client denies further
I/O, including buffered plaintext. A denied handle stays denied. Existing
unscoped protected-stream methods retain their original admission semantics.

These APIs are native only. The candidate version is not a claim of registry
publication; use a published version that contains them for normal consumers.

## Important Rule
Rust owns discovery, signaling, presence, lifecycle, and peer identity. TypeScript should wrap those contracts, not duplicate them.

See the [lifecycle ownership contract](https://github.com/bluestarburst/openrtc/blob/main/docs/architecture/lifecycle-ownership-contract.md)
and [application security and delivery contract](https://github.com/bluestarburst/openrtc/blob/main/docs/architecture/application-security-and-delivery-contract.md)
before adding host retry, connection, admission, or delivery behavior.

## Native IP exclusions for overlay hosts (candidate)

The opt-in `native-ip-exclusions` feature uses the public pinned OpenRTC Iroh
fork, not stock crates.io Iroh. Cargo does not propagate dependency patches:
standalone hosts must supply the exact four `iroh`, `iroh-base`, `iroh-dns` and
`iroh-relay` overrides documented in `docs/operations/dependency-tracking.md`.
The candidate revision is `2bf4c5ae8ab068e3d680d12f041e56a8e41c3042`.
The public fork carries the reviewed Noq/Proto and HTTP sources with upstream
notices. This remains a source candidate; normal consumer VPN lanes stay
disabled until SDK publication and complete shipping qualification.

Supply parsed `ipnet::IpNet` networks with
`ClientBuilder::native_ip_exclusions` before initialization, or await
`Client::set_native_ip_exclusions` on the existing runtime before creating OS
overlay routes. Retain the input until owned route removal is verified. A
cancelled update is not a commit acknowledgement. Explicit requests fail closed
without the feature; stock Iroh cannot compile an enabled request surface.

`Client::supports_native_ip_exclusions` reports native UDP/candidate filtering
only. Relay/custom DNS, TCP and ICE exclusions are not covered. This does not
authorize OS changes or claim a shipping VPN/complete underlay guarantee.
OpenRTC retains the single endpoint, admission, selection and recovery owners;
the host owns TUN/route effects and their cleanup.

## Native IP UDP socket initializer (candidate)

With `native-udp-socket-initializer`, a trusted native host can supply
`ClientBuilder::native_udp_socket_initializer` before SDK-owned endpoint
construction. Its bounded callback borrows the actual unused IP transport UDP
socket and returns an I/O result. The constructor passes it to the existing
Iroh/Netwatch socket factory; it runs before first bind and every rebind. A
refusal propagates before endpoint/node publication, including optional bindings.
There is no live retrofit API. Requested input refuses adoption of an already-
bound host endpoint; unspecified input preserves existing host adoption behavior.
An already registered SDK endpoint remains the same immutable owner.

This does not configure DNS, relay TCP/QUIC, portmapper, custom carrier or HTTP
sockets, grant OS permission, or constitute complete native underlay protection.
The callback must perform only bounded local mechanism work and must not choose
peers, admission, routes, retries or connection lifetimes. Socket protection
errors require the underlying socket owner to discard the new socket.

The source cohort uses Iroh revision
`2bf4c5ae8ab068e3d680d12f041e56a8e41c3042` plus unchanged upstream net-tools
`87ad510e0749a5c21ce736478d1369449200da44`, whose retained Netwatch configurator
is not yet in registry 0.19.3. Root patches are not inherited by a packaged
consumer. No publication or full protected-socket capability is implied by
this candidate API; keep product VPN activation disabled until the complete
standalone graph, all required socket factories and OS effects are qualified.


## Native relay TCP socket initializer (candidate)

The default-off `native-relay-tcp-socket-initializer` feature adds
`ClientBuilder::native_relay_tcp_socket_initializer`. A bounded trusted host
callback borrows `client::NativeTcpSocketRef` (Unix `AsFd`, Windows `AsSocket`)
and returns an I/O result. The SDK passes it into its existing endpoint
constructor. Iroh retains it through the relay actor and configures each actual
WebSocket relay or HTTP CONNECT proxy TCP socket before connect. A policy
refusal ends that dial; ordinary address/family fallback cannot bypass it.

Explicit input refuses adoption of an already-bound external endpoint without
closing that owner's resource. Unspecified input preserves ordinary adoption;
an already registered SDK endpoint keeps its identity. This installs no peer,
incoming reader, route selector, reconnect loop or SDK credential in an OS helper.

The source cohort pins all four Iroh packages to
`2bf4c5ae8ab068e3d680d12f041e56a8e41c3042`, with the existing upstream Netwatch
and SDK Noq/Proto patches. Root patches are not inherited by packaged consumers.
This input excludes DNS, relay HTTPS latency probes, QUIC relay, portmapper,
coordination gateway and custom-carrier sockets. These factories and a complete
published-only graph still need qualification before product VPN activation.
The 2.9.8 candidate remains unpublished; old signed artifacts do not prove this
source.


## Native coordination TCP socket initializer (candidate)

The default-off `native-gateway-socket-initializer` feature adds
`native::NativeGatewaySocketInitializer` and
`NativeCapabilities::with_native_gateway_socket_initializer`. A bounded trusted
host callback borrows each fresh Tokio `TcpSocket` before bind/connect. The
existing coordination actor retains it across reconnects; policy refusal is
terminal and cannot fall through to another address or retry. Unspecified
input retains the original WebSocket dial path. TLS/SNI and WebSocket framing
remain owned by tokio-tungstenite.

`ControlPlane::with_native_gateway_socket_initializer` carries the input to
device, authority-room and anonymous capability constructors. Configure it
before sharing/cloning the control plane. This does not protect the control
plane's Reqwest HTTP client or Tokio/system DNS. It does not change peer
lifecycle, admission, grant refresh, presence or bounded reconnect ownership.

Real loopback tests cover authenticated owned-device publication, reconnect on
a new protected descriptor, and refusal without TCP acceptance or retry. A
separate copied SDK with forwarding omitted fails both wire tests for the
intended reasons. The Rust CI job explicitly enables these default-off tests.

A separate isolated Linux consumer composes this input with Proxy's existing
root helper. UID 54321 with zero effective capabilities completes publication
and reconnect through original helper-marked TCP descriptors under kernel
rejection of unmarked traffic. Omitting only the factory callback in a copied
SDK fails actual publication and records kernel rejection. Both lanes restore
exact firewall/IPv4 state after normal helper Stop; the preserved positive
binary remains unchanged and passes after the control. Receipt:
`docs/testing/evidence/2026-10-13-native-gateway-sdk-helper-adoption.json`.

Managed WSS/enrollment, installed OS helpers and independent devices remain
unqualified. Candidate 2.9.8 remains unpublished; ordinary Proxy uses 2.9.7
with VPN disabled.


## Shared gateway DNS resolver (candidate)

The default-off `native-gateway-dns-resolver` feature includes the gateway TCP
and endpoint DNS socket-policy features. After configuring the client's DNS
initializer and initializing its SDK-owned endpoint,
`Client::native_gateway_dns_resolver` returns an opaque clone of that endpoint's
resolver. It refuses clients without constructor DNS policy and clients whose
endpoint is not initialized. The handle retains the same cache, reset, TLS and
socket configuration; it creates no second resolver or lifecycle owner.

Attach the handle through `NativeGatewaySocketInitializer::with_dns_resolver`
before passing that initializer to NativeCapabilities or an unshared
ControlPlane. Gateway hostname lookup uses the shared resolver. IP literals
create no DNS traffic. A typed DNS socket refusal, including nested family
errors, becomes a terminal gateway error before any TCP descriptor is created;
the existing adapter cannot fall through to system DNS or retry around it.
Without this input, the prior system-resolution behavior is preserved. TLS/SNI
and WebSocket framing still receive the original URI. HTTP is separate.

Public tests cover constructor guards, actual resolver socket refusal through
owned-device publication, no subsequent TCP/retry, and successful literal-IP
authentication/reconnect with the shared input. Removing only resolver
retention in a separate SDK source copy falls back to system DNS retries and
fails the unchanged refusal test. Coordination owner suite: 44 passed. This
does not qualify named-host WSS/root-helper composition or managed enrollment;
previous TCP/helper and endpoint DNS/helper receipts remain separate and pinned
to their earlier source. Candidate 2.9.8 is unpublished; VPN stays disabled.

### Published feature metadata and native policy source activation

The native policy features are SDK capability markers. They deliberately do
not forward unpublished dependency feature names: Cargo validates those names
when packaging even if the SDK feature is inactive. The ordinary registry
package remains resolvable with upstream Iroh/Reqwest.

A native VPN consumer must select the reviewed public fork cohort and activate
its dependency policy features explicitly. The current public source cohort is
`2bf4c5ae8ab068e3d680d12f041e56a8e41c3042`; all four Iroh pins must match.
For example, alongside OpenRTC's SDK marker features, the consumer declares:

```toml
[dependencies]
iroh = { version = "=1.2.0", default-features = false, features = [
  "tls-ring", "native-udp-socket-initializer",
  "native-dns-socket-initializer", "native-tcp-socket-initializer",
] }
```

An SDK-workspace command may select dependency features explicitly, such as
`--features native-gateway-dns-resolver,iroh/native-dns-socket-initializer`.
None of these markers promises support with unpatched registry dependencies;
unsupported source compositions fail compilation rather than bypassing policy.