+5
-12
pkg/appview/handlers/api.go
+5
-12
pkg/appview/handlers/api.go
···
1
1
package handlers
2
2
3
3
import (
4
-
"context"
5
4
"database/sql"
6
5
"encoding/json"
7
6
"errors"
···
37
36
repository := chi.URLParam(r, "repository")
38
37
39
38
// Resolve owner's handle to DID
40
-
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
39
+
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
41
40
if err != nil {
42
41
slog.Warn("Failed to resolve handle for star", "handle", handle, "error", err)
43
42
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
···
95
94
repository := chi.URLParam(r, "repository")
96
95
97
96
// Resolve owner's handle to DID
98
-
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
97
+
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
99
98
if err != nil {
100
99
slog.Warn("Failed to resolve handle for unstar", "handle", handle, "error", err)
101
100
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
···
156
155
repository := chi.URLParam(r, "repository")
157
156
158
157
// Resolve owner's handle to DID
159
-
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
158
+
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
160
159
if err != nil {
161
160
slog.Warn("Failed to resolve handle for check star", "handle", handle, "error", err)
162
161
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
···
200
199
repository := chi.URLParam(r, "repository")
201
200
202
201
// Resolve owner's handle to DID
203
-
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
202
+
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
204
203
if err != nil {
205
204
http.Error(w, "Failed to resolve handle", http.StatusBadRequest)
206
205
return
···
231
230
digest := chi.URLParam(r, "digest")
232
231
233
232
// Resolve owner's handle to DID
234
-
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
233
+
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
235
234
if err != nil {
236
235
http.Error(w, "Failed to resolve handle", http.StatusBadRequest)
237
236
return
···
253
252
w.Header().Set("Content-Type", "application/json")
254
253
json.NewEncoder(w).Encode(manifest)
255
254
}
256
-
257
-
// resolveIdentityToDID is a helper function that resolves a handle or DID to a DID
258
-
func resolveIdentityToDID(ctx context.Context, directory identity.Directory, identityStr string) (string, error) {
259
-
// Resolve to DID via directory (handles both handles and DIDs)
260
-
return atproto.ResolveHandleToDID(ctx, identityStr)
261
-
}
+3
-1
pkg/appview/holdhealth/checker_test.go
+3
-1
pkg/appview/holdhealth/checker_test.go
···
6
6
"net/http/httptest"
7
7
"testing"
8
8
"time"
9
+
10
+
"atcr.io/pkg/atproto"
9
11
)
10
12
11
13
func TestNewChecker(t *testing.T) {
···
317
319
318
320
for _, tt := range tests {
319
321
t.Run(tt.name, func(t *testing.T) {
320
-
result := normalizeHoldEndpoint(tt.input)
322
+
result := atproto.ResolveHoldDIDFromURL(tt.input)
321
323
if result != tt.expected {
322
324
t.Errorf("normalizeHoldEndpoint(%q) = %q, want %q", tt.input, result, tt.expected)
323
325
}
+3
-28
pkg/appview/holdhealth/worker.go
+3
-28
pkg/appview/holdhealth/worker.go
···
5
5
"database/sql"
6
6
"fmt"
7
7
"log/slog"
8
-
"strings"
9
8
"sync"
10
9
"time"
10
+
11
+
"atcr.io/pkg/atproto"
11
12
)
12
13
13
14
// DBQuerier interface for database queries (allows mocking in tests)
···
129
130
130
131
for _, endpoint := range endpoints {
131
132
// Normalize to canonical DID format
132
-
normalizedDID := normalizeHoldEndpoint(endpoint)
133
+
normalizedDID := atproto.ResolveHoldDIDFromURL(endpoint)
133
134
134
135
// Skip if we've already seen this normalized DID
135
136
if seen[normalizedDID] {
···
219
220
220
221
return endpoints, nil
221
222
}
222
-
223
-
// normalizeHoldEndpoint converts a hold endpoint (URL or DID) to canonical DID format
224
-
// This ensures that different representations of the same hold are deduplicated:
225
-
// - http://172.28.0.3:8080 → did:web:172.28.0.3:8080
226
-
// - http://hold01.atcr.io → did:web:hold01.atcr.io
227
-
// - https://hold01.atcr.io → did:web:hold01.atcr.io
228
-
// - did:web:hold01.atcr.io → did:web:hold01.atcr.io (passthrough)
229
-
func normalizeHoldEndpoint(endpoint string) string {
230
-
// Strip protocol and trailing slashes
231
-
normalized := endpoint
232
-
normalized = strings.TrimPrefix(normalized, "http://")
233
-
normalized = strings.TrimPrefix(normalized, "https://")
234
-
normalized = strings.TrimSuffix(normalized, "/")
235
-
236
-
// If already a DID, return as-is
237
-
if strings.HasPrefix(endpoint, "did:") {
238
-
return endpoint
239
-
}
240
-
241
-
// Extract hostname (remove path if present)
242
-
parts := strings.Split(normalized, "/")
243
-
hostname := parts[0]
244
-
245
-
// Convert to did:web
246
-
return "did:web:" + hostname
247
-
}
+2
-2
pkg/appview/jetstream/processor.go
+2
-2
pkg/appview/jetstream/processor.go
···
25
25
// useCache: true for Worker (live streaming), false for Backfill (batch processing)
26
26
func NewProcessor(database *sql.DB, useCache bool) *Processor {
27
27
p := &Processor{
28
-
db: database,
29
-
useCache: useCache,
28
+
db: database,
29
+
useCache: useCache,
30
30
}
31
31
32
32
if useCache {
-1
pkg/appview/ui_test.go
-1
pkg/appview/ui_test.go
+6
-3
pkg/atproto/lexicon.go
+6
-3
pkg/atproto/lexicon.go
···
406
406
}
407
407
408
408
// ResolveHoldDIDFromURL converts a hold endpoint URL to a did:web DID
409
-
// For did:web holds: https://hold01.atcr.io → did:web:hold01.atcr.io
410
-
// If input is already a DID, returns it as-is
409
+
// This ensures that different representations of the same hold are deduplicated:
410
+
// - http://172.28.0.3:8080 → did:web:172.28.0.3:8080
411
+
// - http://hold01.atcr.io → did:web:hold01.atcr.io
412
+
// - https://hold01.atcr.io → did:web:hold01.atcr.io
413
+
// - did:web:hold01.atcr.io → did:web:hold01.atcr.io (passthrough)
411
414
func ResolveHoldDIDFromURL(holdURL string) string {
412
415
// Handle empty URLs
413
416
if holdURL == "" {
···
415
418
}
416
419
417
420
// If already a DID, return as-is
418
-
if strings.HasPrefix(holdURL, "did:") {
421
+
if IsDID(holdURL) {
419
422
return holdURL
420
423
}
421
424
+2
-3
pkg/auth/oauth/client.go
+2
-3
pkg/auth/oauth/client.go
···
20
20
type App struct {
21
21
clientApp *oauth.ClientApp
22
22
baseURL string
23
-
directory identity.Directory
24
23
}
25
24
26
25
// NewApp creates a new OAuth app for ATCR with default scopes
···
32
31
func NewAppWithScopes(baseURL string, store oauth.ClientAuthStore, scopes []string) (*App, error) {
33
32
config := NewClientConfigWithScopes(baseURL, scopes)
34
33
clientApp := oauth.NewClientApp(&config, store)
34
+
clientApp.Dir = atproto.GetDirectory()
35
35
36
36
return &App{
37
37
clientApp: clientApp,
38
38
baseURL: baseURL,
39
-
directory: atproto.GetDirectory(),
40
39
}, nil
41
40
}
42
41
···
102
101
103
102
// Directory returns the identity directory used by the OAuth app
104
103
func (a *App) Directory() identity.Directory {
105
-
return a.directory
104
+
return a.clientApp.Dir
106
105
}
107
106
108
107
// ClientIDWithScopes generates a client ID with custom scopes
+2
-2
pkg/hold/pds/xrpc.go
+2
-2
pkg/hold/pds/xrpc.go
···
263
263
264
264
// Normalize actor to DID
265
265
actorDID := actor
266
-
if !strings.HasPrefix(actor, "did:") {
266
+
if !atproto.IsDID(actor) {
267
267
// It's a handle, resolve to DID
268
268
expectedHandle := strings.TrimPrefix(h.pds.DID(), "did:web:")
269
269
if actor == expectedHandle {
···
306
306
for _, actor := range actors {
307
307
// Normalize actor to DID
308
308
actorDID := actor
309
-
if !strings.HasPrefix(actor, "did:") {
309
+
if !atproto.IsDID(actor) {
310
310
// It's a handle, check if it matches
311
311
if actor == expectedHandle {
312
312
actorDID = h.pds.DID()