feat(contacts): add Contacts gRPC vertical (proto, daemon, CLI, E2E) #218

Merged
KofTwentyTwo merged 1 commits from story/m2-contacts-api into dev 2026-07-29 23:23:32 +00:00
1 Commits
Author SHA1 Message Date
James Maes abf6c9fb11 feat(contacts): add Contacts gRPC vertical (proto, daemon, CLI, E2E)
Publish the M2-S1 contacts engine over the daemon's authenticated gRPC
API, mirroring the Calendar vertical: add nuncio.v1.Contacts (Sync,
ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/
ContactPhone messages to the proto contract and regenerate the golden
descriptor.

Wire ContactsEngineOverrides + ContactsGrpcService into nunciod,
mounted behind its own BearerAuthInterceptor exactly like every other
service. ListContacts/GetContact always read genuinely persisted data
from DatabaseEngine; Sync fetches from an injected ContactsBackend
(MockContactsBackend in tests) and persists via
contacts_sync::sync_with_backend, mirroring calendar_sync.rs; with no
backend injected, production Sync returns an honest error since
AccountConfig carries no per-account CardDAV configuration yet.

Add connect_contacts/AuthenticatedContactsClient to nuncio-proto's
shared client helpers. Replace the CLI's Commands::Contact handlers
(previously a disposable in-memory ContactsDatabase.in_memory() per
invocation) with real gRPC calls against the daemon, so a contact
added via `contact add` persists and is visible to a later, separate
CLI invocation.

Deviations from the issue's literal proto sketch, flagged for review:
- Added CreateContact (not in the issue's message list): the
  acceptance criteria requires `contact add` to persist a locally-
  authored contact across CLI invocations, but the daemon has no
  per-account CardDAV credential to write back to a real server. This
  RPC only ever writes to the daemon's own local store -- never a
  fabricated CardDAV write-back -- which is the same honest-surface
  spirit as Calendar's "no create/delete" note (that note is about not
  pretending to write to the remote server, not about never
  persisting locally-authored data at all).
- ListContacts/CreateContact take a required account_id (the issue
  sketched a bare ListContactsRequest{}), because the real
  DatabaseEngine::list_contacts/save_contact are account-scoped;
  ContactSubcommand::List/Search/Add/Sync now all take --account to
  match.
- Contact search has no server-side RPC; `contact search` fetches the
  full ListContacts result and filters client-side by display name,
  organization, and email.
- Contact's created_at/updated_at/last_interacted_at are omitted from
  the proto message -- no client use case identified for them yet.

Full-daemon offline E2E (contacts_e2e_test.rs) proves Sync -> List/Get
over the authenticated API, CreateContact's persistence surviving a
second, independent client connection, and an unauthenticated call
rejected with Code::Unauthenticated.
2026-07-29 18:14:17 -05:00