···108108}
109109110110// Tags returns the tag service
111111-// Tags will be handled by ATProto as well
111111+// Tags are stored in ATProto as io.atcr.tag records
112112func (r *RoutingRepository) Tags(ctx context.Context) distribution.TagService {
113113- // For now, delegate to the base repository
114114- // In a full implementation, this would also use ATProto
115115- return r.Repository.Tags(ctx)
113113+ return atproto.NewTagStore(r.atprotoClient, r.repositoryName)
116114}
+10-2
pkg/atproto/client.go
···135135 return nil, err
136136 }
137137138138- req.Header.Set("Authorization", "Bearer "+c.accessToken)
138138+ // Only set Authorization header if we have a token
139139+ // Empty Bearer tokens will be rejected by PDS
140140+ if c.accessToken != "" {
141141+ req.Header.Set("Authorization", "Bearer "+c.accessToken)
142142+ }
139143140144 resp, err := c.httpClient.Do(req)
141145 if err != nil {
···217221 return nil, err
218222 }
219223220220- req.Header.Set("Authorization", "Bearer "+c.accessToken)
224224+ // Only set Authorization header if we have a token
225225+ // Empty Bearer tokens will be rejected by PDS
226226+ if c.accessToken != "" {
227227+ req.Header.Set("Authorization", "Bearer "+c.accessToken)
228228+ }
221229222230 resp, err := c.httpClient.Do(req)
223231 if err != nil {
+125
pkg/atproto/tag_store.go
···11+package atproto
22+33+import (
44+ "context"
55+ "encoding/json"
66+ "fmt"
77+88+ "github.com/distribution/distribution/v3"
99+ "github.com/opencontainers/go-digest"
1010+)
1111+1212+// TagStore implements distribution.TagService
1313+// It stores tags in ATProto as records
1414+type TagStore struct {
1515+ client *Client
1616+ repository string
1717+}
1818+1919+// NewTagStore creates a new ATProto-backed tag store
2020+func NewTagStore(client *Client, repository string) *TagStore {
2121+ return &TagStore{
2222+ client: client,
2323+ repository: repository,
2424+ }
2525+}
2626+2727+// Get retrieves the descriptor for a tag
2828+func (s *TagStore) Get(ctx context.Context, tag string) (distribution.Descriptor, error) {
2929+ // Build record key
3030+ rkey := repositoryTagToRKey(s.repository, tag)
3131+3232+ // Fetch tag record from ATProto
3333+ record, err := s.client.GetRecord(ctx, TagCollection, rkey)
3434+ if err != nil {
3535+ return distribution.Descriptor{}, distribution.ErrTagUnknown{Tag: tag}
3636+ }
3737+3838+ var tagRecord TagRecord
3939+ if err := json.Unmarshal(record.Value, &tagRecord); err != nil {
4040+ return distribution.Descriptor{}, fmt.Errorf("failed to unmarshal tag record: %w", err)
4141+ }
4242+4343+ // Parse manifest digest
4444+ dgst, err := digest.Parse(tagRecord.ManifestDigest)
4545+ if err != nil {
4646+ return distribution.Descriptor{}, fmt.Errorf("invalid manifest digest in tag record: %w", err)
4747+ }
4848+4949+ // Return descriptor pointing to the manifest
5050+ return distribution.Descriptor{
5151+ Digest: dgst,
5252+ MediaType: "application/vnd.oci.image.manifest.v1+json",
5353+ }, nil
5454+}
5555+5656+// Tag associates a tag with a descriptor (manifest digest)
5757+func (s *TagStore) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {
5858+ // Create tag record
5959+ tagRecord := NewTagRecord(s.repository, tag, desc.Digest.String())
6060+6161+ // Store in ATProto
6262+ rkey := repositoryTagToRKey(s.repository, tag)
6363+ _, err := s.client.PutRecord(ctx, TagCollection, rkey, tagRecord)
6464+ if err != nil {
6565+ return fmt.Errorf("failed to store tag in ATProto: %w", err)
6666+ }
6767+6868+ return nil
6969+}
7070+7171+// Untag removes a tag
7272+func (s *TagStore) Untag(ctx context.Context, tag string) error {
7373+ rkey := repositoryTagToRKey(s.repository, tag)
7474+ return s.client.DeleteRecord(ctx, TagCollection, rkey)
7575+}
7676+7777+// All returns all tags for this repository
7878+func (s *TagStore) All(ctx context.Context) ([]string, error) {
7979+ // List all records in the tag collection
8080+ records, err := s.client.ListRecords(ctx, TagCollection, 100)
8181+ if err != nil {
8282+ return nil, fmt.Errorf("failed to list tags: %w", err)
8383+ }
8484+8585+ var tags []string
8686+ for _, record := range records {
8787+ var tagRecord TagRecord
8888+ if err := json.Unmarshal(record.Value, &tagRecord); err != nil {
8989+ // Skip invalid records
9090+ continue
9191+ }
9292+9393+ // Only include tags for this repository
9494+ if tagRecord.Repository == s.repository {
9595+ tags = append(tags, tagRecord.Tag)
9696+ }
9797+ }
9898+9999+ return tags, nil
100100+}
101101+102102+// Lookup returns the set of tags for a given digest
103103+func (s *TagStore) Lookup(ctx context.Context, desc distribution.Descriptor) ([]string, error) {
104104+ // List all records in the tag collection
105105+ records, err := s.client.ListRecords(ctx, TagCollection, 100)
106106+ if err != nil {
107107+ return nil, fmt.Errorf("failed to list tags: %w", err)
108108+ }
109109+110110+ var tags []string
111111+ for _, record := range records {
112112+ var tagRecord TagRecord
113113+ if err := json.Unmarshal(record.Value, &tagRecord); err != nil {
114114+ // Skip invalid records
115115+ continue
116116+ }
117117+118118+ // Only include tags for this repository that match the digest
119119+ if tagRecord.Repository == s.repository && tagRecord.ManifestDigest == desc.Digest.String() {
120120+ tags = append(tags, tagRecord.Tag)
121121+ }
122122+ }
123123+124124+ return tags, nil
125125+}