Appearance
Identity (Rust)
Your app works with cryptographic identities — real people and devices, not usernames and passwords. Reach the identity API through app.identity().
Listing identities
identity().list() () -> Vec<Identity>
List the identities available to your app — own identities and contacts (remote identities you've been introduced to). Own identities have privkey: true; the key itself stays in wish-core.
rust
for id in app.identity().list()? {
let kind = if id.privkey { "(own)" } else { "(contact)" };
println!("{} {}", id.name.as_deref().unwrap_or("?"), kind);
}Identity:
| Field | Type | Description |
|---|---|---|
uid | Vec<u8> | Identity UID (32 bytes) — stable, never changes |
name | Option<String> | Display name |
privkey | bool | true for own identities, false for contacts |
Getting identity details
identity().get(uid) (uid: &[u8]) -> IdentityFull
Get full details for an identity — signers, contacts, and hosts.
rust
let identity = app.identity().get(&uid)?;
for host in &identity.hosts {
println!(" {}", host.name.as_deref().unwrap_or("unnamed"));
}IdentityFull adds:
| Field | Type | Description |
|---|---|---|
signers | Vec<Signer> | Cryptographic keys that can act as this identity — { signer_id, name? } |
contacts | Vec<Vec<u8>> | Contact UIDs (own identities only) |
hosts | Vec<IdentityHost> | Known devices — { host_id, name?, role?, transports, .. } |
Signing and verification
identity.sign / identity.verify are not namespaced in wish-sdk-rs yet — call the endpoints directly:
rust
let signed: ciborium::value::Value =
app.rpc().request("identity.sign", sign_args)?;
let result: ciborium::value::Value =
app.rpc().request("identity.verify", vec![signed_document])?;The claim passed to identity.sign is an arbitrary byte string that becomes part of the signed statement.
Asking the admin app for help
An app never creates identities, adds contacts or joins devices itself — the user does that in Wish, the admin app — the one app that holds the identities' private keys and decides which apps may use them. What an app can do is ask the admin app to help the user with one of those tasks. That is admin.request. There is no typed wrapper yet; call it through app.rpc().
admin.request (kind: string, args?: map) -> int
Send a request to every admin app. Returns the request id. The kernel keeps the request until an admin app answers it (see admin.response) and forwards it as an admin.request signal — also to admin apps that connect after it was sent (Wish lists unanswered requests on start-up), so it's safe to send before Wish is running.
kind names the admin API family the admin app should drive, and args scopes it. The kernel treats both as opaque: it stores kind and forwards args verbatim. The kinds Wish understands:
kind | args | What Wish does |
|---|---|---|
identity | { contacts?: "all" | "none" } — access hint | Asks the user which identity the app should use (existing, new, or joined from another device) and grants it. The hint proposes the contact visibility to grant by default; the user confirms and can tighten it. Absent hint → identity only, no contacts. |
identity.contact | { uid } | Opens the People view for uid with contact discovery ready (nearby peers, directory search). For "find people" onboarding steps. |
identity.join | { uid } | Opens the People view on uid with the join another device flow (join mode) ready. |
Every kind except identity is scoped to an identity the app already holds — args.uid is required (byte string), and the admin app rejects the request if it's missing. The naming rule: a kind mirrors the admin endpoint family it lands in (identity, identity.contact.*, identity.join.*).
rust
use ciborium::value::Value;
// First run: no identity yet — ask for one, proposing access to all its contacts.
if !app.identity().list()?.iter().any(|id| id.privkey) {
app.rpc().request::<Value>(
"admin.request",
Value::Array(vec![
Value::Text("identity".into()),
Value::Map(vec![(Value::Text("contacts".into()), Value::Text("all".into()))]),
]),
)?;
}
// Later, from an onboarding hint in the app's own UI:
app.rpc().request::<Value>(
"admin.request",
Value::Array(vec![
Value::Text("identity.contact".into()),
Value::Map(vec![(Value::Text("uid".into()), Value::Bytes(me.uid.clone()))]),
]),
)?;Reacting. Don't block on the request id — the admin app answers when the user is done, which may be never. Instead re-fetch identity().list() on the identity signal (a grant, a new contact and a new host all fire it), and optionally watch the admin.response signal ({ id, requestType, success, message? }) to learn the request was closed.
Unknown kinds are shown to the user as such and declined. Only the request/response contract above is stable; Wish's UI is not.
Reacting to changes
The identity signal fires whenever identities change — new identity created, contact added, name updated. Subscribe via app.signals() and re-fetch app.identity().list() to stay current.