A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1package atproto
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9)
10
11// ResolveHoldURL converts a hold identifier (DID or URL) to an HTTP/HTTPS URL
12// Handles both formats for backward compatibility:
13// - DID format: did:web:hold01.atcr.io → https://hold01.atcr.io
14// - DID with port: did:web:172.28.0.3:8080 → http://172.28.0.3:8080
15// - URL format: https://hold.example.com → https://hold.example.com (passthrough)
16func ResolveHoldURL(holdIdentifier string) string {
17 // If it's already a URL (has scheme), return as-is
18 if strings.HasPrefix(holdIdentifier, "http://") || strings.HasPrefix(holdIdentifier, "https://") {
19 return holdIdentifier
20 }
21
22 // If it's a DID, convert to URL
23 if after, ok := strings.CutPrefix(holdIdentifier, "did:web:"); ok {
24 hostname := after
25
26 // Use HTTP for localhost/IP addresses with ports, HTTPS for domains
27 if strings.Contains(hostname, ":") ||
28 strings.Contains(hostname, "127.0.0.1") ||
29 strings.Contains(hostname, "localhost") ||
30 // Check if it's an IP address (contains only digits and dots in first part)
31 (len(hostname) > 0 && hostname[0] >= '0' && hostname[0] <= '9') {
32 return "http://" + hostname
33 }
34 return "https://" + hostname
35 }
36
37 // Fallback: assume it's a hostname and use HTTPS
38 return "https://" + holdIdentifier
39}
40
41// ResolveDIDToPDS resolves a DID to its PDS endpoint.
42// Uses the shared identity directory with cache TTL and event-driven invalidation.
43func ResolveDIDToPDS(ctx context.Context, did string) (string, error) {
44 directory := GetDirectory()
45 didParsed, err := syntax.ParseDID(did)
46 if err != nil {
47 return "", fmt.Errorf("invalid DID: %w", err)
48 }
49
50 ident, err := directory.LookupDID(ctx, didParsed)
51 if err != nil {
52 return "", fmt.Errorf("failed to resolve DID: %w", err)
53 }
54
55 pdsEndpoint := ident.PDSEndpoint()
56 if pdsEndpoint == "" {
57 return "", fmt.Errorf("no PDS endpoint found for DID")
58 }
59
60 return pdsEndpoint, nil
61}
62
63// ResolveIdentity resolves an ATProto identifier (handle or DID) to DID, handle, and PDS endpoint.
64// Uses the shared identity directory with cache TTL and event-driven invalidation.
65//
66// If the handle is invalid (handle.invalid), it returns the DID as the handle for display purposes.
67// Returns: did, handle, pdsEndpoint, error
68func ResolveIdentity(ctx context.Context, identifier string) (string, string, string, error) {
69 directory := GetDirectory()
70 atID, err := syntax.ParseAtIdentifier(identifier)
71 if err != nil {
72 return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err)
73 }
74
75 ident, err := directory.Lookup(ctx, *atID)
76 if err != nil {
77 return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err)
78 }
79
80 did := ident.DID.String()
81 handle := ident.Handle.String()
82 pdsEndpoint := ident.PDSEndpoint()
83
84 // If handle is invalid, use DID as display name
85 if handle == "handle.invalid" || handle == "" {
86 handle = did
87 }
88
89 // PDS endpoint is required for XRPC calls
90 if pdsEndpoint == "" {
91 return "", "", "", fmt.Errorf("no PDS endpoint found for identifier %q", identifier)
92 }
93
94 return did, handle, pdsEndpoint, nil
95}
96
97// ResolveHandleToDID resolves a handle or DID to just the DID.
98// Uses the shared identity directory with cache TTL and event-driven invalidation.
99// This is useful when you only need the DID and don't care about handle/PDS.
100func ResolveHandleToDID(ctx context.Context, identifier string) (string, error) {
101 directory := GetDirectory()
102 atID, err := syntax.ParseAtIdentifier(identifier)
103 if err != nil {
104 return "", fmt.Errorf("invalid identifier: %w", err)
105 }
106
107 ident, err := directory.Lookup(ctx, *atID)
108 if err != nil {
109 return "", err
110 }
111
112 return ident.DID.String(), nil
113}
114
115// InvalidateIdentity purges cached identity data for a DID or handle.
116// This should be called when identity changes are detected (e.g., via Jetstream events)
117// to ensure the cache is refreshed on the next lookup.
118//
119// Use cases:
120// - Handle changes (identity events from Jetstream)
121// - Account deactivation/migration (account events from Jetstream)
122// - PDS migrations (deactivation followed by reactivation at new PDS)
123func InvalidateIdentity(ctx context.Context, identifier string) error {
124 directory := GetDirectory()
125 atID, err := syntax.ParseAtIdentifier(identifier)
126 if err != nil {
127 return fmt.Errorf("invalid identifier for cache invalidation: %w", err)
128 }
129
130 return directory.Purge(ctx, *atID)
131}