diff --git a/docs/rfcs/0014-contacts-api.md b/docs/rfcs/0014-contacts-api.md new file mode 100644 index 00000000..788a9d94 --- /dev/null +++ b/docs/rfcs/0014-contacts-api.md @@ -0,0 +1,160 @@ +# RFC-0014: Contacts API + +| | | +| --------------- | --------------------------------------------------------------- | +| **Start Date** | 2026-04-17 | +| **Description** | Expose the user's contact list to products via TrUAPI | +| **Authors** | Filippo Vecchiato | + +## Summary + +Products can read the user's host-managed address book. Each contact pairs local metadata with a context-scoped map keyed by `ProductAccountId` (`DotNsIdentifier` + `DerivationIndex`) — the same namespace used for Ring VRF alias derivation. By default a product only sees entries for its own context; cross-context access is a separate privilege. + +## Motivation + +Our privacy model gives each user a different alias and account in each product context, so no single handle identifies a person across products. A contact list matters because it is the most convenient way for a user to maintain a private notebook of mappings — local name ("Alice") to whichever identifier represents her in each product. + +The host already manages an address book, but does not expose it to products. Without this API, products cannot leverage the user's social circle to provide useful features — users must paste raw keys or scan QR codes for every interaction. Think of it like Spotify connecting to your Facebook friends, or WhatsApp reading your phone's contact list: letting products see the user's contacts (with permission) unlocks a class of social features that are otherwise impossible. + +Exposing the contact list: + +1. **Unlocks social features** — products can use the host's contact list to show who among the user's contacts is relevant in their context (e.g. "friends who also use this app"), without the user re-entering information. +2. **Per-product views of shared contacts** — multiple products see the same contact through their own context lens, each resolving to the appropriate alias and account for that product. +3. **Lets users navigate contextual identities** — a contact has different aliases and accounts per DotNS context; the API lets users see and navigate these mappings while preserving unlinkability across products. + +## Detailed Design + +### Data Model + +Each host already has its own contact schema (e.g. desktop uses `P2PPeer { type, accountId, name }`, mobile uses `Chat.Contact { accountId, username, ... }`). This RFC does not replace those internal schemas — it defines the product-facing API shape that hosts translate their internal data into. + +```rust +type ContactContext = ProductAccountId; // (DotNsIdentifier, DerivationIndex) + +struct ContextContactInfo { + alias: Option>, + account_id: Option +} + +struct LocalContactInfo { + display_name: Option +} + +struct Contact { + local: LocalContactInfo, + entries: Map +} +``` + +`ContactContext` is a `ProductAccountId` (`DotNsIdentifier` + `DerivationIndex`). The `DerivationIndex` is needed since there can be multiple derivations for a given account. The host derives the `[u8; 32]` Ring VRF context by hashing this identifier internally — note that the Ring VRF context type (`[u8; 32]`) differs from `ProductAccountId` in format; the conversion is a host-internal concern. + +`ContextContactInfo` fields are optional; either or both may be present. + +### Access Tiers + +#### Tier 1: Own-context (default) + +The host filters `entries` to only the requesting product's `ProductAccountId`. `LocalContactInfo` is always included. The product sees only identifiers scoped to its own context — it cannot learn the user's aliases or accounts in other products. + +Note: while a product already knows the *current user's own* alias in its context, it does not know the aliases of *other users* in that same context. Tier 1 reveals those peer aliases within the product's context only, which is the minimum needed for social features like showing "friends who also use this app." + +#### Tier 2: Cross-context (privileged) + +Returns the full `entries` map. Required for host-privileged products that aggregate identities across contexts (e.g. Browse, profile, honour). The host MAY grant implicit tier 2 access to built-in host products that need it for their core function (e.g. a contact management UI). + +### API + +```rust +enum ContactsErr { + NotConnected, // user has no active session + Rejected, // user denied the permission prompt + Unknown(GenericErr) +} + +fn host_contacts_get( + context: Option +) -> Result, ContactsErr>; + +fn host_contacts_subscribe( + callback: fn(Vec) +) -> Result; +``` + +Both require authentication (RFC-0009). The host prompts for permission before returning. `host_contacts_subscribe` delivers the full filtered list on each callback; hosts MAY debounce. + +When `context` is `None`, the host uses the calling product's own `DotNsIdentifier` (tier 1). When `context` is `Some(identifier)` and matches the calling product, it is equivalent to `None` (tier 1). When `context` names a different product, the host requires `DevicePermission::ContactsCrossContext` (tier 2) and filters entries to that product's context. + +This API returns only contacts the user has explicitly saved in their address book. It is not a global name resolution service — resolving arbitrary accounts to DotNS names is a separate concern (on-chain DotNS lookup). + +### Permission Model + +Extends `DevicePermission` from RFC-0002 with two new variants. These are defined here rather than amending RFC-0002, since they are specific to this feature. Hosts add them to their existing `DevicePermission` enum. + +```rust +enum DevicePermission { + // ... existing variants from RFC-0002 ... + Contacts, + ContactsCrossContext +} +``` + +| Permission | Tier | Grants | +|-----------|------|--------| +| `Contacts` | 1 | Own-context entries + local info | +| `ContactsCrossContext` | 2 | Full entries across all contexts | + +The tier 2 prompt SHOULD warn that the product can correlate contacts across contexts. `ContactsCrossContext` implies `Contacts`. + +### Example + +``` +Product ("voting.dot", 0) calls host_contacts_get(): + +→ Host checks DevicePermission::Contacts grant +→ Host filters each contact's entries to key ("voting.dot", 0) +→ Returns: + [ + Contact { + local: { display_name: "Alice" }, + entries: { ("voting.dot", 0): { alias: 0xab.., account_id: 0x12.. } } + }, + Contact { + local: { display_name: "Bob" }, + entries: {} // Bob has no entry in ("voting.dot", 0) context + } + ] +``` + +### Privacy-Preserving Display + +The host can render a contact picker in a privileged overlay using full contact data, returning only the selected contact's own-context entry to the product. This lets users see rich details without the product receiving cross-context data. The overlay mechanism is host-specific and out of scope. + +## Drawbacks + +- **Privacy surface.** Even tier 1 reveals the user's social graph. The permission prompt mitigates but does not eliminate this. +- **Full-list delivery.** No per-contact queries. The overlay pattern partially addresses this for picker UIs. +- **Read-only.** Products cannot add contacts. Deferred intentionally. + +## Alternatives + +### A: Freeform context keys instead of ProductAccountId + +Context keys could be arbitrary strings chosen by each product (e.g. `"my-app-v2"`). This would lose alignment with Ring VRF contexts — there would be no canonical key for "this product's view of a contact," and products could invent colliding keys. Using `ProductAccountId` keeps context keys deterministic and tied to DotNS identity, which the host already understands. + +### B: Per-contact lookup by alias instead of full list + +An API like `host_contact_lookup(alias: Vec) -> Option` would require the product to already know a contact's alias before looking them up, which defeats the discovery use case. The core scenario — "show me which of my contacts are relevant here" — requires browsing the full list. A lookup API could complement the list API but cannot replace it. + +### C: No context scoping — return all entries to all products + +The simplest approach: every product gets every contact's full `entries` map. This breaks unlinkability — a malicious product could correlate aliases across all contexts to learn which contacts the user interacts with in other products, building a cross-product social graph the user never consented to share. The two-tier model preserves unlinkability by default while still allowing privileged products (with explicit permission) to access cross-context data. + +## Unresolved Questions + +1. **How do contacts enter the address book?** This RFC is read-only. The mechanism by which contacts are added (peer discovery, QR scan, manual entry, chat history) is host-specific and not specified here. A follow-up RFC should define a product-facing write API. +2. **Honour.** Needs a protected path so UAs can display honour without exposing the alias to the product. Whether honour is per-product or universal (or both) needs design. Likely a separate RFC. +3. **Common triage contexts.** Should well-known contexts (profile, honour) have a lighter permission model? +4. **Contact mutation.** Write access deferred to a follow-up RFC. +5. **Filtered subscriptions.** Should tier 2 `host_contacts_subscribe` accept a context filter? +6. **Overlay specification.** The exact overlay mechanism needs its own spec. +7. **Pagination.** May be needed for large contact lists — full-list delivery could become a performance concern as address books grow. diff --git a/docs/rfcs/_index.md b/docs/rfcs/_index.md index 2e478a63..bc6b43f1 100644 --- a/docs/rfcs/_index.md +++ b/docs/rfcs/_index.md @@ -17,3 +17,4 @@ created: 2026-03-13 | 0008 | [Statement Store Host API v0.2](0008-statement-store.md) | accepted | @johnthecat | [#118](https://github.com/paritytech/triangle-js-sdks/pull/118) | | 0009 | [Unauthenticated Product Access](0009-unauthenticated-product-access.md) | accepted | @filvecchiato | [#128](https://github.com/paritytech/triangle-js-sdks/pull/128) | | 0010 | [Root account access Host API](0010-get-root-account.md) | accepted | @johnthecat | [#126](https://github.com/paritytech/triangle-js-sdks/pull/126) | +| 0014 | [Contacts API](0014-contacts-api.md) | draft | @filvecchiato | [#137](https://github.com/paritytech/triangle-js-sdks/pull/137) |