package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net" "net/http" "net/url" "os" "os/exec" "runtime" "strings" "time" "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/spf13/cobra" ) func main() { rootCmd := &cobra.Command{ Use: "atvouch", Short: "AT Protocol vouching tool", } loginCmd := &cobra.Command{ Use: "login ", Short: "Authenticate with your PDS via OAuth", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return login(cmd.Context(), args[0]) }, } meCmd := &cobra.Command{ Use: "me", Short: "Show current authenticated session info", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return me(cmd.Context()) }, } createCmd := &cobra.Command{ Use: "create ", Short: "Vouch for a user by their handle", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return create(cmd.Context(), args[0]) }, } checkCmd := &cobra.Command{ Use: "check ", Short: "Check vouch paths to a user", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return check(cmd.Context(), args[0]) }, } rootCmd.AddCommand(loginCmd, meCmd, createCmd, checkCmd) if err := rootCmd.ExecuteContext(context.Background()); err != nil { os.Exit(1) } } func newStore() (*Store, error) { return NewStore() } func newOAuthClient(store *Store, callbackURL string) *oauth.ClientApp { config := oauth.NewLocalhostConfig(callbackURL, []string{ "atproto", "repo:dev.atvouch.graph.vouch", }) return oauth.NewClientApp(&config, store) } func login(ctx context.Context, handle string) error { store, err := newStore() if err != nil { return err } // Start the callback server on a random available port callbackCh := make(chan url.Values, 1) port, server, err := listenForCallback(ctx, callbackCh) if err != nil { return err } defer server.Close() callbackURL := fmt.Sprintf("http://127.0.0.1:%d/callback", port) oauthClient := newOAuthClient(store, callbackURL) // Start the OAuth flow fmt.Printf("Logging in as %s...\n", handle) authURL, err := oauthClient.StartAuthFlow(ctx, handle) if err != nil { return fmt.Errorf("starting auth flow: %w", err) } // Open the browser to the authorization URL fmt.Printf("Opening browser...\n") if !strings.HasPrefix(authURL, "https://") { return fmt.Errorf("unexpected non-https auth URL") } if err := openBrowser(authURL); err != nil { fmt.Printf("Could not open browser automatically.\nPlease visit: %s\n", authURL) } // Wait for the OAuth callback fmt.Println("Waiting for authorization...") params := <-callbackCh // Exchange the authorization code for a session sessData, err := oauthClient.ProcessCallback(ctx, params) if err != nil { return fmt.Errorf("processing callback: %w", err) } // Mark this as the active session if err := store.SetActive(sessData.AccountDID, sessData.SessionID); err != nil { return fmt.Errorf("saving active session: %w", err) } fmt.Printf("Logged in as %s (%s)\n", handle, sessData.AccountDID) return nil } func resumeSession(ctx context.Context) (*oauth.ClientSession, error) { store, err := newStore() if err != nil { return nil, err } active, err := store.GetActive() if err != nil { return nil, err } // We need a callback URL to construct the client config, but we won't // actually start any auth flow here. Use a dummy port. callbackURL := "http://127.0.0.1:0/callback" oauthClient := newOAuthClient(store, callbackURL) session, err := oauthClient.ResumeSession(ctx, active.DID, active.SessionID) if err != nil { return nil, fmt.Errorf("resuming session: %w", err) } return session, nil } func me(ctx context.Context) error { session, err := resumeSession(ctx) if err != nil { return err } client := session.APIClient() var resp json.RawMessage if err := client.Get(ctx, "com.atproto.server.getSession", nil, &resp); err != nil { return fmt.Errorf("fetching session: %w", err) } // Pretty-print the response var pretty bytes.Buffer json.Indent(&pretty, resp, "", " ") fmt.Println(pretty.String()) return nil } func create(ctx context.Context, handle string) error { session, err := resumeSession(ctx) if err != nil { return err } client := session.APIClient() // Resolve handle to DID var resolveResp struct { DID string `json:"did"` } if err := client.Get(ctx, "com.atproto.identity.resolveHandle", map[string]any{ "handle": handle, }, &resolveResp); err != nil { return fmt.Errorf("resolving handle %q: %w", handle, err) } subjectDID, err := syntax.ParseDID(resolveResp.DID) if err != nil { return fmt.Errorf("invalid DID from resolution: %w", err) } // Check if a vouch already exists for this subject var existingRecord struct { URI string `json:"uri"` Value any `json:"value"` } err = client.Get(ctx, "com.atproto.repo.getRecord", map[string]any{ "repo": session.Data.AccountDID, "collection": "dev.atvouch.graph.vouch", "rkey": subjectDID.String(), }, &existingRecord) if err == nil { fmt.Printf("You have already vouched for %s (%s)\n", handle, subjectDID) fmt.Printf("Record: %s\n", existingRecord.URI) return nil } // Create the vouch record record := map[string]any{ "$type": "dev.atvouch.graph.vouch", "subject": subjectDID.String(), "createdAt": time.Now().UTC().Format(time.RFC3339), } var createResp struct { URI string `json:"uri"` CID string `json:"cid"` } if err := client.Post(ctx, "com.atproto.repo.createRecord", map[string]any{ "repo": session.Data.AccountDID, "collection": "dev.atvouch.graph.vouch", "rkey": subjectDID.String(), "record": record, }, &createResp); err != nil { return fmt.Errorf("creating vouch record: %w", err) } fmt.Printf("Vouched for %s (%s)\n", handle, subjectDID) fmt.Printf("Record: %s\n", createResp.URI) return nil } // checkDeps holds injectable dependencies for the check logic. type checkDeps struct { myDID string resolveHandle func(handle string) (string, error) resolveDidToHandle func(did string) (string, error) fetchVouchers func(targetDID string) ([]string, error) listMyVouches func() ([]string, error) } // checkResult holds the output of a check operation. type checkResult struct { targetDID string paths [][]string // each path is a list of DIDs handleMap map[string]string // DID -> handle } func check(ctx context.Context, handle string) error { session, err := resumeSession(ctx) if err != nil { return err } client := session.APIClient() myDID := session.Data.AccountDID.String() deps := checkDeps{ myDID: myDID, resolveHandle: slingshotResolveHandle, resolveDidToHandle: slingshotResolveDidToHandle, fetchVouchers: fetchVouchersFromMicrocosm, listMyVouches: func() ([]string, error) { return listVouchSubjects(ctx, client, myDID) }, } result, err := checkWithDeps(handle, deps) if err != nil { return err } fmt.Printf("Checking vouch paths to %s (%s)...\n", handle, result.targetDID) if result.paths == nil { fmt.Printf("\nyou -> %s\n", handle) return nil } if len(result.paths) == 0 { fmt.Println("no vouch routes found") return nil } fmt.Printf("\nFound %d vouch route(s):\n", len(result.paths)) for _, path := range result.paths { parts := make([]string, len(path)) for i, did := range path { parts[i] = result.handleMap[did] } fmt.Println(strings.Join(parts, " -> ")) } return nil } // checkWithDeps contains the core check logic with injected dependencies. // Returns a checkResult where paths == nil means direct vouch found, // paths == empty means no routes, otherwise contains discovered paths. func checkWithDeps(handle string, deps checkDeps) (*checkResult, error) { targetDID, err := deps.resolveHandle(handle) if err != nil { return nil, fmt.Errorf("resolving handle %q: %w", handle, err) } myVouches, err := deps.listMyVouches() if err != nil { return nil, fmt.Errorf("fetching your vouches: %w", err) } // Direct vouch check (depth 1) for _, did := range myVouches { if did == targetDID { return &checkResult{targetDID: targetDID, paths: nil}, nil } } // Build reverse graph from target using microcosm (up to 3 levels back) // reverseGraph[did] = set of DIDs that vouch for did reverseGraph := make(map[string]map[string]bool) // Level 1: who vouches for target level1, err := deps.fetchVouchers(targetDID) if err != nil { return nil, fmt.Errorf("querying microcosm: %w", err) } reverseGraph[targetDID] = toSet(level1) // Level 2: who vouches for each level-1 voucher level2DIDs := []string{} for _, did := range level1 { vouchers, err := deps.fetchVouchers(did) if err != nil { return nil, fmt.Errorf("querying microcosm: %w", err) } reverseGraph[did] = toSet(vouchers) level2DIDs = append(level2DIDs, vouchers...) } // Level 3: who vouches for each level-2 voucher for _, did := range level2DIDs { if _, exists := reverseGraph[did]; exists { continue // already fetched } vouchers, err := deps.fetchVouchers(did) if err != nil { return nil, fmt.Errorf("querying microcosm: %w", err) } reverseGraph[did] = toSet(vouchers) } // Find all paths: me -> (someone I vouch for) -> ... -> target myVouchSet := toSet(myVouches) var paths [][]string // Depth 2: me -> X -> target (X vouches for target, I vouch for X) for voucher := range reverseGraph[targetDID] { if myVouchSet[voucher] { paths = append(paths, []string{deps.myDID, voucher, targetDID}) } } // Depth 3: me -> X -> Y -> target (Y vouches for target, X vouches for Y, I vouch for X) for yDID := range reverseGraph[targetDID] { for xDID := range reverseGraph[yDID] { if myVouchSet[xDID] { paths = append(paths, []string{deps.myDID, xDID, yDID, targetDID}) } } } // Resolve all unique DIDs to handles for display handleMap := make(map[string]string) if len(paths) > 0 { uniqueDIDs := make(map[string]bool) for _, path := range paths { for _, did := range path { uniqueDIDs[did] = true } } handleMap[targetDID] = handle // we already know this one for did := range uniqueDIDs { if _, exists := handleMap[did]; exists { continue } resolved, err := deps.resolveDidToHandle(did) if err != nil { handleMap[did] = did // fallback to DID } else { handleMap[did] = resolved } } } return &checkResult{ targetDID: targetDID, paths: paths, handleMap: handleMap, }, nil } // listVouchSubjects returns the DIDs that the given repo has vouched for. func listVouchSubjects(ctx context.Context, client *atclient.APIClient, repo string) ([]string, error) { var subjects []string var cursor string for { params := map[string]any{ "repo": repo, "collection": "dev.atvouch.graph.vouch", "limit": 100, } if cursor != "" { params["cursor"] = cursor } var resp struct { Records []struct { Value struct { Subject string `json:"subject"` } `json:"value"` } `json:"records"` Cursor *string `json:"cursor"` } if err := client.Get(ctx, "com.atproto.repo.listRecords", params, &resp); err != nil { return nil, err } for _, rec := range resp.Records { if rec.Value.Subject != "" { subjects = append(subjects, rec.Value.Subject) } } if resp.Cursor == nil || *resp.Cursor == "" { break } cursor = *resp.Cursor } return subjects, nil } // fetchVouchersFromMicrocosm returns DIDs that have vouched for the given target DID. func fetchVouchersFromMicrocosm(targetDID string) ([]string, error) { u := "https://constellation.microcosm.blue/links/distinct-dids?" + url.Values{ "target": {targetDID}, "collection": {"dev.atvouch.graph.vouch"}, "path": {".subject"}, }.Encode() req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != 200 { return nil, fmt.Errorf("microcosm returned %d: %s", resp.StatusCode, string(body)) } var result struct { LinkingDIDs []string `json:"linking_dids"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("parsing microcosm response: %w", err) } return result.LinkingDIDs, nil } // slingshotResolveHandle resolves a handle to a DID via slingshot. func slingshotResolveHandle(handle string) (string, error) { u := "https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle?" + url.Values{ "handle": {handle}, }.Encode() resp, err := http.Get(u) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 200 { return "", fmt.Errorf("slingshot returned %d", resp.StatusCode) } var result struct { DID string `json:"did"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } return result.DID, nil } // slingshotResolveDidToHandle resolves a DID to a handle via slingshot. func slingshotResolveDidToHandle(did string) (string, error) { u := "https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?" + url.Values{ "identifier": {did}, }.Encode() resp, err := http.Get(u) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 200 { return "", fmt.Errorf("slingshot returned %d", resp.StatusCode) } var result struct { Handle string `json:"handle"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } return result.Handle, nil } func toSet(items []string) map[string]bool { s := make(map[string]bool, len(items)) for _, item := range items { s[item] = true } return s } func listenForCallback(ctx context.Context, res chan url.Values) (int, *http.Server, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return 0, nil, err } mux := http.NewServeMux() server := &http.Server{Handler: mux} mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { res <- r.URL.Query() w.Header().Set("Content-Type", "text/html") w.WriteHeader(200) w.Write([]byte("

Authorized! You can close this tab.

")) go server.Shutdown(ctx) }) go func() { if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } }() return listener.Addr().(*net.TCPAddr).Port, server, nil } func openBrowser(url string) error { switch runtime.GOOS { case "darwin": return exec.Command("open", url).Run() case "windows": return exec.Command("cmd", "/c", "start", url).Run() default: return exec.Command("xdg-open", url).Run() } }