Header menu logo FSharp.ATProto

PLC Directory

The PLC Directory is the primary DID registry for AT Protocol. All did:plc:* identifiers resolve through it. The Plc module in FSharp.ATProto.Core provides read operations (resolve, audit log, export) that require no authentication, and operation construction helpers for creating or updating DID documents.

All read functions take an HttpClient directly -- no AtpAgent needed.

Types

type PlcDocument =
    { Did : Did
      AlsoKnownAs : string list                  // e.g. ["at://handle.bsky.social"]
      VerificationMethods : Map<string, string>   // fragment ID -> did:key
      RotationKeys : string list                  // did:key values
      Services : Map<string, PlcService> }        // service ID -> { Type; Endpoint }

type PlcService = { Type : string; Endpoint : string }

type PlcError =
    | HttpError of statusCode: int * body: string
    | ParseError of message: string
    | NotFound of did: string

Resolving a DID

open FSharp.ATProto.Core
open FSharp.ATProto.Syntax

let client = new System.Net.Http.HttpClient()
let did = Did.parse "did:plc:ewvi7nxzyoun6zhxrhs64oiz" |> Result.defaultWith failwith

task {
    match! Plc.resolve client did None with
    | Ok doc ->
        printfn "Handle: %A" doc.AlsoKnownAs
        for svc in doc.Services do
            printfn "Service %s: %s" svc.Key svc.Value.Endpoint
    | Error (Plc.PlcError.NotFound d) ->
        printfn "DID not found: %s" d
    | Error (Plc.PlcError.HttpError (code, body)) ->
        printfn "HTTP %d: %s" code body
    | Error (Plc.PlcError.ParseError msg) ->
        printfn "Parse error: %s" msg
}

