Install
$ agentstack add skill-ngerakines-atproto-skills-atproto-identity-resolution ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
AT Protocol Identity Resolution
Every AT Protocol account is a DID; most accounts also have a human-friendly handle. Turning either one into the other — and trusting the result — is the bedrock operation this skill covers. This file is a router that sits on top of a language-neutral spec in shared/ and per-language guides in rust/, typescript/, go/.
Defaults
An atproto identity resolution pipeline always does the same four things:
- Normalize and classify the input (
at://,@, whitespace stripped; then handle vs DID by prefix). - Resolve handle → DID using DNS TXT (
_atproto.) and HTTPS (/.well-known/atproto-did) concurrently (the spec requires it; Rust usestokio::join!, TypeScript races, Go's reference library currently runs sequentially and is a divergence — seeshared/handle-spec.md§4.3 andshared/divergence-matrix.md), with a documented conflict policy. - Fetch the DID document via the method-specific transport (
did:plc→ PLC directory,did:web→ host well-known,did:webvh→ log + verifier). - Validate the document and bidi-check:
alsoKnownAscontainsat://, a#atprotoMultikeyis present, an#atproto_pdsAtprotoPersonalDataServerservice is present.
Anything outside those four steps (rotation keys, PLC operation logs, OAuth, records) belongs in an adjacent skill — see the frontmatter.
Full normative rules: shared/handle-spec.md and shared/did-spec.md. End-to-end sequence: shared/resolution-flow.md. Fixtures: shared/test-vectors.md. Cross-language differences: shared/divergence-matrix.md.
Language detection
Before generating or reviewing identity-resolution code, determine the target language from project files or the file being edited:
Cargo.toml,*.rs,rust-toolchain.toml, any mention ofatproto-identity(crate) → Rust — read fromrust/.package.json,tsconfig.json,*.ts,*.tsx, imports of@atproto/identity/@atproto/syntax/@atproto-labs/handle-resolver→ TypeScript — read fromtypescript/. Also*.js/*.jsxwhen there is no.tspresent in the repo.go.mod,*.go, imports ofgithub.com/bluesky-social/indigo/atproto/identityor.../atproto/syntax→ Go — read fromgo/.
Prefer the file being edited over the repo root when they disagree: a .ts client inside a Rust-workspace monorepo still means TypeScript for that task.
If multiple languages are present and the task does not point at one unambiguously, ask which one applies. Never mix resolver libraries across languages in generated code.
If an unsupported language is detected (Python, Java, Elixir, Swift, …), point the user at shared/handle-spec.md + shared/did-spec.md + shared/resolution-flow.md for the transport-level rules and offer the Rust atproto-identity crate as a reference implementation to transliterate from.
Reading guide
For every identity-resolution task:
- Read
shared/handle-spec.mdandshared/did-spec.mdfirst — the rules your code must enforce. - Read the relevant task file in the detected language directory:
- Validating handles or DIDs as strings →
{lang}/syntax.md - Resolving handle → DID, DID → DID document, or end-to-end →
{lang}/resolution.md - Validating a DID document (bidi check, required methods/services) →
{lang}/validation.md - Dependency setup, library choice, idioms →
{lang}/README.md
- Consult
shared/divergence-matrix.mdwhen porting between languages or reviewing cross-stack interop — concurrency strategy, browser support, webvh support, and error shapes all diverge.
Always prefer the official library (atproto-identity in Rust, @atproto/identity + @atproto/syntax in TypeScript, github.com/bluesky-social/indigo/atproto/identity in Go) over hand-rolling.
Architecture (shared concepts)
Input normalization
Before any syntax check:
- Strip leading
at://. - Strip leading
@(a UI convention; never stored). - Trim whitespace.
- Lowercase handles (handles are case-insensitive). DIDs are case-sensitive — reject mis-cased.
An empty input after normalization is always an error, never a wildcard.
Classification
Given normalized input, classify in prefix order — did:webvh: before did:web: before did:plc:, else handle:
did:webvh:… → DID, method = webvh
did:web:… → DID, method = web
did:plc:… → DID, method = plc
→ candidate handle (run handle syntax validation)
Note: classification is not the same as resolution. See §"DID methods across libraries" below — all three reference libraries classify webvh but none of them fetch the did:webvh log. Treat a webvh input as "validated syntax, resolution deferred" unless you wire in a webvh-specific resolver.
Handle resolution (DNS + HTTPS)
Handles resolve by two transports run together:
- DNS TXT on
_atproto.: filter records starting withdid=, take the single value, fail if multiple distinct values appear. - HTTPS on
https:///.well-known/atproto-did: expect 2xx, body starting withdid:, trimmed.
The concurrency strategy and the conflict policy differ across the three libraries — see shared/divergence-matrix.md §concurrency-strategy. In short: Rust runs both and fails on disagreement (strict-join); TypeScript races and takes the first to resolve (DNS preferred); Go runs DNS first, falls back to HTTPS on miss (sequential). All three are spec-conformant.
DID document shape (what the bidi check asserts)
An atproto-usable DID document must contain all three:
- Handle binding — an entry in
alsoKnownAsof the format://. First syntactically valid entry wins. - Atproto signing key — in
verificationMethod, an entry whoseidends with#atproto,typeis exactlyMultikey,controllerequals the DID,publicKeyMultibaseis set. - PDS service entry — in
service, an entry whoseidends with#atproto_pds,typeis exactlyAtprotoPersonalDataServer,serviceEndpointis an HTTPS URL with only scheme + host + optional port (no path, userinfo, or query).
Any missing piece → the account is "likely broken" (DID spec wording). Treat as a resolution failure for almost all consumer operations.
Bidirectional verification
For any resolution that started from a handle: the resolved DID document's alsoKnownAs must contain at:// (case-insensitive on the handle label). If it does not → the handle does not trust this DID; emit handle.invalid.
Who performs the bidi check differs by library — Rust and TypeScript leave it to the caller by default, Go performs it internally in LookupHandle. See shared/divergence-matrix.md §bidi-check.
DID methods across libraries
| Method | Rust atproto-identity | TypeScript @atproto/identity | Go indigo | | -------------- | ------------------------------- | --------------------------------------------- | ---------------------------------------- | | did:plc:… | Fetches from PLC directory. | DidPlcResolver → PLC directory. | ResolveDIDPLC → PLC directory. | | did:web:… | Fetches /.well-known/did.json. | DidWebResolver → /.well-known/did.json. | ResolveDIDWeb → /.well-known/did.json. | | did:webvh:… | Syntax only. parse_input has no webvh branch; its did:web: check is starts_with("did:web:") with the trailing colon, so webvh strings don't match web either — they fall through to handle validation and return ResolveError::InvalidInput. | Syntax only. No webvh resolver ships in @atproto/identity. | Syntax only. No webvh resolver ships in indigo. |
If you need real did:webvh resolution, wire in a language-specific webvh library alongside the atproto resolver. Do not silently fall back to did:web on a webvh input — webvh requires log integrity verification and a lossy fallback is a security regression. The webvh DID Method spec lives at — transliterate the log-verification rules from there if no library exists for your stack.
handle.invalid
Per the handle spec, the literal string handle.invalid replaces a real handle after DID resolution when any of:
- The bidirectional check failed (
alsoKnownAsdoes not list this handle). - Handle syntax is invalid.
- Handle resolution returned no DID at all after retries.
Do not emit handle.invalid during transient outages (PDS or PLC directory downtime). Retry with backoff first; latching prematurely is a worse UX than a temporary stale render.
Decision rules
- Conflict between DNS and HTTPS? Pick and document one policy: strict-agreement (default in Rust; loud failures, good for PDSes) or prefer-DNS (default in TypeScript/Go; faster, tolerant of broken
/.well-known/). Silent divergence across services manifests as intermittent handle flapping. - Cache successful resolutions? Yes — minutes-to-hours at the application layer. DNS TTL is usually too short. DID documents cache for tens of minutes.
- Cache failed resolutions? Seconds-to-minutes only. Users retry faster than you'd expect.
- Emit
handle.invalideagerly? No. Emit only when the failure is structural (bidi mismatch, syntax, empty result after retries), not transient. - Accept a path-based
did:web:example.com:users:alice? Not in atproto. Reject. - Use
com.atproto.identity.resolveHandleon a PDS instead of rolling a DNS stack? Yes for client apps — it aggregates DNS and HTTPS with the PDS's own caching and saves you from shipping a resolver.
Common pitfalls
Draw from the relevant shared/ file and shared/divergence-matrix.md. The high-impact ones:
- DNS returns two
did=…records with different values — publisher has a stale record. Reject and log; don't pick one silently. /.well-known/atproto-didreturns HTML — host is serving a wildcard 200 page. Resolver must reject onContent-Type/ prefix; don't try to parse.alsoKnownAslacksat://— DID was configured for a different handle. Bidi check fails →handle.invalid.- DID document missing
#atprotoMultikeyor#atproto_pdsservice — document is not atproto-ready. Reject for atproto flows regardless of whether it's a valid generic DID document. - PDS endpoint has a path segment — non-conformant per spec (scheme + host + optional port only). Reject.
did:webvh:…inputs rejected outright — see §"DID methods across libraries". The Rust resolver returnsInvalidInput(webvh falls through to handle validation); TypeScript and Go reject at the resolver entry point. No library silently downgrades todid:web; all three fail loudly, so you must wire in a webvh-aware resolver if your traffic contains webvh DIDs.- Reserved TLDs slipping through — Rust's validator covers 4/9 reserved TLDs; Go covers 8/9; TypeScript covers 9/9. If you need full spec coverage in Rust, add the remainder yourself. See
shared/divergence-matrix.md§reserved-tlds. - Browser-side handle resolution in TypeScript —
@atproto/identityrequires Node'sdns/fetchand does not run in browsers. Use@atproto-labs/handle-resolverwith DoH or XRPC delegation for isomorphic code.
Tools you can call
Prefer these MCP tools when the goal is to compute or validate an identity result rather than teach an implementation how.
lexicon-garden→resolve_identity(input)does parse + resolve + document fetch against a trusted resolver. Use it to generate expected values for cross-language test vectors.atpmcp→resolve_handle_to_did(handle)andresolve_identity(subject)expose local resolver implementations with configurable DNS / PDS.- PDS XRPC →
com.atproto.identity.resolveHandle?handle=returns{ did: "…" }on success. Use when writing clients that shouldn't ship a DNS stack. - PLC directory →
GET https://plc.directory/returns the current DID document for adid:plc. Also//logand//log/auditfor operation history (outside this skill's scope).
Directory layout
atproto-identity-resolution/
├── SKILL.md # this file — router
├── shared/
│ ├── handle-spec.md # normative handle rules
│ ├── did-spec.md # normative DID rules + document shape
│ ├── resolution-flow.md # end-to-end sequence (normalize → classify → resolve → verify)
│ ├── test-vectors.md # fixtures
│ └── divergence-matrix.md # cross-language differences
├── rust/
│ ├── README.md # crate setup, idioms
│ ├── syntax.md # is_valid_handle / is_valid_did_method_*
│ ├── resolution.md # parse_input, resolve_handle, InnerIdentityResolver
│ └── validation.md # Document helpers, bidi check, handle.invalid
├── typescript/
│ ├── README.md # @atproto/identity + @atproto/syntax + @atproto-labs setup
│ ├── syntax.md # ensureValidHandle / ensureValidDid / INVALID_HANDLE
│ ├── resolution.md # IdResolver / HandleResolver / DidResolver
│ └── validation.md # AtprotoData, bidi verification, signing-key helpers
└── go/
├── README.md # indigo atproto/identity + atproto/syntax setup
├── syntax.md # syntax.ParseHandle / ParseDID / HandleInvalid
├── resolution.md # Directory, BaseDirectory, DefaultDirectory, Lookup*
└── validation.md # Identity struct helpers, built-in bidi check
References
Everything below is reachable from the directory tree above. Listed here for quick grep:
shared/handle-spec.mdshared/did-spec.mdshared/resolution-flow.mdshared/test-vectors.mdshared/divergence-matrix.mdrust/README.md,rust/syntax.md,rust/resolution.md,rust/validation.mdtypescript/README.md,typescript/syntax.md,typescript/resolution.md,typescript/validation.mdgo/README.md,go/syntax.md,go/resolution.md,go/validation.md
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ngerakines
- Source: ngerakines/atproto-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.