Header menu logo FSharp.ATProto

Repository

AT Protocol repositories are Merkle Search Trees (MSTs) with signed commits. The FSharp.ATProto.Repo package provides data structures for reading, writing, and verifying repository contents -- including MST operations, commit signing, and CAR v1 export.

Merkle Search Tree (MST)

The MST is the core data structure that stores records in a repository. Keys are strings in the form collection/rkey (e.g. app.bsky.feed.post/3jui7kd2z2y2e), and values are CIDs pointing to the record's DAG-CBOR block.

Types

type Entry =
    { Key : string
      Value : Cid
      Tree : Node option }

and Node =
    { Left : Node option
      Entries : Entry list }

Building and Querying

open FSharp.ATProto.Repo

// Start with an empty tree
let tree = Mst.empty

// Build from a sorted list of entries
let tree2 =
    Mst.create
        [ ("app.bsky.feed.post/abc", cidA)
          ("app.bsky.feed.post/def", cidB) ]

// Insert and delete (both rebuild the tree)
let tree3 = tree2 |> Mst.insert "app.bsky.feed.post/ghi" cidC
let tree4 = tree3 |> Mst.delete "app.bsky.feed.post/abc"

// Look up a key
match Mst.lookup "app.bsky.feed.post/def" tree4 with
| Some cid -> printfn "Found: %s" (Cid.value cid)
| None -> printfn "Not found"

// List all entries in sorted order
let allPairs = Mst.allEntries tree4

Diffing

Compare two MST states to find what changed:

let (added, updated, deleted) = Mst.diff oldTree newTree
// added: (key, cid) list -- new keys
// updated: (key, cid) list -- keys with changed CIDs
// deleted: string list -- removed keys

Serialization

Serialize an MST to DAG-CBOR blocks and compute its root CID:

let (rootCid, blocks) = Mst.serialize tree5
// rootCid: Cid -- the MST root
// blocks: Map<string, byte[]> -- CID string -> DAG-CBOR bytes

// Deserialize from a block store
match Mst.deserialize blocks rootCid with
| Ok node -> printfn "Loaded %d entries" (Mst.allEntries node).Length
| Error msg -> printfn "Error: %s" msg

Signed Commits

Every repository state is anchored by a signed commit that references the MST root.

type SignedCommit =
    { Did : Did
      Version : int       // always 3
      Data : Cid          // MST root CID
      Rev : string        // TID-based revision
      Prev : Cid option   // previous commit CID
      Sig : byte[] }      // 64-byte ECDSA signature

Creating and Verifying Commits

open FSharp.ATProto.Repo
open FSharp.ATProto.Crypto

let keyPair = Keys.generate Algorithm.P256
let rev = Commit.createRev ()

// Create a signed commit
let commit = Commit.create did rootCid2 rev None keyPair

// Encode to DAG-CBOR
let commitBytes = Commit.encode commit

// Decode from DAG-CBOR
match Commit.decode commitBytes with
| Ok decoded -> printfn "Rev: %s" decoded.Rev
| Error msg -> printfn "Decode error: %s" msg

// Verify signature
match Commit.verify (Keys.publicKey keyPair) commitBytes with
| Ok true -> printfn "Valid signature"
| Ok false -> printfn "Invalid signature"
| Error msg -> printfn "Verification error: %s" msg

In-Memory Repository

The Repo module provides a higher-level wrapper combining the MST with repository metadata:

open FSharp.ATProto.Repo
open FSharp.ATProto.Syntax
let did2 = Did.parse "did:plc:example" |> Result.defaultWith failwith
let repo = Repo.empty did2

// Add and remove records
let repo2 = repo |> Repo.putRecord "app.bsky.feed.post/abc" recordCid
let repo3 = repo2 |> Repo.deleteRecord "app.bsky.feed.post/abc"

// Query
let maybeCid = Repo.getRecord "app.bsky.feed.post/abc" repo3
let allRecords = Repo.listRecords repo3

// Compute root CID and blocks
let (rootCid3, blocks3) = Repo.computeRoot repo3

CAR Export

Export a repository's blocks as a CAR v1 file:

let (rootCid4, blocks4) = Repo.computeRoot repo4
let carBytes = Repo.exportCar [ rootCid4 ] blocks4
// carBytes is a complete CAR v1 file ready for transport

