132 lines
4.3 KiB
Go
132 lines
4.3 KiB
Go
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)
|
|
}
|