A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1package main
2
3import (
4 "context"
5 "flag"
6 "fmt"
7 "log"
8 "net/http"
9 "os"
10 "time"
11
12 "atcr.io/pkg/atproto"
13 "atcr.io/pkg/auth/oauth"
14 indigo_oauth "github.com/bluesky-social/indigo/atproto/auth/oauth"
15)
16
17func main() {
18 handle := flag.String("handle", "", "Your Bluesky handle (e.g., yourname.bsky.social)")
19 holdURL := flag.String("hold-url", "http://localhost:8080", "Hold service URL")
20 repo := flag.String("repo", "", "Repository DID (e.g., did:web:172.28.0.3:8080)")
21 collection := flag.String("collection", "io.atcr.hold.crew", "Collection to delete from")
22 rkey := flag.String("rkey", "", "Record key to delete")
23
24 flag.Parse()
25
26 if *handle == "" {
27 fmt.Println("Usage: oauth-helper --handle yourname.bsky.social [options]")
28 fmt.Println("\nOptions:")
29 flag.PrintDefaults()
30 os.Exit(1)
31 }
32
33 ctx := context.Background()
34
35 fmt.Printf("🔐 Starting OAuth flow for %s...\n\n", *handle)
36
37 // Create a simple HTTP server for the callback
38 mux := http.NewServeMux()
39 server := &http.Server{
40 Addr: ":8765",
41 Handler: mux,
42 }
43
44 // Channel to receive the result
45 resultChan := make(chan *oauth.InteractiveResult, 1)
46 errorChan := make(chan error, 1)
47
48 // Register callback handler
49 registerCallback := func(handler http.HandlerFunc) error {
50 mux.HandleFunc("/auth/oauth/callback", handler)
51 return nil
52 }
53
54 // Display auth URL (will open browser)
55 displayAuthURL := func(authURL string) error {
56 fmt.Printf("🌐 Opening browser for authorization...\n")
57 fmt.Printf(" URL: %s\n\n", authURL)
58 fmt.Printf(" If the browser doesn't open, visit the URL above.\n\n")
59 return oauth.OpenBrowser(authURL)
60 }
61
62 // Start server in background
63 go func() {
64 if err := server.ListenAndServe(); err != http.ErrServerClosed {
65 errorChan <- fmt.Errorf("server error: %w", err)
66 }
67 }()
68
69 // Give server time to start
70 time.Sleep(100 * time.Millisecond)
71
72 // Run interactive OAuth flow
73 go func() {
74 result, err := oauth.InteractiveFlowWithCallback(
75 ctx,
76 "http://localhost:8765",
77 *handle,
78 nil, // Use default scopes
79 registerCallback,
80 displayAuthURL,
81 )
82 if err != nil {
83 errorChan <- err
84 return
85 }
86 resultChan <- result
87 }()
88
89 // Wait for result
90 var result *oauth.InteractiveResult
91 select {
92 case result = <-resultChan:
93 fmt.Printf("✅ OAuth successful!\n\n")
94 case err := <-errorChan:
95 log.Fatalf("❌ OAuth failed: %v\n", err)
96 case <-time.After(5 * time.Minute):
97 log.Fatalf("❌ OAuth timed out\n")
98 }
99
100 // Shutdown server
101 server.Shutdown(ctx)
102
103 // Print session information
104 fmt.Printf("DID: %s\n", result.SessionData.AccountDID)
105 fmt.Printf("Access Token: %s\n", result.SessionData.AccessToken)
106 fmt.Printf("DPoP Key: %s\n\n", result.SessionData.DPoPPrivateKeyMultibase)
107
108 // Generate DPoP proof for deleteRecord endpoint if all params provided
109 if *repo != "" && *rkey != "" {
110 deleteURL := fmt.Sprintf("%s%s?repo=%s&collection=%s&rkey=%s",
111 *holdURL, atproto.RepoDeleteRecord, *repo, *collection, *rkey)
112
113 dpopProof, err := generateDPoPProof(result.Session, "POST", deleteURL)
114 if err != nil {
115 log.Fatalf("❌ Failed to generate DPoP proof: %v\n", err)
116 }
117
118 fmt.Printf("📋 Ready-to-use curl command:\n\n")
119 fmt.Printf("curl -X POST \\\n")
120 fmt.Printf(" -H \"Authorization: DPoP %s\" \\\n", result.SessionData.AccessToken)
121 fmt.Printf(" -H \"DPoP: %s\" \\\n", dpopProof)
122 fmt.Printf(" \"%s\"\n", deleteURL)
123 } else {
124 fmt.Printf("💡 To generate a curl command for deleteRecord, provide:\n")
125 fmt.Printf(" --repo <did>\n")
126 fmt.Printf(" --collection <collection>\n")
127 fmt.Printf(" --rkey <rkey>\n")
128 }
129}
130
131// generateDPoPProof generates a DPoP proof JWT for a specific request
132func generateDPoPProof(session *indigo_oauth.ClientSession, method, reqURL string) (string, error) {
133 // Use the session's NewHostDPoP method to generate the proof
134 return session.NewHostDPoP(method, reqURL)
135}