The CAR format is used by AT Protocol sync endpoints (com.atproto.sync.getRepo, etc.) to transfer repository data.

type Entry = { Key: string Value: obj Tree: Node option }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
type Node = { Left: Node option Entries: Entry list }
type 'T option = Option<'T>
type 'T list = List<'T>
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.DRISL
namespace FSharp.ATProto.Crypto
namespace FSharp.ATProto.Repo
val cidA: Cid
module Unchecked from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
Multiple items
module Cid from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Cid" /> values. </summary>

--------------------
type Cid = private | Cid of string override ToString: unit -> string
<summary> A Content Identifier (CID) used to reference content-addressed data in the AT Protocol. CIDs are self-describing content hashes that uniquely identify a piece of data. Only CIDv1 is supported; CIDv0 (starting with <c>Qmb</c>) is rejected. </summary>
<remarks> CIDs in the AT Protocol use CIDv1 with DAG-CBOR codec and SHA-256 hash. This type performs syntactic validation only (base-encoded alphanumeric string of 8-256 characters). See https://github.com/multiformats/cid for the CID specification. </remarks>
val cidB: Cid
val cidC: Cid
val tree: Mst.Node
module Mst from FSharp.ATProto.Repo
<summary> Merkle Search Tree (MST) data structure for AT Protocol repositories. </summary>
val empty: Mst.Node
<summary> An empty MST. </summary>
val tree2: Mst.Node
val create: entries: (string * Cid) list -> Mst.Node
<summary> Build an MST from a sorted list of (key, valueCid) pairs. Higher heightForKey values = closer to root. Layer 0 = leaf level. </summary>
val tree3: Mst.Node
val insert: key: string -> value: Cid -> node: Mst.Node -> Mst.Node
<summary> Insert a key-value pair into the MST. Rebuilds the tree. </summary>
val tree4: Mst.Node
val delete: key: string -> node: Mst.Node -> Mst.Node
<summary> Delete a key from the MST. Rebuilds the tree. </summary>
val lookup: key: string -> node: Mst.Node -> Cid option
<summary> Look up a key in the MST. </summary>
union case Option.Some: Value: 'T -> Option<'T>
val cid: Cid
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val value: Cid -> string
<summary> Extract the string representation of a CID. </summary>
<param name="cid">The CID to extract the value from.</param>
<returns>The CID string in its base-encoded form.</returns>
union case Option.None: Option<'T>
val allPairs: (string * Cid) list
val allEntries: node: Mst.Node -> (string * Cid) list
<summary> Collect all (key, value) pairs from an MST in sorted order. </summary>
val oldTree: Mst.Node
type Node = { Left: Node option Entries: Entry list }
<summary> An MST node. </summary>
val newTree: Mst.Node
val added: (string * Cid) list
val updated: (string * Cid) list
val deleted: string list
val diff: oldNode: Mst.Node -> newNode: Mst.Node -> (string * Cid) list * (string * Cid) list * string list
<summary> Diff two MSTs, returning (added, updated, deleted) key-value pairs. </summary>
val tree5: Mst.Node
val rootCid: Cid
val blocks: Map<string,byte array>
val serialize: node: Mst.Node -> Cid * Map<string,byte array>
<summary> Serialize an MST node to DAG-CBOR and return (CID, block map). </summary>
val deserialize: blocks: Map<string,byte array> -> cid: Cid -> Result<Mst.Node,string>
<summary> Deserialize an MST from a block store starting from a root CID. </summary>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val node: Mst.Node
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val msg: string
type SignedCommit = { Did: obj Version: int Data: obj Rev: string Prev: obj Sig: byte array }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
namespace Microsoft.FSharp.Data
Multiple items
val byte: value: 'T -> byte (requires member op_Explicit)

--------------------
type byte = System.Byte

--------------------
type byte<'Measure> = byte
val did: Did
Multiple items
module Did from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Did" /> values. </summary>