The optional third parameter overrides the PLC Directory URL (defaults to https://plc.directory).

Audit Log

Get the full operation history for a DID:

task {
    match! Plc.getAuditLog client did None with
    | Ok entries ->
        for entry in entries do
            printfn "[%O] %A (nullified: %b)" entry.CreatedAt entry.Operation.Type entry.Nullified
    | Error err -> printfn "Error: %A" err
}

Each AuditEntry contains the signed PlcOperation, its CID, whether it has been nullified, and the timestamp.

Bulk Export

The export endpoint returns operations across all DIDs as NDJSON, useful for building mirrors or analytics:

task {
    match! Plc.export client None (Some 100) None with
    | Ok entries -> printfn "Got %d entries" entries.Length
    | Error err -> printfn "Error: %A" err
}

Parameters: after (ISO 8601 cursor for pagination), count (max entries), and baseUrl.

Creating Operations

Build unsigned operations for DID document management:

// Genesis operation (first operation for a new DID)
let genesis =
    Plc.createGenesisOp
        [ "did:key:zQ3sh..." ]                         // rotation keys
        (Map.ofList [ "atproto", "did:key:zDnae..." ]) // verification methods
        [ "at://handle.example.com" ]                   // alsoKnownAs
        (Map.ofList [ "atproto_pds",
                       { Type = "AtprotoPersonalDataServer"
                         Endpoint = "https://pds.example.com" } ])

// Update operation (references previous operation CID)
let rotation =
    Plc.createRotationOp prevCid rotationKeys verificationMethods alsoKnownAs services

// Tombstone (deactivate the DID)
let tombstone = Plc.createTombstoneOp prevCid

Signing and Submitting

Sign an operation and submit it to the PLC Directory. The signing function is injected -- this module does not depend on FSharp.ATProto.Crypto directly.

open FSharp.ATProto.Crypto

let keyPair = Keys.generate Algorithm.P256
let sign = Signing.sign keyPair
// Sign the operation
let signed = Plc.signOperation sign genesis

// Submit to the directory
task {
    match! Plc.submitOperation client did signed None with
    | Ok () -> printfn "Operation submitted"
    | Error err -> printfn "Error: %A" err
}

signOperation serializes the operation to canonical JSON (omitting the sig field), signs it, and returns the operation with the base64url-encoded signature populated.

type PlcDocument = { Did: obj AlsoKnownAs: string list VerificationMethods: Map<string,string> RotationKeys: string list Services: obj }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
type 'T list = List<'T>
Multiple items
module Map from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...

--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
type PlcService = { Type: string Endpoint: string }
type PlcError = | HttpError of statusCode: int * body: string | ParseError of message: string | NotFound of did: string
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.Core
namespace FSharp.ATProto.Bluesky
val client: System.Net.Http.HttpClient
namespace System
namespace System.Net
namespace System.Net.Http
Multiple items
type HttpClient = inherit HttpMessageInvoker new: unit -> unit + 2 overloads member CancelPendingRequests: unit -> unit member DeleteAsync: requestUri: string -> Task<HttpResponseMessage> + 3 overloads member GetAsync: requestUri: string -> Task<HttpResponseMessage> + 7 overloads member GetByteArrayAsync: requestUri: string -> Task<byte array> + 3 overloads member GetStreamAsync: requestUri: string -> Task<Stream> + 3 overloads member GetStringAsync: requestUri: string -> Task<string> + 3 overloads member PatchAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads member PostAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads ...
<summary>Provides a class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.</summary>

--------------------
System.Net.Http.HttpClient() : System.Net.Http.HttpClient
System.Net.Http.HttpClient(handler: System.Net.Http.HttpMessageHandler) : System.Net.Http.HttpClient
System.Net.Http.HttpClient(handler: System.Net.Http.HttpMessageHandler, disposeHandler: bool) : System.Net.Http.HttpClient
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 task: TaskBuilder
module Plc from FSharp.ATProto.Core
<summary> Client for the PLC Directory -- the public ledger for <c>did:plc:*</c> DIDs. Provides read operations (resolve, audit log, export) that require no authentication, and operation construction/signing helpers for DID document updates. </summary>
<remarks> The PLC Directory is a centralized but auditable registry. All read endpoints are public and require only an <see cref="System.Net.Http.HttpClient" />. Write operations (creating or updating DID documents) require signing with a rotation key, but this module does NOT depend on <c>FSharp.ATProto.Crypto</c> directly -- signing is injected via a <c>byte[] -&gt; byte[]</c> function parameter. </remarks>
val resolve: client: System.Net.Http.HttpClient -> did: Did -> baseUrl: string option -> System.Threading.Tasks.Task<Result<Plc.PlcDocument,Plc.PlcError>>
<summary> Resolve a <c>did:plc:*</c> DID to its current DID document. Calls <c>GET {baseUrl}/{did}</c> on the PLC Directory. </summary>
<param name="client">An <see cref="System.Net.Http.HttpClient" /> for making the request.</param>
<param name="did">The DID to resolve.</param>
<param name="baseUrl">The PLC Directory base URL. Defaults to <c>https://plc.directory</c>.</param>
<returns>The resolved <see cref="PlcDocument" /> on success, or a <see cref="PlcError" /> on failure.</returns>
union case Option.None: Option<'T>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val doc: Plc.PlcDocument
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
Plc.PlcDocument.AlsoKnownAs: string list
<summary>Alternative identifiers (e.g. <c>["at://handle.bsky.social"]</c>).</summary>
val svc: System.Collections.Generic.KeyValuePair<string,Plc.PlcService>
Plc.PlcDocument.Services: Map<string,Plc.PlcService>
<summary>Service endpoints keyed by service ID (e.g. <c>"atproto_pds"</c>).</summary>
property System.Collections.Generic.KeyValuePair.Key: string with get
<summary>Gets the key in the key/value pair.</summary>
<returns>A <typeparamref name="TKey" /> that is the key of the <see cref="T:System.Collections.Generic.KeyValuePair`2" />.</returns>
property System.Collections.Generic.KeyValuePair.Value: Plc.PlcService with get
<summary>Gets the value in the key/value pair.</summary>
<returns>A <typeparamref name="TValue" /> that is the value of the <see cref="T:System.Collections.Generic.KeyValuePair`2" />.</returns>
Plc.PlcService.Endpoint: string
<summary>The service endpoint URL (e.g. <c>"https://bsky.social"</c>).</summary>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
type PlcError = | HttpError of statusCode: int * body: string | ParseError of message: string | NotFound of did: string
<summary> Error type for PLC Directory operations. </summary>
union case Plc.PlcError.NotFound: did: string -> Plc.PlcError
<summary>The DID was not found (404).</summary>
val d: string
union case Plc.PlcError.HttpError: statusCode: int * body: string -> Plc.PlcError
<summary>HTTP request failed with the given status code and body.</summary>
val code: int
val body: string
union case Plc.PlcError.ParseError: message: string -> Plc.PlcError
<summary>The response body could not be parsed.</summary>
val msg: string
val getAuditLog: client: System.Net.Http.HttpClient -> did: Did -> baseUrl: string option -> System.Threading.Tasks.Task<Result<Plc.AuditEntry list,Plc.PlcError>>
<summary> Get the audit log for a <c>did:plc:*</c> DID. Calls <c>GET {baseUrl}/{did}/log/audit</c> on the PLC Directory. Returns the complete history of signed operations for the DID. </summary>
<param name="client">An <see cref="System.Net.Http.HttpClient" /> for making the request.</param>
<param name="did">The DID to get the audit log for.</param>
<param name="baseUrl">The PLC Directory base URL. Defaults to <c>https://plc.directory</c>.</param>
<returns>A list of <see cref="AuditEntry" /> on success, or a <see cref="PlcError" /> on failure.</returns>
val entries: Plc.AuditEntry list
val entry: Plc.AuditEntry
Plc.AuditEntry.CreatedAt: System.DateTimeOffset
<summary>When the PLC Directory accepted this operation.</summary>
Plc.AuditEntry.Operation: Plc.PlcOperation
<summary>The signed operation.</summary>
Plc.PlcOperation.Type: Plc.PlcOperationType
<summary>The operation type.</summary>
Plc.AuditEntry.Nullified: bool
<summary>Whether this operation has been nullified.</summary>
val err: Plc.PlcError
val export: client: System.Net.Http.HttpClient -> after: string option -> count: int option -> baseUrl: string option -> System.Threading.Tasks.Task<Result<Plc.ExportEntry list,Plc.PlcError>>
<summary> Export operations from the PLC Directory. Calls <c>GET {baseUrl}/export?after={after}&amp;count={count}</c>. The export endpoint returns newline-delimited JSON (NDJSON). </summary>
<param name="client">An <see cref="System.Net.Http.HttpClient" /> for making the request.</param>
<param name="after">Optional cursor (ISO 8601 timestamp) to resume export from.</param>
<param name="count">Optional maximum number of entries to return.</param>
<param name="baseUrl">The PLC Directory base URL. Defaults to <c>https://plc.directory</c>.</param>
<returns>A list of <see cref="ExportEntry" /> on success, or a <see cref="PlcError" /> on failure.</returns>
union case Option.Some: Value: 'T -> Option<'T>
val entries: Plc.ExportEntry list
property List.Length: int with get
val prevCid: string
module Unchecked from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
val rotationKeys: string list
val verificationMethods: Map<string,string>
val alsoKnownAs: string list
val services: Map<string,Plc.PlcService>
type PlcService = { Type: string Endpoint: string }
<summary> A service endpoint entry in a PLC operation. Maps a service identifier (e.g. <c>"atproto_pds"</c>) to its type and endpoint URL. </summary>
val genesis: Plc.PlcOperation
val createGenesisOp: rotationKeys: string list -> verificationMethods: Map<string,string> -> alsoKnownAs: string list -> services: Map<string,Plc.PlcService> -> Plc.PlcOperation
<summary> Create an unsigned genesis operation (the first operation for a new DID). The <c>prev</c> field is <c>None</c> because there is no prior operation. </summary>
<param name="rotationKeys">The rotation keys (1-5 <c>did:key</c> values) that will control this DID.</param>
<param name="verificationMethods">Verification methods as a map of key ID to <c>did:key</c> value.</param>
<param name="alsoKnownAs">Alternative identifiers (e.g. <c>["at://handle.bsky.social"]</c>).</param>
<param name="services">Service endpoints keyed by service ID.</param>
<returns>An unsigned <see cref="PlcOperation" /> with <c>prev = None</c> and <c>sig = None</c>.</returns>
val ofList: elements: ('Key * 'T) list -> Map<'Key,'T> (requires comparison)
val rotation: Plc.PlcOperation
val createRotationOp: prev: string -> rotationKeys: string list -> verificationMethods: Map<string,string> -> alsoKnownAs: string list -> services: Map<string,Plc.PlcService> -> Plc.PlcOperation
<summary> Create an unsigned rotation (update) operation that modifies the DID document. </summary>
<param name="prev">The CID of the previous operation in the log.</param>
<param name="rotationKeys">The new rotation keys.</param>
<param name="verificationMethods">The new verification methods.</param>
<param name="alsoKnownAs">The new alternative identifiers.</param>
<param name="services">The new service endpoints.</param>
<returns>An unsigned <see cref="PlcOperation" /> with the given <c>prev</c> CID.</returns>
val tombstone: Plc.PlcOperation
val createTombstoneOp: prev: string -> Plc.PlcOperation
<summary> Create an unsigned tombstone operation that deactivates the DID. </summary>
<param name="prev">The CID of the previous operation in the log.</param>
<returns>An unsigned tombstone <see cref="PlcOperation" />.</returns>
val sign: (byte array -> byte array)
Multiple items
val byte: value: 'T -> byte (requires member op_Explicit)

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

--------------------
type byte<'Measure> = byte
namespace Microsoft.FSharp
val keyPair: obj
val sign: obj
val signed: Plc.PlcOperation
val signOperation: sign: (byte array -> byte array) -> op: Plc.PlcOperation -> Plc.PlcOperation
<summary> Sign a PLC operation with a rotation key. The sign function should produce a 64-byte compact ECDSA signature (r || s) over the input bytes. The data is the canonical JSON encoding of the operation (without the <c>sig</c> field). </summary>
<param name="sign">A signing function that takes raw bytes and returns a 64-byte signature. Typically <c>Signing.sign keyPair</c> from the Crypto project.</param>
<param name="op">The unsigned operation to sign.</param>
<returns>The operation with the <c>sig</c> field populated.</returns>
val submitOperation: client: System.Net.Http.HttpClient -> did: Did -> op: Plc.PlcOperation -> baseUrl: string option -> System.Threading.Tasks.Task<Result<unit,Plc.PlcError>>
<summary> Submit a signed PLC operation to the PLC Directory. Calls <c>POST {baseUrl}/{did}</c> with the serialized operation as the JSON body. </summary>
<param name="client">An <see cref="System.Net.Http.HttpClient" /> for making the request.</param>
<param name="did">The DID to submit the operation for.</param>
<param name="op">The signed operation to submit. Must have a <c>Sig</c> value.</param>
<param name="baseUrl">The PLC Directory base URL. Defaults to <c>https://plc.directory</c>.</param>
<returns><c>Ok ()</c> on success, or a <see cref="PlcError" /> on failure.</returns>

Type something to start searching.