Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ jobs:
run: |
cargo run -p xtask -- generate-schemas
git diff --exit-code schemas/
- name: Regenerate the verdict manifest and check for drift
run: |
cargo run -p xtask -- gen-contracts
git diff --exit-code schemas/contracts-v1.json
- name: Validate fixtures against schemas
run: cargo run -p xtask -- validate-schemas

Expand Down
35 changes: 34 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/auths-witness-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ thiserror.workspace = true
# Only the OS-backed `OsRng` is used (the workspace bans `thread_rng`/`random`).
rand = { workspace = true }
hex = "0.4"
# Registry sync: the workspace git2 is deliberately transportless; the node
# opts into HTTPS to fetch the parties' public `refs/auths/*` registry in-process
# (same posture as packages/auths-node). A plain `git clone` would miss that ref.
git2 = { version = "0.21.0", default-features = false, features = ["vendored-libgit2", "https", "vendored-openssl"] }
# The serve surface: hardened HTTP ingest for the anchor role (body cap,
# concurrency limit, timeout — the same posture as the KEL witness server).
axum = { version = "0.8" }
Expand Down
31 changes: 31 additions & 0 deletions crates/auths-witness-node/src/bin/witness_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ enum Command {
Serve(ServeArgs),
/// Probe a node's /health endpoint (container healthcheck entrypoint).
Healthcheck(HealthcheckArgs),
/// Fetch the parties' public registry into `--registry` (the `refs/auths/*`
/// namespace the anchor role resolves keys from). Run before `serve`, or as
/// an init step — a plain `git clone` does NOT bring these refs.
SyncRegistry(SyncRegistryArgs),
}

#[derive(Parser)]
struct SyncRegistryArgs {
/// Registry git URL to fetch the parties' public KELs from (must expose
/// `refs/auths/*`), e.g. the aggregated first-party registry.
#[arg(long, value_name = "URL")]
from: String,
/// Local registry dir to populate — the same path passed to `serve --registry`.
#[arg(long, value_name = "DIR")]
registry: PathBuf,
}

#[derive(Parser)]
Expand Down Expand Up @@ -208,6 +223,22 @@ async fn main() -> std::process::ExitCode {
std::process::ExitCode::FAILURE
}
},
Command::SyncRegistry(args) => {
match auths_witness_node::sync::sync_registry(&args.from, &args.registry) {
Ok(()) => {
eprintln!(
"witness-node: synced registry from {} → {}",
args.from,
args.registry.display()
);
std::process::ExitCode::SUCCESS
}
Err(e) => {
eprintln!("witness-node: sync-registry: {e:#}");
std::process::ExitCode::FAILURE
}
}
}
Command::Serve(args) => {
for role in &args.roles {
match role.as_str() {
Expand Down
1 change: 1 addition & 0 deletions crates/auths-witness-node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub mod registry;
pub mod signer;
pub mod sqlite_store;
pub mod standup;
pub mod sync;
pub mod vocabulary;

pub use anchor_role::{
Expand Down
52 changes: 52 additions & 0 deletions crates/auths-witness-node/src/sync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Registry sync: populate the witness's `--registry` with the parties' public
//! KELs by fetching the custom `refs/auths/*` namespace.
//!
//! The anchor role resolves a submitter's keys from the tree at
//! `refs/auths/registry` (see [`crate::registry`]). A plain `git clone` only
//! fetches `refs/heads/*` + tags, so it leaves that ref — and therefore every
//! identity — absent, which is why an operator who "cloned" the registry still
//! 422s every submission. This fetches the `refs/auths/*` namespace explicitly,
//! in-process (git2's own HTTPS transport — no `git` binary needed), mirroring
//! the SDK's `fetch_registry`.

use std::path::Path;

use crate::registry::registry_ready;

/// Fetch or refresh the parties' public registry at `registry` from `url`.
///
/// Idempotent: creates and initializes the repo if absent, then force-fetches
/// `refs/auths/*` (the namespace [`crate::registry`] reads). Confirms the sync
/// produced a resolvable registry (`refs/auths/registry` present) before
/// returning, so a wrong URL or an empty remote fails loudly here rather than
/// as a later 422 on every submission.
///
/// Args:
/// * `url`: the aggregated registry's git URL (must expose `refs/auths/*`).
/// * `registry`: the local dir the node serves with `--registry`.
///
/// Usage:
/// ```ignore
/// sync_registry("https://github.com/auths-dev/registry", Path::new("/data/registry"))?;
/// ```
pub fn sync_registry(url: &str, registry: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(registry)
.map_err(|e| anyhow::anyhow!("create registry dir {}: {e}", registry.display()))?;
let repo = git2::Repository::init(registry)
.map_err(|e| anyhow::anyhow!("init registry dir {}: {e}", registry.display()))?;
let mut remote = repo
.remote_anonymous(url)
.map_err(|e| anyhow::anyhow!("open remote {url}: {e}"))?;
// Force-fetch the custom namespace the backend reads. NOT a plain clone.
remote
.fetch(&["+refs/auths/*:refs/auths/*"], None, None)
.map_err(|e| anyhow::anyhow!("fetch refs/auths/* from {url}: {e}"))?;
drop(remote);
registry_ready(registry).map_err(|e| {
anyhow::anyhow!(
"registry synced from {url} but is not resolvable \
(does the remote expose refs/auths/registry?): {e}"
)
})?;
Ok(())
}
6 changes: 5 additions & 1 deletion crates/auths-witness-node/tests/cases/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ fn compose_default_registry_interpolates_without_env() {
std::fs::read_to_string(workspace_root.join("deploy/witness/docker-compose.yml")).unwrap();
let registry_line = compose
.lines()
.find(|line| line.contains(":/registry:ro"))
.find(|line| line.contains("${WITNESS_REGISTRY"))
.expect("registry volume line present");
assert!(
registry_line.contains(":/registry"),
"registry must mount at /registry, got: {registry_line}"
);
assert!(
registry_line.contains("${WITNESS_REGISTRY:-"),
"registry mount must interpolate a default, got: {registry_line}"
Expand Down
Loading
Loading