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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ scripts/battery.sh run the generated battery against both headless CLI h
bindings, and the `ios-swift` job compiles the package and its test target on
pull requests touching `ios/`, `Package.swift`, the `Makefile` or `native*`,
which is what catches a hand-written conformer that missed a new protocol
requirement. `TrUAPIHost.kt` and the embedding apps are compiled by neither.
requirement. `TrUAPIHost.kt` and the embedding apps are compiled by neither;
run `make android-check` after touching the Kotlin surface.
Hosts implement `HostBridge`, whose protocol extension defaults the optional
callbacks; `TrUAPIHostRuntime` and `TrUAPIHostCore` both accept one.
To publish the binary, include `@parity/ios-host <version>`
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Run `make help` for the list of targets.

.DEFAULT_GOAL := help
.PHONY: help setup build codegen test check clean playground wasm wasm-crypto-test uniffi uniffi-kotlin ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-bootstrap dev-link-check e2e-dotli e2e-signing-cli e2e-pairing-cli headless install matrix explorer xcframework
.PHONY: help setup build codegen test check clean playground wasm wasm-crypto-test uniffi uniffi-kotlin android-check ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-bootstrap dev-link-check e2e-dotli e2e-signing-cli e2e-pairing-cli e2e-chat-cli headless install matrix explorer xcframework

CARGO ?= cargo
TRUAPI_PKG := js/packages/truapi
Expand Down Expand Up @@ -206,6 +206,9 @@ android-jni: ## Cross-compile libtruapi_server.so for Android ABIs into jniLibs
-o $(ANDROID_JNILIBS) \
build --release -p truapi-server --features ws-bridge

android-check: uniffi-kotlin ## Compile the Kotlin host adapter against freshly generated bindings (needs Gradle + Android SDK).
gradle :truapi-host:compileReleaseKotlin

android-publish-local: uniffi-kotlin ## Generate Kotlin bindings, then publish the AAR to ~/.m2 (needs Gradle + JDK 17). The AAR does not bundle the cdylib; consumers build it per ABI (see android-jni).
gradle :truapi-host:publishReleasePublicationToMavenLocal

Expand Down Expand Up @@ -344,6 +347,9 @@ e2e-signing-cli: ## Run the generated battery against the direct signing-host CL
e2e-pairing-cli: ## Run the generated battery against the paired pairing-host CLI.
scripts/battery.sh --pairing-host

e2e-chat-cli: ## Run the Chat content-screening battery against a chat signing-host CLI.
scripts/battery.sh --chat-host

matrix: ## Regenerate the host compatibility matrix from explorer/diagnosis-reports.
cd $(EXPLORER) && npm run generate-matrix

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ js/packages/
(embedded smoldot light client + remote WebSocket RPC)
js/container/ TS lockdown container for the iOS host web view; bundles into
ios/truapi-host/Sources/TrUAPIHost/Resources/truapi-container.js
android/truapi-host/ Kotlin host adapter package over the truapi-server UniFFI core
android/truapi-host/ Kotlin host adapter package over the truapi-server UniFFI core;
compiled by no CI job, so run `make android-check` after changing it
android/truapi-provider/ truapi-provider-android: chain transport AAR (bindings + cdylib)
ios/truapi-host/ Swift host adapter package over the truapi-server UniFFI core
ios/truapi-provider/ TrUAPIProvider Swift package: chain transport over UniFFI
Expand Down Expand Up @@ -168,6 +169,7 @@ scripts/battery.sh --signing-host # direct phase only
scripts/battery.sh --pairing-host # paired phase only
make e2e-signing-cli # same direct signing-host phase
make e2e-pairing-cli # same paired pairing-host phase
make e2e-chat-cli # chat content screening against a chat signing-host
```

To run the playground locally:
Expand Down
78 changes: 78 additions & 0 deletions android/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,80 @@ The public surface lives in [`src/main/kotlin/io/parity/truapi/TrUAPIHost.kt`](s
- `HostCoreStorage` - core-owned read/write/clear interface for auth session, pairing identity, and persisted permission decisions (`key` is a SCALE-encoded `CoreStorageKey`).
- `TrUAPIHostCore` - owning wrapper around the UniFFI-generated `NativeTrUApiCore`. Holds the bridge alive for the lifetime of the core and exposes the localhost WebSocket bridge, core-owned disconnect, local-session activation, permission-authorization status, and native change notifications for session storage, theme, and preimage updates.
- `LocalhostBridgeBootstrap` - JS snippet that publishes the WS bridge endpoint (`window.__truapi_localhost`) to the product page so it can dial back in.
- `TrUAPIHostRuntime` - process-owned runtime whose product executions share one authentication session. Open a connection per executable with `openProductExecution`, which returns a `TrUAPIProductExecution` carrying that connection's own WS bridge, permission authorization, theme/preimage/chain notifications, and the Chat controls below.
- `ChatHostBridge` - native Chat storage and UI, implemented by hosts that serve the Chat modality and passed to `openProductExecution`. Hosts without it pass nothing and Chat calls answer unsupported.

## Chat

A host serving the Chat modality implements `ChatHostBridge` (`createRoom`, `registerBot`, `postMessage`, `listRooms`) and opens the execution with `ProductExecutionKind.CHAT`:

```kotlin
import io.parity.truapi.*
import uniffi.truapi.ChatBotRegistrationStatus
import uniffi.truapi.ChatMessageContent
import uniffi.truapi.ChatRoom
import uniffi.truapi.ChatRoomParticipation
import uniffi.truapi.ChatRoomRegistrationStatus
import uniffi.truapi_server.HostRejection

