Contact Catalog¶
Contact Catalog is a node-attached domain catalog for opt-in private contact
discovery. It lets a user publish a route candidate for an external contact
handle, such as an email address or phone number, without turning Seed Directory
into a people directory and without publishing raw phone/email -> participant
maps.
Status: hard-MVP done (the broader solution remains partial for
post-MVP privacy/federation expansion)
Date: 2026-05-16
Hard-MVP tracker status: 100% implemented. All Must Implement
capabilities in the Solution 025 sidecar and all Proposal 058 tracker rows are
done; remaining work named in this document is explicitly post-MVP hardening.
Executive Summary¶
Contact Catalog answers one narrow question:
Given a contact handle I already know, is there a safe Orbiplex route candidate
for requesting contact with the handle controller?
The MVP profile is intentionally conservative:
authenticated caller
-> invitation-only lookup
-> route candidate
-> contact-request.v1 over Artifact Delivery / INAC
-> recipient-local consent
-> capability-passport.v1 for messaging-receive
The catalog owns contact-domain admission and lookup policy. Seed Directory only
discovers Contact Catalog providers through the contact-catalog capability.
The Node daemon does not learn phone or email semantics; it supervises the
middleware, exposes host capabilities, and transports artifacts through existing
planes.
The solution-level Seed Directory boundary is defined separately in
doc/project/60-solutions/031-seed-directory/031-seed-directory.md.
Context and Problem Statement¶
Proposal 058 defines Contact Catalog as a domain catalog separate from Seed Directory. Story 010 uses it to let Daniel message Marcin by an email address Daniel already knows.
The implementation must preserve three boundaries:
- local address-book data stays local and may contain raw handles;
- Contact Catalog records contain opt-in claims, lookup indexes, route candidates, expiry, revocation, and admission evidence;
- Seed Directory advertises providers and capability evidence, not people.
Must Implement¶
Contact Claim Admission¶
Based on:
doc/project/40-proposals/058-contact-catalog.mddoc/project/30-stories/story-010-message-to-a-friend.md
Related schemas:
contact-claim.v1capability-passport.v1
Responsibilities:
- accept
contact-claim.v1only after validating contact-control evidence; - require
email-controlorphone-controlpassport freshness for email and phone handles; - reject raw handle publication in catalog records under the MVP profile;
- enforce purpose allowlists, expiry, revocation reference shape, and monotonic
sequence/no; - store admitted claims through
CatalogStore<ContactClaimRecord>rather than a bespoke storage layer.
Status:
done— Nodecontact-catalog-corevalidates canonical route-setcontact-claim.v1, verifies participant/delegated participant signatures, rejects node-only signatures, evaluatesemail-control@v1/phone-control@v1passports, and checks passport signature, expiry, profile match, and revocation freshness before admission. It also owns shared lookup request/result DTOs and the normalized lookup-index helper used by consumer-side code. Normalized lookup indexes are versioned and domain-separated by contact kind before hashing, soemail,phone, and future handle kinds do not share a raw digest namespace.
Invitation-Only Lookup¶
Based on:
doc/project/40-proposals/058-contact-catalog.mddoc/project/60-solutions/023-artifact-delivery/023-artifact-delivery.md
Related schemas:
contact-lookup-result.v1routing-subject-binding.v1
Responsibilities:
- expose
/v1/contact-catalog/lookupor an equivalent supervised middleware endpoint undercatalog_kind = "contact"; - require authenticated callers and rate limits before lookup execution;
- return
contact-lookup-result.v1as a route-set candidate or invitation-required result; - never return raw
participant:did:keyby default; - use opaque
contact-route:<digest>invitation refs when the local contactability draft is participant-routed; - emit no-match audit entries without storing or exposing raw queried handles.
- support host-composed Artifact Delivery
selector/kind = "contact-lookup"without making Contact Catalog an AD payload resolver. In that integration, AD passesselector/purpose = "contact-request/messaging"to express that lookup is being used for a messaging contact request, not for full message delivery.
Status:
done— Nodecontact-catalog-serviceexposes public invitation-only, blinded-digest and PSI-modePOST /v1/contact-catalog/lookups, returnscontact-lookup-result.v1withresult/routes[], optionalroute/idmirrors,routing-subject/contact-nym/invitation-route/noderoute candidates, andselected/route, rate limits by client fingerprint + digest + purpose, rejects raw handle-like lookup inputs, writes redacted lookup audit without raw query values or root participant ids, and exposes redacted counters/recent policy events in service status. Nodecontact-catalog-clientis the provider-free consumer adapter: it performs bounded HTTP lookups and parses canonicalcontact-lookup-result.v1without owning provider admission, provider sync, or storage. The daemon uses that client for ADcontact-lookupand exposes the same consumer path as host capabilitycontact.lookup, so a node may consume Contact Catalog providers even when it does not runcontact-catalog-service. The daemon owns an opt-in supervised runtime on stable loopback and a/v1/contact-catalog/statusproxy; a process smoke starts the real service binary and verifies readiness plus projection status through that proxy.
Contact Request Admission¶
Based on:
doc/project/30-stories/story-010-message-to-a-friend.mddoc/project/40-proposals/057-user-and-operator-notifications.mddoc/project/60-solutions/017-inter-node-artifact-channel/017-inter-node-artifact-channel.mddoc/project/60-solutions/023-artifact-delivery/023-artifact-delivery.md
Related schemas:
contact-request.v1notification.v1notification-action.v1capability-passport.v1
Responsibilities:
- register exactly one authoritative acceptor for
contact-request.v1; - turn accepted contact requests into durable user notifications with accept and reject actions;
- on acceptance, issue a narrow
messaging-receivecapability passport scoped to the accepted sender, recipient route, public handle reference, operation, expiry, and revocation reference; - on rejection, record the local decision without minting authority.
Status:
done for hard-MVP— The daemon registers a default in-process Artifact Delivery acceptor targetcontact.request, persistscontact-request.v1state, creates durablecontact-request.receivednotifications, exposes host-owned accept/reject actions, and issuesmessaging-receive@v1passports on acceptance. Validation tests cover real participant signatures, expiry, bad purpose, bad signature, and redacted notification wording. Story 010 strict smoke exercises the operator notification accept path end-to-end; broader failure-matrix AD accept/reject tests remain post-MVP hardening.
Local Contact Store¶
Ownership note (cross-reference to Solution 032): With the introduction of the Local Relationship Layer (Solution 032), semantic relationship state — classes, memberships, relationship predicates, and pairwise relationship facts — moves to Solution 032. Raw handles, UX labels, and local address-book records stay in the daemon Local Contact Store. Contact Catalog retains only public discovery / route-set lookup state. The expected flow becomes:
Catalog lookup (public discovery) → Local Relationship annotation (private) → consumer policy (passport-gated).
local-contact.v1schema is preserved unchanged. Its recovery bundle remains sealed through the Pseudonym Vault, whilelocal-relationshipvault entries carry relationship facts. Relationship classes such asfriendsare not a Catalog concept; Catalog never sees them.
Based on:
doc/project/40-proposals/058-contact-catalog.mddoc/project/30-stories/story-010-message-to-a-friend.mddoc/project/60-solutions/032-local-relationship-layer/032-local-relationship-layer.md
Related schemas:
local-contact.v1
Responsibilities:
- store raw external handles, labels, and local address-book mappings locally;
- keep the store unpublished by default;
- persist user-level continuity data needed to restore local contacts after node recovery;
- expose only redacted or digest-bound references to network artifacts.
Solution 032 does not make Contact Catalog the owner of private relationships. It only moves relationship classes, membership facts, predicates, and pairwise relationship facts out of Messaging/AD/Catalog into the Local Relationship Layer.
Status:
done for hard-MVP— The daemon owns<node-data-dir>/storage/local-contacts.sqliteand exposes local create/list/get/patch/archive routes. Records now carry compatiblelabel, explicitlabels[],metadata {}, UX/provenance fields, and an active pairwise contact-nym pointer. The store also maintainslocal_contact_pairwise_mappingswithactive,rotated,revoked, andarchivedlifecycle states so nym rotation is history-preserving. Raw handles remain daemon-local and are not emitted by Contact Catalog lookup, Seed Directory records, or shared lookup audit. Durable local recovery mirror records exist for messaging recovery; the local contact store can export and replay local-contact, pairwise-mapping, and messaging-recovery mirror records without reactivating revoked or archived pairwise mappings.pseudonym-vault.v1can carry a sealedlocal-contact-recovery-bundle.v1; vault import replays that bundle, and daemon startup replays the latest operational-vault-key sealed vault snapshot. Legacy root-only snapshots are migration/recovery inputs, not the normal hot-path profile. Explicit operator replay/export hooks acceptlocal_passphraseforroot+local-passphrasevaults without opening those vaults during unattended startup or weakening their wrap profile.
May Implement¶
Federated Contact Catalog Peer Fetch¶
Based on:
doc/project/40-proposals/058-contact-catalog.md
Related schemas:
contact-claim.v1
Responsibilities:
- use
ObservedCatalogStore<ContactClaimRecord>only for communities that explicitly accept a private peer/fetch federation model; - never expose a public anonymous dump of the contact catalog; ordinary clients use authenticated lookup, while bulk/list fetch is local-operator or trusted-provider sync only;
- let the host/daemon own discovery, trust policy, passport validation, auth material, and revocation freshness;
- use direct catalog-to-catalog HTTP as the data plane only after host policy has marked the provider trusted;
- implement reusable fetch/merge/replay mechanics in the generic catalog abstraction, while this service owns Contact Catalog runtime and privacy policy;
- preserve publisher and origin provenance;
- forbid raw contact handles, unsalted public hashes, and public-log-style contact projections in federated records;
- keep Agora out of the Contact Catalog propagation path.
Status:
done for hard-MVP—node/catalog::CatalogAdapter<T, F>now has a genericObservedRecord<T>fetch contract and Contact Catalog definesRemoteContactClaimFilter;contact-catalog-serviceuses aRemoteContactCatalogHttpAdapterto refresh trusted provider claims into a sidecar remote cache.node/catalognow also carries the genericsync_catalog_provider(...)mechanic for fetch, observed-record validation, loop guardrails, self-origin rejection when the daemon provides the local node id, sequence-aware merge, data-derived high-water metadata and counted outcomes. Contact Catalog exposes authenticatedGET /v1/contact-catalog/sync/claimsas the trusted provider snapshot contract and records provider sync state in the sidecar with last-run counts and cumulative totals. Provider tombstones are now part of the trusted-provider snapshot body and are cached durably; replay/restart hides remote claims whose sequence is covered by a cached tombstone, and sync status records a retryable/terminal failure class. Provider policy remains trusted-only for sync, but operators can now inspect discovered providers and settrusted,uncertain, orblockedfrom/admin/contact-catalogover the service policy endpoint. No Agora publication/relay path is introduced. Provider trust changes are durably audited with actor, a required reason capped at 1000 characters, previous/next state, passport hash and endpoint. Local tombstone facts are produced for operator-tombstoned and projection-revoked claims, exported throughtombstones[], and applied on active-only read paths after restart. Broader production federation matrices remain post-MVP hardening.
Blinded or PSI Lookup¶
Based on:
doc/project/40-proposals/058-contact-catalog.md
Related schemas:
contact-lookup-result.v1
Responsibilities:
- replace or supplement public invitation-only digest lookup with stronger private discovery profiles once the cryptographic protocol is selected;
- keep the result artifact stable so clients do not depend on the lookup protocol internals.
Status:
done for hard-MVP—orbiplex-node-crypto::contact_psiprovides auditable Ristretto255 DH-PSI primitives, and the Contact Catalog service acceptsblinded-digestandpsilookup modes with mode/index pairing, redacted audit, rate limiting and route-set responses. Hard-MVP PSI usesPOST /v1/contact-catalog/psi/evaluatefor bounded evaluated elements and candidate tokens, thenPOST /v1/contact-catalog/lookupswithlookup_mode = "psi"pluspsi/match-token. The service-local PSI server seed is stored as plain base64url in the service SQLite database; the hard-MVP security boundary is the local data-dir filesystem, and the seed is not emitted through status, audit or sync. Broader PSI failure matrices remain focused lower-level tests rather than a separate top-level E2E harness; Story-010 strictad-smokeremains the E2E gate.
Out of Scope¶
- becoming a global address book;
- storing raw personal address books in a network catalog;
- treating phone or email control as legal identity assurance;
- issuing general friend-class capabilities;
- owning messaging UI or mailbox storage beyond the contact-request acceptor;
- making Seed Directory contact-aware.
Consumes¶
contact-claim.v1contact-request.v1capability-passport.v1routing-subject-binding.v1- Seed Directory capability provider records for
contact-catalog,email-control, andphone-control
Contact Catalog provider discovery is capability-first. The daemon asks Seed
Directory for providers advertising the contact-catalog capability and applies
the same local multi-directory query policy used by Artifact Delivery and other
discovery consumers. Contact Catalog remains the domain lookup service after a
provider is selected; Seed Directory does not become contact-aware and does not
authorize message delivery by itself.
Produces¶
contact-lookup-result.v1- notification records for
messaging.contact-request capability-passport.v1undermessaging-receive- local audit facts about admission, lookup, no-match, acceptance, and rejection
Notes¶
The default MVP path remains invitation-only digest lookup, and the runtime also
accepts blinded-digest and psi lookup modes for claims indexed with the
matching private mode. This keeps the common path simple while letting stricter
privacy mechanics evolve at the lookup edge without changing the route-candidate
contract.
Concrete crate/module ownership belongs in the Node implementation repository.
The implementation is a Rust supervised channel_json middleware with a separately
owned bounded Contact Catalog product HTTP service. It
links orbiplex-node-catalog, implements ContactClaimRecord: CatalogRecord,
and stores data under a middleware-owned SQLite database such as
<node-data-dir>/storage/contact-catalog.sqlite.