vtc-service 0.12.0

Service for Verifiable Trust Communities
//! Trust-registry integration surface — Phase 3 M3.1.
//!
//! Spec §8 + §13. The VTC publishes member records to the
//! configured trust registry asynchronously: every
//! `MemberAdded` / `MemberRemoved` / `RoleChanged` audit event,
//! and every `MemberUpdated` that changes `publishConsent`,
//! drives a `SyncJob` against the registry, with exponential
//! backoff and boot-time replay.
//!
//! ## Only members who consented are published
//!
//! A member is published only while their
//! [`crate::members::Member::publish_consent`] is `true` — the
//! applicant's `registryConsent` on `vtc/join-requests/submit`
//! (spec: *Consent/purpose*). An administrator may withdraw it
//! through `members/update` but never grant it
//! (`vtc/members/update:consentGrantForbidden`): consent is the
//! member's to give. The syncer reads it at dispatch time and
//! enforces it in code;
//! `registry.rego`'s `publish_on_join` can narrow publication
//! further but can never publish a member who did not consent.
//! Withdrawing consent removes the member's record from the
//! registry on the next tick. See `syncer::MembershipSyncer`
//! (`resolve`) for the full decision table.
//!
//! ## Transport split (planning outcome)
//!
//! The upstream `affinidi-trust-registry-rs` ships a server +
//! a TRQP-v2.0-compliant HTTP query surface (`POST
//! /recognition`, `POST /authorization`) **plus** a DIDComm-only
//! admin protocol for record mutations (URIs under
//! `https://affinidi.com/didcomm/protocols/tr-admin/1.0/`). The
//! upstream does **not** publish a Rust client crate.
//!
//! Phase 3 D1 originally proposed a git-dep on the upstream;
//! that turned out to mean depending on the server crate, which
//! pulls in an `affinidi-tdk = "0.4"` that conflicts with our
//! workspace's `0.7`. Decision (per user, this PR) is to write
//! an in-tree client wrapping both transports:
//!
//! - **Reads** (cross-community recognition / authorization
//!   queries) → HTTP via `reqwest`. Lands in M3.10.
//! - **Writes** (create / update / delete record) → DIDComm
//!   against the upstream's `tr-admin/1.0/*` message types.
//!   Lands in M3.2 + M3.4.
//!
//! [`TrustRegistryClient`] is the trait both transports route
//! through. M3.1 lands the trait + the in-memory
//! [`MockRegistryClient`] for tests; the live HTTP +
//! DIDComm clients land alongside their consumers.
//!
//! ## Storage shape
//!
//! Three new keyspaces (spec §13):
//!
//! - `registry_records:<member_did>` — local mirror of what
//!   the registry knows about each member. Updated when a
//!   `SyncJob` completes successfully so the daemon can detect
//!   divergence at boot.
//! - `sync_queue:<job_id>` — pending / in-flight / failed
//!   sync jobs. Drained by `MembershipSyncer` (M3.4).
//! - `sync_cursor` — singleton row tracking the audit-log
//!   tail's last-seen timestamp so a daemon restart picks up
//!   exactly where the prior run left off (M3.3).

pub mod client;
pub mod drift;
pub mod health;
pub mod messaging;
pub mod model;
pub mod policy;
pub mod storage;
pub mod syncer;
pub mod tail;
pub mod upstream;

/// The TRQP `action` every membership record and recognition query uses.
///
/// A record is only ever found by the **whole** four-part key
/// (entity+authority+action+resource), so the tuple a member record is
/// published under and the tuple `recognise()` queries with must be
/// character-identical. Two constants, read by both the messaging client and
/// the HTTP client, are what stop those drifting into a registry full of rows
/// nobody looks up — a failure with no error anywhere.
pub const RECOGNISE_ACTION: &str = "recognise";

/// The TRQP `resource` scope for the community's recognition graph. See
/// [`RECOGNISE_ACTION`].
pub const TRUST_GRAPH_RESOURCE: &str = "trust-graph";

pub use client::{
    CapabilityModuleChange, MockRegistryClient, RegistryError, RegistryTransport,
    TrustRegistryClient,
};
pub use drift::{
    Disagreement, DriftEntry, DriftSnapshot, DriftState, MAX_REPORTED as MAX_REPORTED_DRIFT,
    check as check_drift,
};
pub use health::{HealthStatus, RegistryHealth, SyncerHealth, SyncerHealthSnapshot};
pub use messaging::MessagingRegistryClient;
pub use model::{
    DEFAULT_MAX_ATTEMPTS, MAX_BACKOFF_SECONDS, RegistryRecord, RegistryStatus, SyncJob,
    SyncJobKind, SyncJobState, exponential_backoff_seconds,
};
pub use policy::{
    ClampOutcome, PublishOnJoinDecision, clamp_disposition, evaluate_publish_on_join,
    is_rtbf_purge, publish_input, read_min_disposition,
};
pub use storage::{
    REGISTRY_RECORDS_PREFIX, SYNC_QUEUE_PREFIX, clear_sync_cursor, delete_record, delete_sync_job,
    get_record, get_sync_cursor, get_sync_job, list_records, list_sync_jobs, set_sync_cursor,
    store_record, store_sync_job,
};
pub use syncer::MembershipSyncer;
pub use tail::{WalkOutcome, walk as walk_audit_tail};
pub use upstream::UpstreamRegistryClient;