Exosphere.ATProto.Identity.DID.PLC.Operation (Exosphere v0.6.0)

Copy Markdown View Source

Construction and validation of did:plc operations.

An operation is a plain map with string keys — the shape the directory serves and the shape DAG-CBOR encodes — rather than a struct, because the bytes are what a DID is derived from and what a signature covers. Keeping the map verbatim means a fetched operation round-trips without a lossy conversion in the middle.

Operation types

  • "plc_operation" — the modern operation, carrying alsoKnownAs, verificationMethods, rotationKeys and services.
  • "plc_tombstone" — permanently deactivates the DID. Carries only type, prev and sig; it cannot record a move target.
  • "create" — the legacy genesis form, still present in real audit logs (see log_legacy_dholms.json). Accepted for validation and normalized by normalize/1; never emitted.

Strictness

The directory tolerates no extra fields, and neither do we: validate/1 rejects any key outside the type's fixed set. rotationKeys must hold 1–5 unique did:keys on secp256k1 or P-256 — the only two curves the method permits for rotation — and services keys carry no # prefix.

prev is explicitly null for a genesis operation. It is present and null, never omitted; an omitted prev changes the DAG-CBOR bytes and therefore the DID.

Summary

Functions

Is this operation a genesis (no predecessor)?

Build an unsigned plc_operation.

Build an unsigned plc_tombstone pointing at prev.

Normalize a legacy create operation into the modern plc_operation shape.

The rotation keys that may sign the next operation after this one.

The DAG-CBOR bytes of a complete, signed operation — what its CID and, for a genesis operation, its DID are derived from.

Is this operation a tombstone?

The DAG-CBOR bytes an operation's signature covers: the operation with sig omitted entirely (not nulled).

Validate an operation's shape.

Types

build_opts()

@type build_opts() :: [
  also_known_as: [String.t()],
  verification_methods: %{required(String.t()) => String.t()},
  rotation_keys: [String.t()],
  services: %{required(String.t()) => %{required(String.t()) => String.t()}},
  prev: String.t() | nil
]

t()

@type t() :: %{required(String.t()) => term()}

Functions

genesis?(op)

@spec genesis?(t()) :: boolean()

Is this operation a genesis (no predecessor)?

new(opts)

@spec new(build_opts()) :: {:ok, t()} | {:error, term()}

Build an unsigned plc_operation.

Returns the operation without a sig key — pass it to Exosphere.ATProto.Identity.DID.PLC.Signer.sign/3.

Options

  • :also_known_as — URIs, e.g. ["at://alice.example.com"] (no duplicates)
  • :verification_methods — map of service id (no #) to did:key
  • :rotation_keys — 1–5 unique did:keys, highest authority first
  • :services — map of service id (no #) to %{"type" => _, "endpoint" => _}
  • :prev — CID string of the previous operation, or nil for genesis

Examples

iex> {:ok, op} = Operation.new(rotation_keys: ["did:key:zQ3sh..."], prev: nil)
iex> op["prev"]
nil

new_tombstone(prev)

@spec new_tombstone(String.t()) :: {:ok, t()} | {:error, term()}

Build an unsigned plc_tombstone pointing at prev.

A tombstone permanently deactivates the DID. It carries no data fields, so it cannot record where an identity moved to — that record has to live somewhere else.

normalize(op)

@spec normalize(t()) :: t()

Normalize a legacy create operation into the modern plc_operation shape.

Modern operations are returned unchanged. The normalized form is for interpretation only — never hash or verify it, because a legacy operation's DID and signature are derived from its own original bytes.

rotation_keys(op)

@spec rotation_keys(t()) :: [String.t()]

The rotation keys that may sign the next operation after this one.

Legacy operations are normalized first, so a create yields [recoveryKey, signingKey] in that authority order.

signed_bytes(op)

@spec signed_bytes(t()) :: {:ok, binary()} | {:error, term()}

The DAG-CBOR bytes of a complete, signed operation — what its CID and, for a genesis operation, its DID are derived from.

tombstone?(arg1)

@spec tombstone?(t()) :: boolean()

Is this operation a tombstone?

unsigned_bytes(op)

@spec unsigned_bytes(t()) :: {:ok, binary()} | {:error, term()}

The DAG-CBOR bytes an operation's signature covers: the operation with sig omitted entirely (not nulled).

prev is left as the string CID it already is — DAG-CBOR string-encodes it rather than emitting an IPLD link, which is the trap this function exists to avoid re-introducing.

validate(op)

@spec validate(t()) :: :ok | {:error, term()}

Validate an operation's shape.

Checks the field set is exactly the one its type permits, that prev is present (possibly null), and — for plc_operation — the alsoKnownAs, rotationKeys, verificationMethods and services rules.

Signature validity is a separate question; see Exosphere.ATProto.Identity.DID.PLC.Signer.verify/3.