Fabric
Fabric enables stateless handle resolution & verification through the HTTP-based certrelay network in any environment, including the browser. Resolution is permissionless against a specified .
Install Fabric
The Fabric client handles relay connections, verification, and broadcasting. Each package bundles libveritas for proof verification.
// cargo add fabric-resolver
Resolve a handle
Resolve a handle to get its records.
let fabric = Fabric::new();
let Some(zone) = fabric.resolve("alice@bitcoin").await? else {
println!("handle not found");
return Ok(());
};
println!("Handle found: {}", zone.handle);
Read records
Unpack and read records from a resolved handle.
for record in zone.records.iter()? {
match record {
ParsedRecord::Txt { key, value } => {
println!("txt {}={}", key, value.to_vec().join(", "))
}
ParsedRecord::Addr { key, value } => {
println!("addr {}={}", key, value.to_vec().join(", "))
}
_ => {}
}
}
Trust & Verification
Fabric tracks three trust pools: trusted, semi-trusted, and observed. The observed id is the latest state seen from peers - it's used for best-effort resolution but isn't trusted on its own. Without any pinned trust, badge() returns Unverified.
The trusted id is pinned by scanning a QR code directly from a local trust source e.g. Veritas menubar app. Handles verified against a locally verified trust id report as "Orange" (if their certificate is final). The semi-trusted id comes from a less authoritative source e.g. user scans trust id from an explorer over HTTPS - it won't produce Orange state, but results won't show as Unverified either.
See the Design Guidelines for how to display badges in your UI.
// Before pinning a trust id: resolve uses observed (peer) state
// badge() returns Unverified
let zone = fabric.resolve("alice@bitcoin").await?
.expect("handle exists");
fabric.badge(&zone); // Unverified
// Pin trust from a QR scan
let qr = "veritas://scan?id=14ef902621df01bdeee0b23fedf67458563a20df600af8979a4748dcd9d1b9f9";
// For highest level of trust (scan QR code from Veritas desktop)
fabric.trust_from_qr(qr).await?;
// Does not require re-resolving, badge now checks
// whether zone was verified against a trusted root
fabric.badge(&zone); // Orange if handle is sovereign (final certificate)
// Or from a semi-trusted source (e.g. an explorer you trust with qr scanned over HTTPS)
// .badge() will not show Orange for roots in this trust pool,
// but it will not report it as "Unverified".
fabric.semi_trust_from_qr(qr).await?;
// Check current trust ids
fabric.trusted(); // pinned id from local verification
fabric.semi_trusted(); // pinned id from semi-trusted source
fabric.observed(); // latest from peers
// Clear trusted state
fabric.clear_trusted();
fabric.clear_semi_trusted();
Publishing Records
Pack records, sign them, and publish to the network.
Pack records
Create a record set with the data you want to publish for your handle.
let records = RecordSet::pack(vec![
Record::seq(1),
Record::txt("website", &["https://example.com"]),
Record::addr("btc", &["bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"]),
Record::addr("nostr", &[
"npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6",
"wss://relay.example.com",
]),
])?;
Publish
Sign and publish your records to the network.
let cert = fabric.export("alice@bitcoin").await?;
fabric.publish(&cert, records, &secret_key, true).await?;
Reverse Resolution
Look up handles from addresses or numeric identifiers.
Search addresses
Relays index the mapping of all ADDR records, for example to find names associated with an npub:
An ADDR record is a one-way statement. For example, if Alice adds her Nostr npub (like npub1abc...xyz) to her handle, she's saying "this is my Nostr identity." But it's on the application layer to verify that she actually controls the npub. Search-addrs simply indexes the mappings found in record sets and returns zones containing the matching ADDR record.
let zones = fabric
.search_addr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6")
.await?;
for zone in &zones {
println!("{}: {:?}", zone.handle, zone.sovereignty);
}
Resolve by id
Handles have a numeric identifier, if you have the id, you could resolve back the handle name:
let Some(zone) = fabric.resolve_by_id("num1qx8dtlzq...").await? else {
println!("handle not found");
return Ok(());
};
println!("Handle found: {}", zone.handle);
More
Resolve multiple handles
Batch resolution returns a ResolvedBatch with shared roots. Use badge_for() to get the badge for each zone in the batch.
let zones = fabric
.resolve_all(&["alice@bitcoin", "bob@bitcoin"])
.await?;
for zone in &zones {
println!("{}: {:?}", zone.handle, zone.sovereignty);
}
MessageBuilder (advanced)
For batching multiple handles into a single message, or when you need access to the raw message bytes before broadcasting.
let cert = CertificateChain::from_slice(&cert_bytes)?;
let mut builder = MessageBuilder::new();
builder.add_handle(cert, records);
let proof = ChainProof::from_slice(
&fabric.prove(&builder.chain_proof_request()).await?,
)?;
let (mut msg, unsigned) = builder.build(proof)?;
for mut u in unsigned {
u.flags |= SIG_PRIMARY_ZONE;
let sig = sign_schnorr(&u.signing_id(), &secret_key)?;
msg.set_records(&u.canonical, u.pack_sig(sig.to_vec()));
}
fabric.broadcast(&msg.to_bytes()).await?;
Configuration
Custom relay seeds, dev mode, and trust pinning.
let mut fabric = Fabric::with_seeds(&["https://relay1.example.com"])
.with_dev_mode();
fabric.trust(TrustId::from_str("abcdef..")?).await?;
Offline verification
Access the internal Veritas instance for custom proof verification.
let fabric = Fabric::new(); fabric.bootstrap().await?; let veritas = fabric.veritas();