From 2bb451ac4de73d6de14677b1a3925e223bb64fbf Mon Sep 17 00:00:00 2001 From: hectorsector Date: Tue, 24 Mar 2026 12:20:04 -0400 Subject: [PATCH] handle discovery and PDS resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the full discovery chain from Bluesky handle to auth server metadata: handle → DID (DNS/HTTP), DID → PDS (plc.directory), PDS → auth server (oauth-protected-resource), auth server → AuthServerMeta. Adds cmd/discover CLI tool and docs/plc.jsonc shape reference. --- .gitignore | 1 + cmd/discover/main.go | 29 +++++++++++++++++++++ docs/plc.jsonc | 40 +++++++++++++++++++++++++++++ internal/auth/discover.go | 53 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100755 cmd/discover/main.go create mode 100644 docs/plc.jsonc diff --git a/.gitignore b/.gitignore index ec76d8c..0bb160c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.claude CLAUDE.md BUILDPLAN.md diff --git a/cmd/discover/main.go b/cmd/discover/main.go new file mode 100755 index 0000000..0f36e9f --- /dev/null +++ b/cmd/discover/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + + "tangled.org/hectorsector.com/cairns/internal/auth" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: discover ") + os.Exit(1) + } + + handle := os.Args[1] + session, err := auth.DiscoverFromhandle(handle) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + fmt.Printf("%-8s %s\n", "DID:", session.DID) + fmt.Printf("%-8s %s\n", "PDS:", session.PDSUrl) + fmt.Printf("%-8s %s\n", "Auth:", session.AuthMeta.Issuer) + fmt.Printf("%-8s %s\n", "Token:", session.AuthMeta.TokenEndpoint) + fmt.Printf("%-8s %s\n", "PAR:", session.AuthMeta.PushedAuthorizationRequestEndpoint) + +} diff --git a/docs/plc.jsonc b/docs/plc.jsonc new file mode 100644 index 0000000..648c32d --- /dev/null +++ b/docs/plc.jsonc @@ -0,0 +1,40 @@ +{ + // -> struct + + // _ignore + // "@context": [ + // "https://www.w3.org/ns/did/v1", + // "https://w3id.org/security/multikey/v1", + // "https://w3id.org/security/suites/secp256k1-2019/v1", + // ], + // "alsoKnownAs": ["at://adomain.com"], + + // _ignore + // "id": "did:plc:abc123", + + // -> struct + "service": + // -> array + [ + // -> struct + { + // _ignore + // "id": "#atproto_pds", + + // -> string + "serviceEndpoint": "https://chaga.us-west.host.bsky.network", + + // _ignore + // "type": "AtprotoPersonalDataServer", + }, + ], + // _ignore + // "verificationMethod": [ + // { + // "controller": "did:plc:abc123", + // "id": "did:plc:abc123#atproto", + // "publicKeyMultibase": "abcxyz", + // "type": "Multikey", + // }, + // ], +} diff --git a/internal/auth/discover.go b/internal/auth/discover.go index 4ac158e..40162ce 100644 --- a/internal/auth/discover.go +++ b/internal/auth/discover.go @@ -1,6 +1,7 @@ package auth import ( + "encoding/json" "fmt" "io" "net" @@ -8,6 +9,19 @@ import ( "strings" ) +type Service struct { + ServiceEndpoint string `json:"serviceEndpoint"` +} + +type ProtectedResourceMeta struct { + AuthorizationServers []string `json:"authorization_servers"` +} + +// ref: https://atproto.com/specs/did +type DIDDoc struct { + Service []Service `json:"service"` +} + // ref: https://atproto.com/specs/oauth#server-metadata type AuthServerMeta struct { Issuer string `json:"issuer"` @@ -26,7 +40,32 @@ type DiscoveredSession struct { // we expect handle to be clean of leading @ func DiscoverFromhandle(handle string) (DiscoveredSession, error) { - // todo + did, _ := resolveHandleToDID(handle) + pds, _ := resolveDIDToPDSUrl(did) + + // ref: https://atproto.com/specs/oauth#server-metadata + + // 1. get auth server url + respAsUrl, _ := http.Get(pds + "/.well-known/oauth-protected-resource") + defer respAsUrl.Body.Close() + b, _ := io.ReadAll(respAsUrl.Body) + var prm ProtectedResourceMeta + json.Unmarshal(b, &prm) + asUrl := prm.AuthorizationServers[0] + + // 2. get the as meta + respAsMeta, _ := http.Get(asUrl + "/.well-known/oauth-authorization-server") + defer respAsMeta.Body.Close() + basm, _ := io.ReadAll(respAsMeta.Body) + + var asm AuthServerMeta + json.Unmarshal(basm, &asm) + + return DiscoveredSession{ + DID: did, + PDSUrl: pds, + AuthMeta: asm, + }, nil } func resolveHandleToDID(handle string) (string, error) { @@ -59,3 +98,15 @@ func resolveHandleToDID(handle string) (string, error) { return did, nil } + +func resolveDIDToPDSUrl(did string) (string, error) { + resp, _ := http.Get("https://plc.directory/" + did) + defer resp.Body.Close() + + b, _ := io.ReadAll(resp.Body) + + var doc DIDDoc + json.Unmarshal(b, &doc) + + return string(doc.Service[0].ServiceEndpoint), nil +} -- 2.51.2