package atproto import ( "context" "fmt" "strings" "github.com/bluesky-social/indigo/atproto/syntax" ) // ResolveHoldURL converts a hold identifier (DID or URL) to an HTTP/HTTPS URL // Handles both formats for backward compatibility: // - DID format: did:web:hold01.atcr.io → https://hold01.atcr.io // - DID with port: did:web:172.28.0.3:8080 → http://172.28.0.3:8080 // - URL format: https://hold.example.com → https://hold.example.com (passthrough) func ResolveHoldURL(holdIdentifier string) string { // If it's already a URL (has scheme), return as-is if strings.HasPrefix(holdIdentifier, "http://") || strings.HasPrefix(holdIdentifier, "https://") { return holdIdentifier } // If it's a DID, convert to URL if after, ok := strings.CutPrefix(holdIdentifier, "did:web:"); ok { hostname := after // Use HTTP for localhost/IP addresses with ports, HTTPS for domains if strings.Contains(hostname, ":") || strings.Contains(hostname, "127.0.0.1") || strings.Contains(hostname, "localhost") || // Check if it's an IP address (contains only digits and dots in first part) (len(hostname) > 0 && hostname[0] >= '0' && hostname[0] <= '9') { return "http://" + hostname } return "https://" + hostname } // Fallback: assume it's a hostname and use HTTPS return "https://" + holdIdentifier } // ResolveDIDToPDS resolves a DID to its PDS endpoint. // Uses the shared identity directory with cache TTL and event-driven invalidation. func ResolveDIDToPDS(ctx context.Context, did string) (string, error) { directory := GetDirectory() didParsed, err := syntax.ParseDID(did) if err != nil { return "", fmt.Errorf("invalid DID: %w", err) } ident, err := directory.LookupDID(ctx, didParsed) if err != nil { return "", fmt.Errorf("failed to resolve DID: %w", err) } pdsEndpoint := ident.PDSEndpoint() if pdsEndpoint == "" { return "", fmt.Errorf("no PDS endpoint found for DID") } return pdsEndpoint, nil } // ResolveIdentity resolves an ATProto identifier (handle or DID) to DID, handle, and PDS endpoint. // Uses the shared identity directory with cache TTL and event-driven invalidation. // // If the handle is invalid (handle.invalid), it returns the DID as the handle for display purposes. // Returns: did, handle, pdsEndpoint, error func ResolveIdentity(ctx context.Context, identifier string) (string, string, string, error) { directory := GetDirectory() atID, err := syntax.ParseAtIdentifier(identifier) if err != nil { return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err) } ident, err := directory.Lookup(ctx, *atID) if err != nil { return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err) } did := ident.DID.String() handle := ident.Handle.String() pdsEndpoint := ident.PDSEndpoint() // If handle is invalid, use DID as display name if handle == "handle.invalid" || handle == "" { handle = did } // PDS endpoint is required for XRPC calls if pdsEndpoint == "" { return "", "", "", fmt.Errorf("no PDS endpoint found for identifier %q", identifier) } return did, handle, pdsEndpoint, nil } // ResolveHandleToDID resolves a handle or DID to just the DID. // Uses the shared identity directory with cache TTL and event-driven invalidation. // This is useful when you only need the DID and don't care about handle/PDS. func ResolveHandleToDID(ctx context.Context, identifier string) (string, error) { directory := GetDirectory() atID, err := syntax.ParseAtIdentifier(identifier) if err != nil { return "", fmt.Errorf("invalid identifier: %w", err) } ident, err := directory.Lookup(ctx, *atID) if err != nil { return "", err } return ident.DID.String(), nil } // InvalidateIdentity purges cached identity data for a DID or handle. // This should be called when identity changes are detected (e.g., via Jetstream events) // to ensure the cache is refreshed on the next lookup. // // Use cases: // - Handle changes (identity events from Jetstream) // - Account deactivation/migration (account events from Jetstream) // - PDS migrations (deactivation followed by reactivation at new PDS) func InvalidateIdentity(ctx context.Context, identifier string) error { directory := GetDirectory() atID, err := syntax.ParseAtIdentifier(identifier) if err != nil { return fmt.Errorf("invalid identifier for cache invalidation: %w", err) } return directory.Purge(ctx, *atID) }