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 .

Open playground
1

Install Fabric

The Fabric client handles relay connections, verification, and broadcasting. Each package bundles libveritas for proof verification.

Rust
// cargo add fabric-resolver
2

Resolve a handle

Resolve a handle to get its records.

Rust
let fabric = Fabric::new();
let Some(zone) = fabric.resolve("alice@bitcoin").await? else {
    println!("handle not found");
    return Ok(());
};

println!("Handle found: {}", zone.handle);
3

Read records

Unpack and read records from a resolved handle.

Rust
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.

Rust
// 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.

1

Pack records

Create a record set with the data you want to publish for your handle.

Rust
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",
    ]),
])?;
2

Publish

Sign and publish your records to the network.

Rust
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.

1

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.

Rust
let zones = fabric
    .search_addr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6")
    .await?;

for zone in &zones {
    println!("{}: {:?}", zone.handle, zone.sovereignty);
}
2

Resolve by id

Handles have a numeric identifier, if you have the id, you could resolve back the handle name:

Rust
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.

Rust
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.

Rust
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.

Rust
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.

Rust
let fabric = Fabric::new();
fabric.bootstrap().await?;
let veritas = fabric.veritas();