atproto: gate local indigo behavior behind a testmode build tag

indigo's identity directory refuses HTTP and IP-hosted did:web, and its
OAuth client is growing an SSRF-guarded transport that refuses loopback
and private addresses. Local development and the test suites need both,
and the workarounds were scattered: two did:web fallbacks in the
resolver, a hand-rolled appview key fetch on the hold, and the OAuth
client left on indigo's defaults so any test driving it against an
httptest server depended on the transport staying permissive.

Move every departure from indigo's defaults into one file pair in
pkg/atproto: indigo_prod.go (!testmode) returns indigo's directory and
OAuth client unchanged; indigo_local.go (testmode) wraps the directory
so a did:web naming an IP, localhost, or a host with a port resolves
over plain HTTP, and gives the OAuth client plain HTTP clients. All six
identity and OAuth constructor call sites go through NewDirectory and
NewOAuthClientApp. The resolver fallbacks, DIDWebToURL, and the hold's
scheme-guessing key fetch are gone; the hold resolves the appview key
through the directory, preferring #appview, and purges and retries once
on a signature failure so a re-keyed appview is not masked by the
24-hour cache.

There is no runtime switch for this: a production binary cannot be
configured to resolve local DIDs. The runtime test_mode flag still
gates the remaining behavioral branches only.

Tests, the harness, make dev, Air, Dockerfile.dev, and docker-compose
build with the tag; fixtures that need loopback did:web fail fast
naming it. Test hold servers now serve a did.json via pkg/testpds so
they resolve as real holds under the tag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwYzaG3Yy7uA8FbZ5qk3tQ
This commit is contained in:
Evan Jarrett
2026-09-11 10:53:27 -05:00
co-authored by Claude Fable 5.1
parent 7b67b3b383
commit a01b08b924
30 changed files with 709 additions and 230 deletions
+23 -2
View File
@@ -10,6 +10,7 @@ import (
"time"
"atcr.io/pkg/atproto"
"atcr.io/pkg/testpds"
)
// 69307c0 verified captain records against the publishing DID's atcr_hold
@@ -23,9 +24,18 @@ import (
// captainServer serves a captain record for any repo, with allowAllCrew set —
// the value that makes a row visible to every user rather than just its author.
//
// It also serves its own did:web document so the test-mode identity directory
// can resolve the DID derived from its URL (see didFromServer) back to it.
func captainServer(t *testing.T) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requireTestModeBuild(t)
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/did.json", func(w http.ResponseWriter, r *http.Request) {
base := "http://" + r.Host
testpds.HoldDIDDocumentHandler(testpds.DIDWebForURL(base), base)(w, r)
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"uri": "at://x/io.atcr.hold.captain/self",
"cid": "bafytest",
@@ -36,11 +46,22 @@ func captainServer(t *testing.T) *httptest.Server {
"allowAllCrew": true,
},
})
}))
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
// requireTestModeBuild fails fast, with the reason, when the binary cannot
// resolve a loopback did:web. Without it these tests die on an opaque dial
// error from indigo's hardened directory.
func requireTestModeBuild(t *testing.T) {
t.Helper()
if !atproto.TestModeBuild {
t.Fatal("this test resolves a did:web on 127.0.0.1 and needs a `-tags testmode` build")
}
}
func didFromServer(url string) string {
return "did:web:" + strings.ReplaceAll(strings.TrimPrefix(url, "http://"), ":", "%3A")
}
+2 -4
View File
@@ -100,8 +100,7 @@ func NewClientApp(baseURL string, store oauth.ClientAuthStore, scopes []string,
slog.Info("Using public OAuth client (localhost development)")
}
clientApp := oauth.NewClientApp(&config, store)
clientApp.Dir = atproto.GetDirectory()
clientApp := atproto.NewOAuthClientApp(&config, store)
return clientApp, nil
}
@@ -132,8 +131,7 @@ func NewClientAppWithKey(baseURL string, store oauth.ClientAuthStore, scopes []s
slog.Info("Using public OAuth client (localhost development)")
}
clientApp := oauth.NewClientApp(&config, store)
clientApp.Dir = atproto.GetDirectory()
clientApp := atproto.NewOAuthClientApp(&config, store)
return clientApp, nil
}