···1414 "atcr.io/pkg/atproto"
1515 "atcr.io/pkg/auth/oauth"
1616 "github.com/bluesky-social/indigo/atproto/identity"
1717- "github.com/bluesky-social/indigo/atproto/syntax"
1817 "github.com/go-chi/chi/v5"
1918)
2019···257256258257// resolveIdentityToDID is a helper function that resolves a handle or DID to a DID
259258func resolveIdentityToDID(ctx context.Context, directory identity.Directory, identityStr string) (string, error) {
260260- // Parse as AT identifier (handle or DID)
261261- atID, err := syntax.ParseAtIdentifier(identityStr)
262262- if err != nil {
263263- return "", err
264264- }
265265-266266- // Resolve to DID via directory
267267- ident, err := directory.Lookup(ctx, *atID)
268268- if err != nil {
269269- return "", err
270270- }
271271-272272- return ident.DID.String(), nil
259259+ // Resolve to DID via directory (handles both handles and DIDs)
260260+ return atproto.ResolveHandleToDID(ctx, identityStr)
273261}
+1-13
pkg/appview/jetstream/backfill.go
···99 "strings"
1010 "time"
11111212- "github.com/bluesky-social/indigo/atproto/syntax"
1313-1412 "atcr.io/pkg/appview/db"
1513 "atcr.io/pkg/atproto"
1614)
···137135 }
138136139137 // Resolve DID to get user's PDS endpoint
140140- didParsed, err := syntax.ParseDID(did)
141141- if err != nil {
142142- return 0, fmt.Errorf("invalid DID %s: %w", did, err)
143143- }
144144-145145- ident, err := b.processor.directory.LookupDID(ctx, didParsed)
138138+ pdsEndpoint, err := atproto.ResolveDIDToPDS(ctx, did)
146139 if err != nil {
147140 return 0, fmt.Errorf("failed to resolve DID to PDS: %w", err)
148148- }
149149-150150- pdsEndpoint := ident.PDSEndpoint()
151151- if pdsEndpoint == "" {
152152- return 0, fmt.Errorf("no PDS endpoint found for DID %s", did)
153141 }
154142155143 // Create a client for this user's PDS with the user's DID
+2-26
pkg/appview/jetstream/processor.go
···99 "strings"
1010 "time"
11111212- "github.com/bluesky-social/indigo/atproto/identity"
1313- "github.com/bluesky-social/indigo/atproto/syntax"
1414-1512 "atcr.io/pkg/appview/db"
1613 "atcr.io/pkg/atproto"
1714)
···2017// This eliminates code duplication between the two data ingestion paths
2118type Processor struct {
2219 db *sql.DB
2323- directory identity.Directory
2420 userCache *UserCache // Optional - enabled for Worker, disabled for Backfill
2521 useCache bool
2622}
···3026func NewProcessor(database *sql.DB, useCache bool) *Processor {
3127 p := &Processor{
3228 db: database,
3333- directory: atproto.GetDirectory(),
3429 useCache: useCache,
3530 }
3631···6257 }
63586459 // Resolve DID to get handle and PDS endpoint
6565- didParsed, err := syntax.ParseDID(did)
6060+ resolvedDID, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, did)
6661 if err != nil {
6767- return fmt.Errorf("failed to parse DID: %w", err)
6868- }
6969-7070- ident, err := p.directory.LookupDID(ctx, didParsed)
7171- if err != nil {
7272- return fmt.Errorf("failed to lookup DID: %w", err)
7373- }
7474-7575- resolvedDID := ident.DID.String()
7676- handle := ident.Handle.String()
7777- pdsEndpoint := ident.PDSEndpoint()
7878-7979- // If handle is invalid, use DID as display name
8080- if handle == "handle.invalid" || handle == "" {
8181- handle = resolvedDID
8282- }
8383-8484- // PDS endpoint is required - we can't make XRPC calls without it
8585- if pdsEndpoint == "" {
8686- return fmt.Errorf("no PDS endpoint found for DID: %s", resolvedDID)
6262+ return err
8763 }
88648965 // Fetch user's Bluesky profile record from their PDS (including avatar)
+3-23
pkg/appview/middleware/registry.go
···88 "strings"
99 "sync"
10101111- "github.com/bluesky-social/indigo/atproto/identity"
1212- "github.com/bluesky-social/indigo/atproto/syntax"
1311 "github.com/distribution/distribution/v3"
1412 "github.com/distribution/distribution/v3/registry/api/errcode"
1513 registrymw "github.com/distribution/distribution/v3/registry/middleware/registry"
···6866// NamespaceResolver wraps a namespace and resolves names
6967type NamespaceResolver struct {
7068 distribution.Namespace
7171- directory identity.Directory
7269 defaultHoldDID string // Default hold DID (e.g., "did:web:hold01.atcr.io")
7370 baseURL string // Base URL for error messages (e.g., "https://atcr.io")
7471 testMode bool // If true, fallback to default hold when user's hold is unreachable
···81788279// initATProtoResolver initializes the name resolution middleware
8380func initATProtoResolver(ctx context.Context, ns distribution.Namespace, _ driver.StorageDriver, options map[string]any) (distribution.Namespace, error) {
8484- // Use shared directory with 8h cache TTL
8585- directory := atproto.GetDirectory()
8686-8781 // Get default hold DID from config (required)
8882 // Expected format: "did:web:hold01.atcr.io"
8983 defaultHoldDID := ""
···107101 // This avoids accessing globals during request handling
108102 return &NamespaceResolver{
109103 Namespace: ns,
110110- directory: directory,
111104 defaultHoldDID: defaultHoldDID,
112105 baseURL: baseURL,
113106 testMode: testMode,
···142135 identityStr := parts[0]
143136 imageName := parts[1]
144137145145- // Parse identity (handle or DID)
146146- atID, err := syntax.ParseAtIdentifier(identityStr)
147147- if err != nil {
148148- return nil, fmt.Errorf("invalid identity %s: %w", identityStr, err)
149149- }
150150-151151- // Resolve identity to DID and PDS using indigo's directory
152152- ident, err := nr.directory.Lookup(ctx, *atID)
138138+ // Resolve identity to DID, handle, and PDS endpoint
139139+ did, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, identityStr)
153140 if err != nil {
154154- return nil, fmt.Errorf("failed to resolve identity %s: %w", identityStr, err)
155155- }
156156-157157- did := ident.DID.String()
158158- handle := ident.Handle.String()
159159- pdsEndpoint := ident.PDSEndpoint()
160160- if pdsEndpoint == "" {
161161- return nil, fmt.Errorf("no PDS endpoint found for %s", identityStr)
141141+ return nil, err
162142 }
163143164144 slog.Debug("Resolved identity", "component", "registry/middleware", "did", did, "pds", pdsEndpoint, "handle", handle)
+82
pkg/atproto/resolver.go
···11+package atproto
22+33+import (
44+ "context"
55+ "fmt"
66+77+ "github.com/bluesky-social/indigo/atproto/syntax"
88+)
99+1010+// ResolveDIDToPDS resolves a DID to its PDS endpoint.
1111+// Uses the shared identity directory with 8h cache TTL.
1212+func ResolveDIDToPDS(ctx context.Context, did string) (string, error) {
1313+ directory := GetDirectory()
1414+ didParsed, err := syntax.ParseDID(did)
1515+ if err != nil {
1616+ return "", fmt.Errorf("invalid DID: %w", err)
1717+ }
1818+1919+ ident, err := directory.LookupDID(ctx, didParsed)
2020+ if err != nil {
2121+ return "", fmt.Errorf("failed to resolve DID: %w", err)
2222+ }
2323+2424+ pdsEndpoint := ident.PDSEndpoint()
2525+ if pdsEndpoint == "" {
2626+ return "", fmt.Errorf("no PDS endpoint found for DID")
2727+ }
2828+2929+ return pdsEndpoint, nil
3030+}
3131+3232+// ResolveIdentity resolves an ATProto identifier (handle or DID) to DID, handle, and PDS endpoint.
3333+// Uses the shared identity directory with 8h cache TTL.
3434+//
3535+// If the handle is invalid (handle.invalid), it returns the DID as the handle for display purposes.
3636+// Returns: did, handle, pdsEndpoint, error
3737+func ResolveIdentity(ctx context.Context, identifier string) (string, string, string, error) {
3838+ directory := GetDirectory()
3939+ atID, err := syntax.ParseAtIdentifier(identifier)
4040+ if err != nil {
4141+ return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err)
4242+ }
4343+4444+ ident, err := directory.Lookup(ctx, *atID)
4545+ if err != nil {
4646+ return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err)
4747+ }
4848+4949+ did := ident.DID.String()
5050+ handle := ident.Handle.String()
5151+ pdsEndpoint := ident.PDSEndpoint()
5252+5353+ // If handle is invalid, use DID as display name
5454+ if handle == "handle.invalid" || handle == "" {
5555+ handle = did
5656+ }
5757+5858+ // PDS endpoint is required for XRPC calls
5959+ if pdsEndpoint == "" {
6060+ return "", "", "", fmt.Errorf("no PDS endpoint found for identifier %q", identifier)
6161+ }
6262+6363+ return did, handle, pdsEndpoint, nil
6464+}
6565+6666+// ResolveHandleToDID resolves a handle or DID to just the DID.
6767+// Uses the shared identity directory with 8h cache TTL.
6868+// This is useful when you only need the DID and don't care about handle/PDS.
6969+func ResolveHandleToDID(ctx context.Context, identifier string) (string, error) {
7070+ directory := GetDirectory()
7171+ atID, err := syntax.ParseAtIdentifier(identifier)
7272+ if err != nil {
7373+ return "", fmt.Errorf("invalid identifier: %w", err)
7474+ }
7575+7676+ ident, err := directory.Lookup(ctx, *atID)
7777+ if err != nil {
7878+ return "", err
7979+ }
8080+8181+ return ident.DID.String(), nil
8282+}
-22
pkg/auth/oauth/client.go
···177177178178 return true
179179}
180180-181181-// ResolveDIDToPDS resolves a DID to its PDS endpoint (for reference)
182182-// This is an alternative approach if we don't trust the token's issuer claim
183183-func ResolveDIDToPDS(ctx context.Context, did string) (string, error) {
184184- directory := atproto.GetDirectory()
185185- didParsed, err := syntax.ParseDID(did)
186186- if err != nil {
187187- return "", fmt.Errorf("invalid DID: %w", err)
188188- }
189189-190190- ident, err := directory.LookupDID(ctx, didParsed)
191191- if err != nil {
192192- return "", fmt.Errorf("failed to resolve DID: %w", err)
193193- }
194194-195195- pdsEndpoint := ident.PDSEndpoint()
196196- if pdsEndpoint == "" {
197197- return "", fmt.Errorf("no PDS endpoint found for DID")
198198- }
199199-200200- return pdsEndpoint, nil
201201-}
+4-3
pkg/auth/oauth/server.go
···88 "net/http"
99 "strings"
1010 "time"
1111+1212+ "atcr.io/pkg/atproto"
1113)
12141315// UISessionStore is the interface for UI session management
···126128 slog.Debug("Invalidated cached session after creating new session", "did", did)
127129 }
128130129129- // Look up identity
130130- ident, err := s.app.directory.LookupDID(r.Context(), sessionData.AccountDID)
131131- handle := ident.Handle.String()
131131+ // Look up identity (resolve DID to handle)
132132+ _, handle, _, err := atproto.ResolveIdentity(r.Context(), did)
132133 if err != nil {
133134 slog.Warn("Failed to resolve DID to handle, using DID as fallback", "error", err, "did", did)
134135 handle = did // Fallback to DID if resolution fails