···170170 }
171171 ctx = context.WithValue(ctx, holdDIDKey, holdDID)
172172173173+ // Auto-reconcile crew membership on first push/pull
174174+ // This ensures users can push immediately after docker login without web sign-in
175175+ // EnsureCrewMembership is best-effort and logs errors without failing the request
176176+ if holdDID != "" && nr.refresher != nil {
177177+ fmt.Printf("DEBUG [registry/middleware]: Auto-reconciling crew membership for DID=%s at hold=%s\n", did, holdDID)
178178+ client := atproto.NewClient(pdsEndpoint, did, "")
179179+ storage.EnsureCrewMembership(ctx, client, nr.refresher, holdDID)
180180+ }
181181+173182 // Get service token for hold authentication
174183 var serviceToken string
175184 if nr.refresher != nil {
+3-2
pkg/appview/storage/proxy_blob_store.go
···8787 return fmt.Errorf("authorization check failed: %w", err)
8888 }
8989 if !allowed {
9090- return distribution.ErrBlobUnknown // Return same error as missing blob for security
9090+ // Return 403 Forbidden instead of masquerading as missing blob
9191+ return errcode.ErrorCodeDenied.WithMessage("read access denied")
9192 }
9293 return nil
9394}
···106107 }
107108 if !allowed {
108109 fmt.Printf("[checkWriteAccess] Write access DENIED for userDID=%s to holdDID=%s\n", p.ctx.DID, p.ctx.HoldDID)
109109- return fmt.Errorf("write access denied to hold %s", p.ctx.HoldDID)
110110+ return errcode.ErrorCodeDenied.WithMessage(fmt.Sprintf("write access denied to hold %s", p.ctx.HoldDID))
110111 }
111112 fmt.Printf("[checkWriteAccess] Write access ALLOWED for userDID=%s to holdDID=%s\n", p.ctx.DID, p.ctx.HoldDID)
112113 return nil