From 583492447eb97a8e327f4d40549f84564f526cc7 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Sun, 23 Mar 2025 10:19:40 +0200 Subject: [PATCH] appview: router: no-@ handles and flattened dids Introduces two new user routing options: handles without @'s will redirect to their @'d counterparts, and did-plc-foobar (a flattened did) will redirect to did:plc:foobar. These can now be used as valid Go modules. --- appview/state/pull.go | 2 +- appview/state/router.go | 64 ++++++++++++++++++++ appview/state/router_test.go | 114 +++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 appview/state/router_test.go diff --git a/appview/state/pull.go b/appview/state/pull.go index c20623c1..69499db3 100644 --- a/appview/state/pull.go +++ b/appview/state/pull.go @@ -122,7 +122,7 @@ func (s *State) mergeCheck(f *FullyResolvedRepo, pull *db.Pull) types.MergeCheck secret, err := db.GetRegistrationKey(s.db, f.Knot) if err != nil { - log.Printf("failed to get registration key: %w", err) + log.Printf("failed to get registration key: %v", err) return types.MergeCheckResponse{ Error: "failed to check merge status: this knot is unregistered", } diff --git a/appview/state/router.go b/appview/state/router.go index 33f586a7..fd4256fc 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -2,6 +2,7 @@ package state import ( "net/http" + "regexp" "strings" "github.com/go-chi/chi/v5" @@ -15,6 +16,27 @@ func (s *State) Router() http.Handler { if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { s.UserRouter().ServeHTTP(w, r) } else { + // Check if the first path element is a valid handle without '@' or a flattened DID + pathParts := strings.SplitN(pat, "/", 2) + if len(pathParts) > 0 { + if isHandleNoAt(pathParts[0]) { + // Redirect to the same path but with '@' prefixed to the handle + redirectPath := "@" + pat + http.Redirect(w, r, "/"+redirectPath, http.StatusFound) + return + } else if isFlattenedDid(pathParts[0]) { + // Redirect to the unflattened DID version + unflattenedDid := unflattenDid(pathParts[0]) + var redirectPath string + if len(pathParts) > 1 { + redirectPath = unflattenedDid + "/" + pathParts[1] + } else { + redirectPath = unflattenedDid + } + http.Redirect(w, r, "/"+redirectPath, http.StatusFound) + return + } + } s.StandardRouter().ServeHTTP(w, r) } }) @@ -22,6 +44,48 @@ func (s *State) Router() http.Handler { return router } +func isHandleNoAt(s string) bool { + // ref: https://atproto.com/specs/handle + re := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) + return re.MatchString(s) +} + +func unflattenDid(s string) string { + if !isFlattenedDid(s) { + return s + } + + parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-" + if len(parts) != 2 { + return s + } + + return "did:" + parts[0] + ":" + parts[1] +} + +// isFlattenedDid checks if the given string is a flattened DID. +// A flattened DID is a DID with the :s swapped to -s to satisfy certain +// application requirements, such as Go module naming conventions. +func isFlattenedDid(s string) bool { + // Check if the string starts with "did-" + if !strings.HasPrefix(s, "did-") { + return false + } + + // Split the string to extract method and identifier + parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-" + if len(parts) != 2 { + return false + } + + // Reconstruct as a standard DID format + // Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc" + reconstructed := "did:" + parts[0] + ":" + parts[1] + re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`) + + return re.MatchString(reconstructed) +} + func (s *State) UserRouter() http.Handler { r := chi.NewRouter() diff --git a/appview/state/router_test.go b/appview/state/router_test.go new file mode 100644 index 00000000..e0c09ea1 --- /dev/null +++ b/appview/state/router_test.go @@ -0,0 +1,114 @@ +package state + +import "testing" + +func TestUnflattenDid(t *testing.T) { + unflattenedMap := map[string]string{ + "did-plc-abcdefghijklmnopqrstuvwxyz": "did:plc:abcdefghijklmnopqrstuvwxyz", + "did-plc-1234567890": "did:plc:1234567890", + "did-key-z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "did-plc-abcdefghi-jklmnopqr-stuvwxyz": "did:plc:abcdefghi-jklmnopqr-stuvwxyz", + "plc-abcdefghijklmnopqrstuvwxyz": "plc-abcdefghijklmnopqrstuvwxyz", + "didplc-abcdefghijklmnopqrstuvwxyz": "didplc-abcdefghijklmnopqrstuvwxyz", + "": "", + "did-": "did-", + "did:plc:abcdefghijklmnopqrstuvwxyz": "did:plc:abcdefghijklmnopqrstuvwxyz", + "did-invalid$format:something": "did-invalid$format:something", + } + + tests := []struct { + name string + input string + expected string + }{} + + for _, tc := range isFlattenedDidTests { + tests = append(tests, struct { + name string + input string + expected string + }{ + name: tc.name, + input: tc.input, + expected: unflattenedMap[tc.input], + }) + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := unflattenDid(tc.input) + if result != tc.expected { + t.Errorf("unflattenDid(%q) = %q, want %q", tc.input, result, tc.expected) + } + }) + } +} + +var isFlattenedDidTests = []struct { + name string + input string + expected bool +}{ + { + name: "valid flattened DID", + input: "did-plc-abcdefghijklmnopqrstuvwxyz", + expected: true, + }, + { + name: "valid flattened DID with numbers", + input: "did-plc-1234567890", + expected: true, + }, + { + name: "valid flattened DID with special characters", + input: "did-key-z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + expected: true, + }, + { + name: "valid flattened DID with dashes", + input: "did-plc-abcdefghi-jklmnopqr-stuvwxyz", + expected: true, + }, + + { + name: "doesn't start with did-", + input: "plc-abcdefghijklmnopqrstuvwxyz", + expected: false, + }, + { + name: "no hyphen after did", + input: "didplc-abcdefghijklmnopqrstuvwxyz", + expected: false, + }, + { + name: "empty string", + input: "", + expected: false, + }, + { + name: "only did-", + input: "did-", + expected: false, + }, + { + name: "standard DID format, not flattened", + input: "did:plc:abcdefghijklmnopqrstuvwxyz", + expected: false, + }, + { + name: "invalid reconstructed DID format", + input: "did-invalid$format:something", + expected: false, + }, +} + +func TestIsFlattenedDid(t *testing.T) { + for _, tc := range isFlattenedDidTests { + t.Run(tc.name, func(t *testing.T) { + result := isFlattenedDid(tc.input) + if result != tc.expected { + t.Errorf("isFlattenedDid(%q) = %v, want %v", tc.input, result, tc.expected) + } + }) + } +} -- 2.51.2