--------------------
type Did = private | Did of string override ToString: unit -> string
<summary> A decentralized identifier (DID) as defined by the AT Protocol. DIDs are the primary stable identifier for accounts. Two methods are currently supported: <c>did:plc:</c> (hosted, managed by PLC directory) and <c>did:web:</c> (self-hosted, DNS-based). </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/did and the W3C DID specification: https://www.w3.org/TR/did-core/ </remarks>
val parse: s: string -> Result<Did,string>
<summary> Parse and validate a DID string. </summary>
<param name="s"> A DID string in the format <c>did:&lt;method&gt;:&lt;method-specific-id&gt;</c> (e.g. <c>"did:plc:z72i7hdynmk6r22z27h6tvur"</c> or <c>"did:web:example.com"</c>). </param>
<returns><c>Ok</c> with a validated <see cref="Did" />, or <c>Error</c> with a message describing the validation failure. Validation failures include: null input, exceeding the 2048-character limit, invalid DID syntax, or invalid percent-encoding. </returns>
<example><code> match Did.parse "did:plc:z72i7hdynmk6r22z27h6tvur" with | Ok did -&gt; printfn "Valid: %s" (Did.value did) | Error e -&gt; printfn "Invalid: %s" e </code></example>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val defaultWith: defThunk: ('Error -> 'T) -> result: Result<'T,'Error> -> 'T
val failwith: message: string -> 'T
val rootCid2: Cid
val keyPair: KeyPair
module Keys from FSharp.ATProto.Crypto
<summary> Key management for P-256 and secp256k1 curves. </summary>
val generate: algorithm: Algorithm -> KeyPair
<summary> Generate a new key pair for the given algorithm. </summary>
type Algorithm = | P256 | K256
<summary> The elliptic curve algorithm used for a key. </summary>
union case Algorithm.P256: Algorithm
val rev: string
module Commit from FSharp.ATProto.Repo
<summary> Signed commit objects for AT Protocol repositories. </summary>
val createRev: unit -> string
<summary> Create a TID-based revision string for the current time. </summary>
val commit: Commit.SignedCommit
val create: did: Did -> data: Cid -> rev: string -> prev: Cid option -> keyPair: KeyPair -> Commit.SignedCommit
<summary> Create and sign a commit. </summary>
val commitBytes: byte array
val encode: commit: Commit.SignedCommit -> byte array
<summary> Encode a signed commit to DAG-CBOR bytes (with signature). </summary>
val decode: data: byte array -> Result<Commit.SignedCommit,string>
<summary> Decode a signed commit from DAG-CBOR bytes. </summary>
val decoded: Commit.SignedCommit
Commit.SignedCommit.Rev: string
val verify: publicKey: PublicKey -> commitBytes: byte array -> Result<bool,string>
<summary> Verify a signed commit's signature against a public key. </summary>
val publicKey: keyPair: KeyPair -> PublicKey
<summary> Get the public key from a key pair. </summary>
val recordCid: Cid
val did2: Did
val repo: Repo.Repository
module Repo from FSharp.ATProto.Repo
<summary> In-memory repository with MST, commit creation, and CAR export/import. </summary>
val empty: did: Did -> Repo.Repository
<summary> Create an empty repository for a DID. </summary>
val repo2: Repo.Repository
val putRecord: key: string -> value: Cid -> repo: Repo.Repository -> Repo.Repository
<summary> Put a record into the repository. </summary>
val repo3: Repo.Repository
val deleteRecord: key: string -> repo: Repo.Repository -> Repo.Repository
<summary> Delete a record from the repository. </summary>
val maybeCid: Cid option
val getRecord: key: string -> repo: Repo.Repository -> Cid option
<summary> Get a record value by its key (collection/rkey path). </summary>
val allRecords: (string * Cid) list
val listRecords: repo: Repo.Repository -> (string * Cid) list
<summary> List all records in the repository. </summary>
val rootCid3: Cid
val blocks3: Map<string,byte array>
val computeRoot: repo: Repo.Repository -> Cid * Map<string,byte array>
<summary> Compute the MST root CID and collect all blocks. </summary>
val repo4: Repo.Repository
type Repository = { Did: Did Tree: Node Rev: string option Prev: Cid option }
<summary> An in-memory repository. </summary>
val rootCid4: Cid
val blocks4: Map<string,byte array>
val carBytes: byte array
val exportCar: roots: Cid list -> blocks: Map<string,byte array> -> byte array
<summary> Serialize a CAR v1 file from roots and blocks. </summary>

Type something to start searching.