// Called from a shared dispatch pool, so the backing store must be
// thread-safe, and a slow call here stalls other product executions.
class MyChatBridge(private val store: ChatStore) : ChatHostBridge {
override fun createRoom(roomId: String, name: String, icon: String) =
if (store.putRoom(roomId, name, icon)) ChatRoomRegistrationStatus.NEW
else ChatRoomRegistrationStatus.EXISTS

override fun registerBot(botId: String, name: String, icon: String) =
if (store.putBot(botId, name, icon)) ChatBotRegistrationStatus.NEW
else ChatBotRegistrationStatus.EXISTS

override fun postMessage(roomId: String, content: ChatMessageContent): String {
if (content is ChatMessageContent.File) {
// Declining a variant is how a host opts out of rendering one.
throw HostRejection.Rejected("this host cannot render file cards")
}
return store.append(roomId, content)
}

override fun listRooms(): List<ChatRoom> = store.rooms()
}

val runtime = TrUAPIHostRuntime(
bridge = bridge,
runtimeConfig = HostRuntimeConfig(
hostName = "My Chat Host",
peopleChainGenesisHash = peopleChainGenesisHash, // exactly 32 bytes
bulletinChainGenesisHash = bulletinChainGenesisHash,
),
)
// Chat needs an active session; without one every Chat call answers `Denied`.
runtime.activateLocalSession(secret)

val execution = runtime.openProductExecution(
bridge = bridge,
configuration = ProductExecutionConfig("chat.dot", ProductExecutionKind.CHAT),
chat = MyChatBridge(store),
)
val endpoint = execution.startWsBridge()
webView.evaluateJavascript(
LocalhostBridgeBootstrap.script(endpoint.port, endpoint.token),
null,
)
```

Chat requires an active session: `openProductExecution` succeeds without one,
but every Chat call then answers `Denied` until `activateLocalSession` or SSO
pairing completes.

The core bounds and screens the product-supplied fields it forwards — ids,
names, icons, message bodies, URLs, and the action and media counts. Ids and
names are also normalized; a message body is bounded and screened but passed
through byte-for-byte, and `ChatFile.size_bytes` is product-asserted and
unverified. Contextual output escaping is the host's job.

`postMessage` receives any `ChatMessageContent` variant; throw from it for one this host cannot render. The id it returns is the correlation key `ActionTrigger.messageId` carries back, so it must name that message for as long as the host stores it.

On the execution: `publishChatAction` delivers a user's action back to the product (buffered until it subscribes), `notifyChatRoomsChanged` republishes the room list, `renderCustomMessage` returns a `Flow` of typed UI for a stored custom message, and `sessionChatIdentityKey` reads the session's X25519 chat identity key.

## Architecture

Expand Down Expand Up @@ -358,3 +432,7 @@ the generator. The `codegen` profile is required because uniffi-bindgen scans
the cdylib's exported metadata symbols, which the `release` profile strips — a
plain `--release` build produces a stripped library and no bindings. (`make
uniffi` regenerates the Swift bindings; use `make uniffi-kotlin` for Android.)

No CI job compiles this package. After changing `TrUAPIHost.kt` or the UniFFI
surface it wraps, run `make android-check` locally — it regenerates the Kotlin
bindings and compiles the module against them.
6 changes: 4 additions & 2 deletions android/truapi-host/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ android {
dependencies {
// UniFFI Kotlin bindings use JNA for FFI.
api("net.java.dev.jna:jna:5.14.0@aar")
// UniFFI async functions and callbacks use cancellable continuations and jobs.
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// UniFFI async functions and callbacks use cancellable continuations and
// jobs, and `TrUAPIProductExecution.renderCustomMessage` returns a `Flow`,
// so consumers compile against this.
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

// Coordinates for the local Maven publication (`publishToMavenLocal`).
Expand Down
Loading