From 1310d77cd62b80a2cc8d68d62948afb06e46c846 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Thu, 23 Apr 2026 13:05:05 +0200 Subject: [PATCH 1/3] RFC-0014: Contacts API --- docs/rfcs/0014-contacts-api.md | 150 +++++++++++++++++++++++++++++++++ docs/rfcs/_index.md | 1 + 2 files changed, 151 insertions(+) create mode 100644 docs/rfcs/0014-contacts-api.md diff --git a/docs/rfcs/0014-contacts-api.md b/docs/rfcs/0014-contacts-api.md new file mode 100644 index 00000000..148b97c1 --- /dev/null +++ b/docs/rfcs/0014-contacts-api.md @@ -0,0 +1,150 @@ +# RFC-0014: Contacts API + +| | | +| --------------- | --------------------------------------------------------------- | +| **Start Date** | 2026-04-17 | +| **Description** | Expose the user's contact list to products via Host API | +| **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 + +Products need to resolve human-readable identities to accounts. The host manages an address book but does not expose it. Without this API, users must paste raw keys or scan QR codes for every interaction. + +Exposing the contact list: + +1. **Removes friction** — products show names instead of raw addresses. +2. **Enables cross-product identity** — multiple products resolve the same contact within their respective contexts. +3. **Preserves user control** — the host gates access and filters responses to the requesting product's scope. +4. **Supports contextual accounts** — a contact has different aliases and accounts per DotNS context, preserving unlinkability. + +## Detailed Design + +### Data Model + +```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 same tuple used for Ring VRF alias derivation. The host derives the `[u8; 32]` Ring VRF context by hashing this identifier internally. + +`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. This is safe because the product could already derive this information through its own alias system. + +#### 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, + Rejected, + Unknown(GenericErr) +} + +fn host_contacts_get() -> 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. + +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: + +```rust +enum DevicePermission { + // ... existing variants ... + 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 + +Loses alignment with Ring VRF contexts and makes scoping ambiguous. + +### B: Per-contact lookup by alias + +Requires knowing the alias upfront; does not support browsing. + +### C: No context scoping + +Breaks unlinkability — any product could correlate aliases across all contexts. + +## Unresolved Questions + +1. **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. +2. **Common triage contexts.** Should well-known contexts (profile, honour) have a lighter permission model? +3. **Contact mutation.** Write access deferred to a follow-up RFC. +4. **Filtered subscriptions.** Should tier 2 `host_contacts_subscribe` accept a context filter? +5. **Overlay specification.** The exact overlay mechanism needs its own spec. +6. **Pagination.** May be needed for large contact lists. 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) | From 69ac4ca4c23105f4148ade3651c98728c3fe3e51 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Thu, 30 Apr 2026 09:12:33 +0200 Subject: [PATCH 2/3] Address review feedback on RFC-0014 Contacts API - Rewrite motivation: frame around privacy model (alias-per-context), contact list as private mapping notebook, social circle analogy - Replace generic bullet points with concrete value propositions - Note that hosts already have internal contact schemas - Clarify ContactContext/Ring VRF type relationship and DerivationIndex need - Fix tier 1 safety explanation - Add context: Option parameter to host_contacts_get - Flesh out alternatives with clear rationale - Add unresolved question about contact ingestion - Expand pagination concern --- docs/rfcs/0014-contacts-api.md | 50 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/docs/rfcs/0014-contacts-api.md b/docs/rfcs/0014-contacts-api.md index 148b97c1..9a6e2f44 100644 --- a/docs/rfcs/0014-contacts-api.md +++ b/docs/rfcs/0014-contacts-api.md @@ -3,7 +3,7 @@ | | | | --------------- | --------------------------------------------------------------- | | **Start Date** | 2026-04-17 | -| **Description** | Expose the user's contact list to products via Host API | +| **Description** | Expose the user's contact list to products via TrUAPI | | **Authors** | Filippo Vecchiato | ## Summary @@ -12,19 +12,22 @@ Products can read the user's host-managed address book. Each contact pairs local ## Motivation -Products need to resolve human-readable identities to accounts. The host manages an address book but does not expose it. Without this API, users must paste raw keys or scan QR codes for every interaction. +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. **Removes friction** — products show names instead of raw addresses. -2. **Enables cross-product identity** — multiple products resolve the same contact within their respective contexts. -3. **Preserves user control** — the host gates access and filters responses to the requesting product's scope. -4. **Supports contextual accounts** — a contact has different aliases and accounts per DotNS context, preserving unlinkability. +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) @@ -43,7 +46,7 @@ struct Contact { } ``` -`ContactContext` is a `ProductAccountId` (`DotNsIdentifier` + `DerivationIndex`) — the same tuple used for Ring VRF alias derivation. The host derives the `[u8; 32]` Ring VRF context by hashing this identifier internally. +`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. @@ -51,7 +54,7 @@ struct Contact { #### Tier 1: Own-context (default) -The host filters `entries` to only the requesting product's `ProductAccountId`. `LocalContactInfo` is always included. This is safe because the product could already derive this information through its own alias system. +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. #### Tier 2: Cross-context (privileged) @@ -66,7 +69,9 @@ enum ContactsErr { Unknown(GenericErr) } -fn host_contacts_get() -> Result, ContactsErr>; +fn host_contacts_get( + context: Option +) -> Result, ContactsErr>; fn host_contacts_subscribe( callback: fn(Vec) @@ -75,6 +80,8 @@ fn host_contacts_subscribe( 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 @@ -128,23 +135,24 @@ The host can render a contact picker in a privileged overlay using full contact ## Alternatives -### A: Freeform context keys +### A: Freeform context keys instead of ProductAccountId -Loses alignment with Ring VRF contexts and makes scoping ambiguous. +Using arbitrary strings as context keys would lose alignment with Ring VRF contexts and make scoping ambiguous — there would be no canonical key for "this product's view of a contact." -### B: Per-contact lookup by alias +### B: Per-contact lookup by alias instead of full list -Requires knowing the alias upfront; does not support browsing. +An API that takes an alias and returns the matching contact would require the product to already know the alias, which defeats the discovery use case. Products need to browse the contact list, not just resolve known identifiers. -### C: No context scoping +### C: No context scoping — return all entries to all products -Breaks unlinkability — any product could correlate aliases across all contexts. +Simpler, but breaks unlinkability. Any product could correlate aliases across all contexts, learning which contacts the user interacts with in other products. ## Unresolved Questions -1. **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. -2. **Common triage contexts.** Should well-known contexts (profile, honour) have a lighter permission model? -3. **Contact mutation.** Write access deferred to a follow-up RFC. -4. **Filtered subscriptions.** Should tier 2 `host_contacts_subscribe` accept a context filter? -5. **Overlay specification.** The exact overlay mechanism needs its own spec. -6. **Pagination.** May be needed for large contact lists. +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. From e1424db1c98316840ec96e6488437726c73fb6f8 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Thu, 7 May 2026 15:54:26 +0200 Subject: [PATCH 3/3] Address review feedback from triangle-js-sdks#137 - Clarify that new DevicePermission variants are defined in this RFC, not as an amendment to RFC-0002 (@Imod7) - Document when each ContactsErr variant is returned - Explain what tier 1 reveals vs what a product already knows: the product knows its own user's alias but not other users' aliases in the same context (@BigTava) - Flesh out alternatives with concrete examples and threat model so trade-offs are easier to evaluate (@BigTava) --- docs/rfcs/0014-contacts-api.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/rfcs/0014-contacts-api.md b/docs/rfcs/0014-contacts-api.md index 9a6e2f44..788a9d94 100644 --- a/docs/rfcs/0014-contacts-api.md +++ b/docs/rfcs/0014-contacts-api.md @@ -56,6 +56,8 @@ struct Contact { 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). @@ -64,8 +66,8 @@ Returns the full `entries` map. Required for host-privileged products that aggre ```rust enum ContactsErr { - NotConnected, - Rejected, + NotConnected, // user has no active session + Rejected, // user denied the permission prompt Unknown(GenericErr) } @@ -86,11 +88,11 @@ This API returns only contacts the user has explicitly saved in their address bo ### Permission Model -Extends `DevicePermission` from RFC-0002 with two new variants: +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 ... + // ... existing variants from RFC-0002 ... Contacts, ContactsCrossContext } @@ -137,15 +139,15 @@ The host can render a contact picker in a privileged overlay using full contact ### A: Freeform context keys instead of ProductAccountId -Using arbitrary strings as context keys would lose alignment with Ring VRF contexts and make scoping ambiguous — there would be no canonical key for "this product's view of a contact." +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 that takes an alias and returns the matching contact would require the product to already know the alias, which defeats the discovery use case. Products need to browse the contact list, not just resolve known identifiers. +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 -Simpler, but breaks unlinkability. Any product could correlate aliases across all contexts, learning which contacts the user interacts with in other 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