dev vouch dev on at. thats about it atvouch.dev
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

add check command

Luna eb41f985 ee927eb5

+279 -1
+279 -1
cli/main.go
··· 6 6 "encoding/json" 7 7 "errors" 8 8 "fmt" 9 + "io" 9 10 "log" 10 11 "net" 11 12 "net/http" ··· 16 17 "strings" 17 18 "time" 18 19 20 + "github.com/bluesky-social/indigo/atproto/atclient" 19 21 "github.com/bluesky-social/indigo/atproto/auth/oauth" 20 22 "github.com/bluesky-social/indigo/atproto/syntax" 21 23 "github.com/spf13/cobra" ··· 54 56 }, 55 57 } 56 58 57 - rootCmd.AddCommand(loginCmd, meCmd, createCmd) 59 + checkCmd := &cobra.Command{ 60 + Use: "check <handle>", 61 + Short: "Check vouch paths to a user", 62 + Args: cobra.ExactArgs(1), 63 + RunE: func(cmd *cobra.Command, args []string) error { 64 + return check(cmd.Context(), args[0]) 65 + }, 66 + } 67 + 68 + rootCmd.AddCommand(loginCmd, meCmd, createCmd, checkCmd) 58 69 59 70 if err := rootCmd.ExecuteContext(context.Background()); err != nil { 60 71 os.Exit(1) ··· 217 228 fmt.Printf("Record: %s\n", createResp.URI) 218 229 219 230 return nil 231 + } 232 + 233 + func check(ctx context.Context, handle string) error { 234 + session, err := resumeSession(ctx) 235 + if err != nil { 236 + return err 237 + } 238 + 239 + client := session.APIClient() 240 + myDID := session.Data.AccountDID.String() 241 + 242 + // Resolve target handle to DID 243 + targetDID, err := slingshotResolveHandle(handle) 244 + if err != nil { 245 + return fmt.Errorf("resolving handle %q: %w", handle, err) 246 + } 247 + 248 + fmt.Printf("Checking vouch paths to %s (%s)...\n", handle, targetDID) 249 + 250 + // Fetch my vouches (people I vouch for) 251 + myVouches, err := listVouchSubjects(ctx, client, myDID) 252 + if err != nil { 253 + return fmt.Errorf("fetching your vouches: %w", err) 254 + } 255 + 256 + // Direct vouch check (depth 1) 257 + for _, did := range myVouches { 258 + if did == targetDID { 259 + fmt.Printf("\nyou -> %s\n", handle) 260 + return nil 261 + } 262 + } 263 + 264 + // Build reverse graph from target using microcosm (up to 3 levels back) 265 + // reverseGraph[did] = set of DIDs that vouch for did 266 + reverseGraph := make(map[string]map[string]bool) 267 + 268 + // Level 1: who vouches for target 269 + level1, err := fetchVouchersFromMicrocosm(targetDID) 270 + if err != nil { 271 + return fmt.Errorf("querying microcosm: %w", err) 272 + } 273 + reverseGraph[targetDID] = toSet(level1) 274 + 275 + // Level 2: who vouches for each level-1 voucher 276 + level2DIDs := []string{} 277 + for _, did := range level1 { 278 + vouchers, err := fetchVouchersFromMicrocosm(did) 279 + if err != nil { 280 + return fmt.Errorf("querying microcosm: %w", err) 281 + } 282 + reverseGraph[did] = toSet(vouchers) 283 + level2DIDs = append(level2DIDs, vouchers...) 284 + } 285 + 286 + // Level 3: who vouches for each level-2 voucher 287 + for _, did := range level2DIDs { 288 + if _, exists := reverseGraph[did]; exists { 289 + continue // already fetched 290 + } 291 + vouchers, err := fetchVouchersFromMicrocosm(did) 292 + if err != nil { 293 + return fmt.Errorf("querying microcosm: %w", err) 294 + } 295 + reverseGraph[did] = toSet(vouchers) 296 + } 297 + 298 + // Find all paths: me -> (someone I vouch for) -> ... -> target 299 + // A path me -> A -> B -> target means: 300 + // I vouch for A, A vouches for B, B vouches for target 301 + // In reverseGraph terms: B is in reverseGraph[target], A is in reverseGraph[B] 302 + myVouchSet := toSet(myVouches) 303 + var paths [][]string 304 + 305 + // Depth 2: me -> X -> target (X vouches for target, I vouch for X) 306 + for voucher := range reverseGraph[targetDID] { 307 + if myVouchSet[voucher] { 308 + paths = append(paths, []string{myDID, voucher, targetDID}) 309 + } 310 + } 311 + 312 + // Depth 3: me -> X -> Y -> target (Y vouches for target, X vouches for Y, I vouch for X) 313 + for yDID := range reverseGraph[targetDID] { 314 + for xDID := range reverseGraph[yDID] { 315 + if myVouchSet[xDID] { 316 + paths = append(paths, []string{myDID, xDID, yDID, targetDID}) 317 + } 318 + } 319 + } 320 + 321 + if len(paths) == 0 { 322 + fmt.Println("no vouch routes found") 323 + return nil 324 + } 325 + 326 + // Resolve all unique DIDs to handles for display 327 + uniqueDIDs := make(map[string]bool) 328 + for _, path := range paths { 329 + for _, did := range path { 330 + uniqueDIDs[did] = true 331 + } 332 + } 333 + 334 + handleMap := make(map[string]string) 335 + handleMap[targetDID] = handle // we already know this one 336 + for did := range uniqueDIDs { 337 + if _, exists := handleMap[did]; exists { 338 + continue 339 + } 340 + resolved, err := slingshotResolveDidToHandle(did) 341 + if err != nil { 342 + handleMap[did] = did // fallback to DID 343 + } else { 344 + handleMap[did] = resolved 345 + } 346 + } 347 + 348 + fmt.Printf("\nFound %d vouch route(s):\n", len(paths)) 349 + for _, path := range paths { 350 + parts := make([]string, len(path)) 351 + for i, did := range path { 352 + parts[i] = handleMap[did] 353 + } 354 + fmt.Println(strings.Join(parts, " -> ")) 355 + } 356 + 357 + return nil 358 + } 359 + 360 + // listVouchSubjects returns the DIDs that the given repo has vouched for. 361 + func listVouchSubjects(ctx context.Context, client *atclient.APIClient, repo string) ([]string, error) { 362 + var subjects []string 363 + var cursor string 364 + 365 + for { 366 + params := map[string]any{ 367 + "repo": repo, 368 + "collection": "dev.atvouch.graph.vouch", 369 + "limit": 100, 370 + } 371 + if cursor != "" { 372 + params["cursor"] = cursor 373 + } 374 + 375 + var resp struct { 376 + Records []struct { 377 + Value struct { 378 + Subject string `json:"subject"` 379 + } `json:"value"` 380 + } `json:"records"` 381 + Cursor *string `json:"cursor"` 382 + } 383 + 384 + if err := client.Get(ctx, "com.atproto.repo.listRecords", params, &resp); err != nil { 385 + return nil, err 386 + } 387 + 388 + for _, rec := range resp.Records { 389 + if rec.Value.Subject != "" { 390 + subjects = append(subjects, rec.Value.Subject) 391 + } 392 + } 393 + 394 + if resp.Cursor == nil || *resp.Cursor == "" { 395 + break 396 + } 397 + cursor = *resp.Cursor 398 + } 399 + 400 + return subjects, nil 401 + } 402 + 403 + // fetchVouchersFromMicrocosm returns DIDs that have vouched for the given target DID. 404 + func fetchVouchersFromMicrocosm(targetDID string) ([]string, error) { 405 + u := "https://constellation.microcosm.blue/links/distinct-dids?" + url.Values{ 406 + "target": {targetDID}, 407 + "collection": {"dev.atvouch.graph.vouch"}, 408 + "path": {".subject"}, 409 + }.Encode() 410 + 411 + req, err := http.NewRequest("GET", u, nil) 412 + if err != nil { 413 + return nil, err 414 + } 415 + req.Header.Set("Accept", "application/json") 416 + 417 + resp, err := http.DefaultClient.Do(req) 418 + if err != nil { 419 + return nil, err 420 + } 421 + defer resp.Body.Close() 422 + 423 + body, err := io.ReadAll(resp.Body) 424 + if err != nil { 425 + return nil, err 426 + } 427 + 428 + if resp.StatusCode != 200 { 429 + return nil, fmt.Errorf("microcosm returned %d: %s", resp.StatusCode, string(body)) 430 + } 431 + 432 + var result struct { 433 + LinkingDIDs []string `json:"linking_dids"` 434 + } 435 + if err := json.Unmarshal(body, &result); err != nil { 436 + return nil, fmt.Errorf("parsing microcosm response: %w", err) 437 + } 438 + 439 + return result.LinkingDIDs, nil 440 + } 441 + 442 + // slingshotResolveHandle resolves a handle to a DID via slingshot. 443 + func slingshotResolveHandle(handle string) (string, error) { 444 + u := "https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle?" + url.Values{ 445 + "handle": {handle}, 446 + }.Encode() 447 + 448 + resp, err := http.Get(u) 449 + if err != nil { 450 + return "", err 451 + } 452 + defer resp.Body.Close() 453 + 454 + if resp.StatusCode != 200 { 455 + return "", fmt.Errorf("slingshot returned %d", resp.StatusCode) 456 + } 457 + 458 + var result struct { 459 + DID string `json:"did"` 460 + } 461 + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 462 + return "", err 463 + } 464 + return result.DID, nil 465 + } 466 + 467 + // slingshotResolveDidToHandle resolves a DID to a handle via slingshot. 468 + func slingshotResolveDidToHandle(did string) (string, error) { 469 + u := "https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?" + url.Values{ 470 + "identifier": {did}, 471 + }.Encode() 472 + 473 + resp, err := http.Get(u) 474 + if err != nil { 475 + return "", err 476 + } 477 + defer resp.Body.Close() 478 + 479 + if resp.StatusCode != 200 { 480 + return "", fmt.Errorf("slingshot returned %d", resp.StatusCode) 481 + } 482 + 483 + var result struct { 484 + Handle string `json:"handle"` 485 + } 486 + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 487 + return "", err 488 + } 489 + return result.Handle, nil 490 + } 491 + 492 + func toSet(items []string) map[string]bool { 493 + s := make(map[string]bool, len(items)) 494 + for _, item := range items { 495 + s[item] = true 496 + } 497 + return s 220 498 } 221 499 222 500 func listenForCallback(ctx context.Context, res chan url.Values) (int, *http.Server, error) {