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.
val string: value: 'T -> string
--------------------
type string = System.String
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>
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
namespace FSharp
--------------------
namespace Microsoft.FSharp
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
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>
<summary> Parse and validate a DID string. </summary>
<param name="s"> A DID string in the format <c>did:<method>:<method-specific-id></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 -> printfn "Valid: %s" (Did.value did) | Error e -> printfn "Invalid: %s" e </code></example>
module Result from Microsoft.FSharp.Core
--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
<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[] -> byte[]</c> function parameter. </remarks>
<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>
<summary>Alternative identifiers (e.g. <c>["at://handle.bsky.social"]</c>).</summary>
<summary>Service endpoints keyed by service ID (e.g. <c>"atproto_pds"</c>).</summary>
<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>
<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>
<summary>The service endpoint URL (e.g. <c>"https://bsky.social"</c>).</summary>
<summary> Error type for PLC Directory operations. </summary>
<summary>The DID was not found (404).</summary>
<summary>HTTP request failed with the given status code and body.</summary>
<summary>The response body could not be parsed.</summary>
<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>
<summary>When the PLC Directory accepted this operation.</summary>
<summary>The signed operation.</summary>
<summary>The operation type.</summary>
<summary>Whether this operation has been nullified.</summary>
<summary> Export operations from the PLC Directory. Calls <c>GET {baseUrl}/export?after={after}&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>
<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>
<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>
<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>
<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 byte: value: 'T -> byte (requires member op_Explicit)
--------------------
type byte = System.Byte
--------------------
type byte<'Measure> = byte
<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>
<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>