···11+description: Add successor column to hold_captain_records for hold migration
22+query: |
33+ ALTER TABLE hold_captain_records ADD COLUMN successor TEXT;
+19
pkg/appview/db/queries.go
···15981598 return time.Time{}, fmt.Errorf("unable to parse timestamp: %s", s)
15991599}
1600160016011601+// UpdateManifestHoldDID rewrites the hold_endpoint column for all manifests
16021602+// belonging to a user that currently point to oldHoldDID, changing them to newHoldDID.
16031603+// Returns the number of rows affected.
16041604+func UpdateManifestHoldDID(db DBTX, did, oldHoldDID, newHoldDID string) (int64, error) {
16051605+ result, err := db.Exec(`
16061606+ UPDATE manifests SET hold_endpoint = ?
16071607+ WHERE did = ? AND hold_endpoint = ?
16081608+ `, newHoldDID, did, oldHoldDID)
16091609+ if err != nil {
16101610+ return 0, err
16111611+ }
16121612+ return result.RowsAffected()
16131613+}
16141614+16011615// HoldDIDDB wraps a sql.DB and implements the HoldDIDLookup interface for middleware
16021616// This is a minimal wrapper that only provides hold DID lookups for blob routing
16031617type HoldDIDDB struct {
···16121626// GetLatestHoldDIDForRepo returns the hold DID from the most recent manifest for a repository
16131627func (h *HoldDIDDB) GetLatestHoldDIDForRepo(did, repository string) (string, error) {
16141628 return GetLatestHoldDIDForRepo(h.db, did, repository)
16291629+}
16301630+16311631+// UpdateManifestHoldDID rewrites hold_endpoint for all manifests belonging to a user
16321632+func (h *HoldDIDDB) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string) (int64, error) {
16331633+ return UpdateManifestHoldDID(h.db, did, oldHoldDID, newHoldDID)
16151634}
1616163516171636// RepoCardSortOrder specifies how repo cards should be sorted
+1
pkg/appview/db/schema.sql
···183183 allow_all_crew BOOLEAN NOT NULL,
184184 deployed_at TEXT,
185185 region TEXT,
186186+ successor TEXT,
186187 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
187188);
188189CREATE INDEX IF NOT EXISTS idx_hold_captain_updated ON hold_captain_records(updated_at);
···293293 // This is a fatal configuration error - registry cannot function without a hold service
294294 return nil, fmt.Errorf("no hold DID configured: ensure default_hold_did is set in middleware config")
295295 }
296296+297297+ // Single-hop hold migration: check if this hold has declared a successor
298298+ holdDID = nr.resolveSuccessor(ctx, holdDID)
296299 // Auto-reconcile crew membership on first push/pull
297300 // This ensures users can push immediately after docker login without web sign-in
298301 // EnsureCrewMembership is best-effort and logs errors without failing the request
···522525523526 // No profile defaultHold - use AppView default
524527 return nr.defaultHoldDID
528528+}
529529+530530+// resolveSuccessor checks if a hold has declared a successor and returns it.
531531+// Single-hop only — does not follow chains. Returns the original holdDID if
532532+// no successor is set or if the captain record can't be fetched.
533533+func (nr *NamespaceResolver) resolveSuccessor(ctx context.Context, holdDID string) string {
534534+ if nr.authorizer == nil {
535535+ return holdDID
536536+ }
537537+ captain, err := nr.authorizer.GetCaptainRecord(ctx, holdDID)
538538+ if err != nil {
539539+ return holdDID
540540+ }
541541+ if captain != nil && captain.Successor != "" {
542542+ slog.Info("Hold successor redirect",
543543+ "component", "registry/middleware",
544544+ "from", holdDID,
545545+ "to", captain.Successor)
546546+ return captain.Successor
547547+ }
548548+ return holdDID
525549}
526550527551// isHoldReachable checks if a hold service is reachable
···693693// Uses CBOR encoding for efficient storage in hold's carstore
694694type CaptainRecord struct {
695695 Type string `json:"$type" cborgen:"$type"`
696696- Owner string `json:"owner" cborgen:"owner"` // DID of hold owner
697697- Public bool `json:"public" cborgen:"public"` // Public read access
698698- AllowAllCrew bool `json:"allowAllCrew" cborgen:"allowAllCrew"` // Allow any authenticated user to register as crew
699699- EnableBlueskyPosts bool `json:"enableBlueskyPosts" cborgen:"enableBlueskyPosts"` // Enable Bluesky posts when manifests are pushed (overrides env var)
700700- DeployedAt string `json:"deployedAt" cborgen:"deployedAt"` // RFC3339 timestamp
701701- Region string `json:"region,omitempty" cborgen:"region,omitempty"` // Deployment region (optional)
696696+ Owner string `json:"owner" cborgen:"owner"` // DID of hold owner
697697+ Public bool `json:"public" cborgen:"public"` // Public read access
698698+ AllowAllCrew bool `json:"allowAllCrew" cborgen:"allowAllCrew"` // Allow any authenticated user to register as crew
699699+ EnableBlueskyPosts bool `json:"enableBlueskyPosts" cborgen:"enableBlueskyPosts"` // Enable Bluesky posts when manifests are pushed (overrides env var)
700700+ DeployedAt string `json:"deployedAt" cborgen:"deployedAt"` // RFC3339 timestamp
701701+ Region string `json:"region,omitempty" cborgen:"region,omitempty"` // Deployment region (optional)
702702+ Successor string `json:"successor,omitempty" cborgen:"successor,omitempty"` // DID of successor hold (migration redirect)
702703}
703704704705// CrewRecord represents a crew member in the hold
+12-4
pkg/auth/hold_remote.go
···144144// getCachedCaptainRecord retrieves a captain record from database cache
145145func (a *RemoteHoldAuthorizer) getCachedCaptainRecord(holdDID string) (*captainRecordWithMeta, error) {
146146 query := `
147147- SELECT owner_did, public, allow_all_crew, deployed_at, region, updated_at
147147+ SELECT owner_did, public, allow_all_crew, deployed_at, region, successor, updated_at
148148 FROM hold_captain_records
149149 WHERE hold_did = ?
150150 `
151151152152 var record atproto.CaptainRecord
153153- var deployedAt, region sql.NullString
153153+ var deployedAt, region, successor sql.NullString
154154 var updatedAt time.Time
155155156156 err := a.db.QueryRow(query, holdDID).Scan(
···159159 &record.AllowAllCrew,
160160 &deployedAt,
161161 ®ion,
162162+ &successor,
162163 &updatedAt,
163164 )
164165···176177 }
177178 if region.Valid {
178179 record.Region = region.String
180180+ }
181181+ if successor.Valid {
182182+ record.Successor = successor.String
179183 }
180184181185 return &captainRecordWithMeta{
···189193 query := `
190194 INSERT INTO hold_captain_records (
191195 hold_did, owner_did, public, allow_all_crew,
192192- deployed_at, region, updated_at
193193- ) VALUES (?, ?, ?, ?, ?, ?, ?)
196196+ deployed_at, region, successor, updated_at
197197+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
194198 ON CONFLICT(hold_did) DO UPDATE SET
195199 owner_did = excluded.owner_did,
196200 public = excluded.public,
197201 allow_all_crew = excluded.allow_all_crew,
198202 deployed_at = excluded.deployed_at,
199203 region = excluded.region,
204204+ successor = excluded.successor,
200205 updated_at = excluded.updated_at
201206 `
202207···207212 record.AllowAllCrew,
208213 nullString(record.DeployedAt),
209214 nullString(record.Region),
215215+ nullString(record.Successor),
210216 time.Now(),
211217 )
212218···250256 AllowAllCrew bool `json:"allowAllCrew"`
251257 DeployedAt string `json:"deployedAt"`
252258 Region string `json:"region,omitempty"`
259259+ Successor string `json:"successor,omitempty"`
253260 } `json:"value"`
254261 }
255262···265272 AllowAllCrew: xrpcResp.Value.AllowAllCrew,
266273 DeployedAt: xrpcResp.Value.DeployedAt,
267274 Region: xrpcResp.Value.Region,
275275+ Successor: xrpcResp.Value.Successor,
268276 }
269277270278 return record, nil
+33-3
pkg/hold/admin/handlers_settings.go
···44 "context"
55 "log/slog"
66 "net/http"
77+ "strings"
7899+ "atcr.io/pkg/atproto"
810 "github.com/spf13/viper"
911)
1012···1315 Public bool
1416 AllowAllCrew bool
1517 EnableBlueskyPosts bool
1818+ Successor string
1619 OwnerDID string
1720 OwnerHandle string
1821 HoldDID string
···4245 Public: captain.Public,
4346 AllowAllCrew: captain.AllowAllCrew,
4447 EnableBlueskyPosts: captain.EnableBlueskyPosts,
4848+ Successor: captain.Successor,
4549 OwnerDID: captain.Owner,
4650 OwnerHandle: ownerHandle,
4751 HoldDID: ui.pds.DID(),
···8084 public := r.FormValue("public") == "on"
8185 allowAllCrew := r.FormValue("allow_all_crew") == "on"
8286 enablePosts := r.FormValue("enable_bluesky_posts") == "on"
8787+ successor := strings.TrimSpace(r.FormValue("successor"))
83888484- _, err := ui.pds.UpdateCaptainRecord(ctx, public, allowAllCrew, enablePosts)
8989+ // Validate successor DID format if provided
9090+ if successor != "" {
9191+ if !atproto.IsDID(successor) || !strings.HasPrefix(successor, "did:web:") {
9292+ setFlash(w, r, "error", "Successor must be a valid did:web: DID (e.g., did:web:hold.example.com)")
9393+ http.Redirect(w, r, "/admin#settings", http.StatusFound)
9494+ return
9595+ }
9696+ }
9797+9898+ // Get existing captain record, modify fields, write back
9999+ _, captain, getErr := ui.pds.GetCaptainRecord(ctx)
100100+ if getErr != nil {
101101+ slog.Error("Failed to get captain record", "error", getErr)
102102+ setFlash(w, r, "error", "Failed to read settings: "+getErr.Error())
103103+ http.Redirect(w, r, "/admin#settings", http.StatusFound)
104104+ return
105105+ }
106106+107107+ captain.Public = public
108108+ captain.AllowAllCrew = allowAllCrew
109109+ captain.EnableBlueskyPosts = enablePosts
110110+ captain.Successor = successor
111111+112112+ _, err := ui.pds.UpdateCaptainRecord(ctx, captain)
85113 if err != nil {
86114 slog.Error("Failed to update captain record", "error", err)
87115 setFlash(w, r, "error", "Failed to update settings: "+err.Error())
···94122 "public", public,
95123 "allowAllCrew", allowAllCrew,
96124 "enableBlueskyPosts", enablePosts,
125125+ "successor", successor,
97126 "by", session.DID)
9812799128 // Write settings back to YAML config file (if one exists)
100129 if ui.config.ConfigPath != "" {
101101- if err := ui.writeConfigSettings(public, allowAllCrew, enablePosts); err != nil {
130130+ if err := ui.writeConfigSettings(public, allowAllCrew, enablePosts, successor); err != nil {
102131 slog.Warn("Failed to write settings to config file",
103132 "path", ui.config.ConfigPath, "error", err)
104133···118147119148// writeConfigSettings updates the toggleable settings in the YAML config file.
120149// Uses a fresh Viper instance to avoid baking env var overrides into the file.
121121-func (ui *AdminUI) writeConfigSettings(public, allowAllCrew, enablePosts bool) error {
150150+func (ui *AdminUI) writeConfigSettings(public, allowAllCrew, enablePosts bool, successor string) error {
122151 v := viper.New()
123152 v.SetConfigFile(ui.config.ConfigPath)
124153···127156 }
128157129158 v.Set("server.public", public)
159159+ v.Set("server.successor", successor)
130160 v.Set("registration.allow_all_crew", allowAllCrew)
131161 v.Set("registration.enable_bluesky_posts", enablePosts)
132162
···111111 // Allow unauthenticated blob reads.
112112 Public bool `yaml:"public" comment:"Allow unauthenticated blob reads. If false, readers need crew membership."`
113113114114+ // DID of successor hold for migration.
115115+ Successor string `yaml:"successor" comment:"DID of successor hold for migration. Appview redirects all requests to the successor."`
116116+114117 // Use localhost for OAuth redirects during development.
115118 TestMode bool `yaml:"test_mode" comment:"Use localhost for OAuth redirects during development."`
116119···157160 v.SetDefault("server.addr", ":8080")
158161 v.SetDefault("server.public_url", "")
159162 v.SetDefault("server.public", false)
163163+ v.SetDefault("server.successor", "")
160164 v.SetDefault("server.test_mode", false)
161165 v.SetDefault("server.relay_endpoint", "")
162166 v.SetDefault("server.read_timeout", "5m")
+9-2
pkg/hold/pds/auth_test.go
···742742 pds, ctx := setupTestPDSWithBootstrap(t, ownerDID, false, false)
743743744744 // Update captain to be private
745745- _, err := pds.UpdateCaptainRecord(ctx, false, false, false)
745745+ _, captain, err := pds.GetCaptainRecord(ctx)
746746+ if err != nil {
747747+ t.Fatalf("Failed to get captain record for update: %v", err)
748748+ }
749749+ captain.Public = false
750750+ captain.AllowAllCrew = false
751751+ captain.EnableBlueskyPosts = false
752752+ _, err = pds.UpdateCaptainRecord(ctx, captain)
746753 if err != nil {
747754 t.Fatalf("Failed to update captain record: %v", err)
748755 }
749756750757 // Verify captain record has public=false
751751- _, captain, err := pds.GetCaptainRecord(ctx)
758758+ _, captain, err = pds.GetCaptainRecord(ctx)
752759 if err != nil {
753760 t.Fatalf("Failed to get captain record: %v", err)
754761 }
+4-14
pkg/hold/pds/captain.go
···5858 return recordCID, captainRecord, nil
5959}
60606161-// UpdateCaptainRecord updates the captain record (e.g., to change public/allowAllCrew/enableBlueskyPosts settings)
6262-func (p *HoldPDS) UpdateCaptainRecord(ctx context.Context, public bool, allowAllCrew bool, enableBlueskyPosts bool) (cid.Cid, error) {
6363- // Get existing record to preserve other fields
6464- _, existing, err := p.GetCaptainRecord(ctx)
6565- if err != nil {
6666- return cid.Undef, fmt.Errorf("failed to get existing captain record: %w", err)
6767- }
6868-6969- // Update the fields
7070- existing.Public = public
7171- existing.AllowAllCrew = allowAllCrew
7272- existing.EnableBlueskyPosts = enableBlueskyPosts
7373-7474- recordCID, err := p.repomgr.UpdateRecord(ctx, p.uid, atproto.CaptainCollection, CaptainRkey, existing)
6161+// UpdateCaptainRecord replaces the captain record with the provided record.
6262+// Callers should GetCaptainRecord first, modify fields, then pass the updated record.
6363+func (p *HoldPDS) UpdateCaptainRecord(ctx context.Context, record *atproto.CaptainRecord) (cid.Cid, error) {
6464+ recordCID, err := p.repomgr.UpdateRecord(ctx, p.uid, atproto.CaptainCollection, CaptainRkey, record)
7565 if err != nil {
7666 return cid.Undef, fmt.Errorf("failed to update captain record: %w", err)
7767 }
+14-5
pkg/hold/pds/captain_test.go
···245245 }
246246247247 // Update to public=true, allowAllCrew=true, enableBlueskyPosts=true
248248- updatedCID, err := pds.UpdateCaptainRecord(ctx, true, true, true)
248248+ captain1.Public = true
249249+ captain1.AllowAllCrew = true
250250+ captain1.EnableBlueskyPosts = true
251251+ updatedCID, err := pds.UpdateCaptainRecord(ctx, captain1)
249252 if err != nil {
250253 t.Fatalf("UpdateCaptainRecord failed: %v", err)
251254 }
···282285 }
283286284287 // Update again to different values (public=true, allowAllCrew=false, enableBlueskyPosts=false)
285285- _, err = pds.UpdateCaptainRecord(ctx, true, false, false)
288288+ captain2.AllowAllCrew = false
289289+ captain2.EnableBlueskyPosts = false
290290+ _, err = pds.UpdateCaptainRecord(ctx, captain2)
286291 if err != nil {
287292 t.Fatalf("Second UpdateCaptainRecord failed: %v", err)
288293 }
···307312 defer pds.Close()
308313309314 // Try to update captain record before creating one
310310- _, err := pds.UpdateCaptainRecord(ctx, true, true, true)
315315+ record := &atproto.CaptainRecord{
316316+ Type: atproto.CaptainCollection,
317317+ Public: true,
318318+ }
319319+ _, err := pds.UpdateCaptainRecord(ctx, record)
311320 if err == nil {
312321 t.Fatal("Expected error when updating non-existent captain record")
313322 }
314323315324 // Verify error message
316325 errMsg := err.Error()
317317- if !strings.Contains(errMsg, "failed to get existing captain record") {
318318- t.Errorf("Expected 'failed to get existing captain record' in error, got: %s", errMsg)
326326+ if !strings.Contains(errMsg, "failed to update captain record") {
327327+ t.Errorf("Expected 'failed to update captain record' in error, got: %s", errMsg)
319328 }
320329}
321330
+5-2
pkg/hold/pds/server.go
···291291 existingCaptain.EnableBlueskyPosts != p.enableBlueskyPosts
292292293293 if needsUpdate {
294294- // Update captain record to match env vars
295295- _, err = p.UpdateCaptainRecord(ctx, public, allowAllCrew, p.enableBlueskyPosts)
294294+ // Update captain record to match env vars (preserves other fields like Successor)
295295+ existingCaptain.Public = public
296296+ existingCaptain.AllowAllCrew = allowAllCrew
297297+ existingCaptain.EnableBlueskyPosts = p.enableBlueskyPosts
298298+ _, err = p.UpdateCaptainRecord(ctx, existingCaptain)
296299 if err != nil {
297300 return fmt.Errorf("failed to update captain record: %w", err)
298301 }
+38
pkg/hold/pds/xrpc.go
···167167 r.Get(atproto.RepoListRecords, h.HandleListRecords)
168168169169 // Sync endpoints
170170+ r.Get(atproto.SyncListBlobs, h.HandleListBlobs)
170171 r.Get(atproto.SyncListRepos, h.HandleListRepos)
171172 r.Get(atproto.SyncGetRecord, h.HandleSyncGetRecord)
172173 r.Get(atproto.SyncGetRepo, h.HandleGetRepo)
···12381239 render.JSON(w, r, map[string]any{
12391240 "repos": repos,
12401241 })
12421242+}
12431243+12441244+// HandleListBlobs lists blob CIDs for an account
12451245+// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-list-blobs
12461246+func (h *XRPCHandler) HandleListBlobs(w http.ResponseWriter, r *http.Request) {
12471247+ did := r.URL.Query().Get("did")
12481248+ if did == "" {
12491249+ http.Error(w, "missing required parameter: did", http.StatusBadRequest)
12501250+ return
12511251+ }
12521252+12531253+ if did != h.pds.DID() {
12541254+ http.Error(w, "repo not found", http.StatusNotFound)
12551255+ return
12561256+ }
12571257+12581258+ // List ATProto blobs from storage (S3 prefix listing)
12591259+ safeDID := strings.ReplaceAll(did, ":", "-")
12601260+ blobsPath := fmt.Sprintf("/repos/%s/blobs", safeDID)
12611261+12621262+ entries, err := h.storageDriver.List(r.Context(), blobsPath)
12631263+ if err != nil {
12641264+ // Path doesn't exist = no blobs, return empty list
12651265+ render.JSON(w, r, map[string]any{"cids": []string{}})
12661266+ return
12671267+ }
12681268+12691269+ cids := make([]string, 0, len(entries))
12701270+ for _, entry := range entries {
12711271+ // entry is like "/repos/.../blobs/{cid}" — extract the CID
12721272+ parts := strings.Split(entry, "/")
12731273+ if len(parts) > 0 {
12741274+ cids = append(cids, parts[len(parts)-1])
12751275+ }
12761276+ }
12771277+12781278+ render.JSON(w, r, map[string]any{"cids": cids})
12411279}
1242128012431281// HandleGetRepoStatus returns the hosting status for a repository
+34-5
pkg/hold/pds/xrpc_test.go
···16001600 handler, ctx := setupTestXRPCHandler(t)
1601160116021602 // Update captain record to allow all crew
16031603- _, err := handler.pds.UpdateCaptainRecord(ctx, true, true, false) // public=true, allowAllCrew=true, enableBlueskyPosts=false
16031603+ _, captain, err := handler.pds.GetCaptainRecord(ctx)
16041604+ if err != nil {
16051605+ t.Fatalf("Failed to get captain record: %v", err)
16061606+ }
16071607+ captain.Public = true
16081608+ captain.AllowAllCrew = true
16091609+ _, err = handler.pds.UpdateCaptainRecord(ctx, captain)
16041610 if err != nil {
16051611 t.Fatalf("Failed to update captain record: %v", err)
16061612 }
···1644165016451651 // Captain record was created with allowAllCrew=false in setupTestXRPCHandler
16461652 // Update to make sure it's false
16471647- _, err := handler.pds.UpdateCaptainRecord(ctx, true, false, false) // public=true, allowAllCrew=false, enableBlueskyPosts=false
16531653+ _, captain, err := handler.pds.GetCaptainRecord(ctx)
16541654+ if err != nil {
16551655+ t.Fatalf("Failed to get captain record: %v", err)
16561656+ }
16571657+ captain.Public = true
16581658+ captain.AllowAllCrew = false
16591659+ _, err = handler.pds.UpdateCaptainRecord(ctx, captain)
16481660 if err != nil {
16491661 t.Fatalf("Failed to update captain record: %v", err)
16501662 }
···16971709 handler, ctx := setupTestXRPCHandler(t)
1698171016991711 // Update captain record to allow all crew
17001700- _, err := handler.pds.UpdateCaptainRecord(ctx, true, true, false) // public=true, allowAllCrew=true
17121712+ _, captain, err := handler.pds.GetCaptainRecord(ctx)
17131713+ if err != nil {
17141714+ t.Fatalf("Failed to get captain record: %v", err)
17151715+ }
17161716+ captain.Public = true
17171717+ captain.AllowAllCrew = true
17181718+ _, err = handler.pds.UpdateCaptainRecord(ctx, captain)
17011719 if err != nil {
17021720 t.Fatalf("Failed to update captain record: %v", err)
17031721 }
···17931811 handler, ctx := setupTestXRPCHandler(t)
1794181217951813 // Update captain record to allow all crew
17961796- _, err := handler.pds.UpdateCaptainRecord(ctx, true, true, false)
18141814+ _, captain, err := handler.pds.GetCaptainRecord(ctx)
18151815+ if err != nil {
18161816+ t.Fatalf("Failed to get captain record: %v", err)
18171817+ }
18181818+ captain.Public = true
18191819+ captain.AllowAllCrew = true
18201820+ _, err = handler.pds.UpdateCaptainRecord(ctx, captain)
17971821 if err != nil {
17981822 t.Fatalf("Failed to update captain record: %v", err)
17991823 }
···25102534 handler, _, ctx := setupTestXRPCHandlerWithBlobs(t)
2511253525122536 // Make hold public
25132513- _, err := handler.pds.UpdateCaptainRecord(ctx, true, false, false)
25372537+ _, captain, err := handler.pds.GetCaptainRecord(ctx)
25382538+ if err != nil {
25392539+ t.Fatalf("Failed to get captain record: %v", err)
25402540+ }
25412541+ captain.Public = true
25422542+ _, err = handler.pds.UpdateCaptainRecord(ctx, captain)
25142543 if err != nil {
25152544 t.Fatalf("Failed to update captain: %v", err)
25162545 }
+12
pkg/hold/server.go
···120120 return nil, fmt.Errorf("failed to bootstrap PDS: %w", err)
121121 }
122122123123+ // Sync successor from config (if set) — separate from Bootstrap to avoid changing its signature
124124+ if cfg.Server.Successor != "" {
125125+ if _, captain, err := s.PDS.GetCaptainRecord(ctx); err == nil && captain.Successor != cfg.Server.Successor {
126126+ captain.Successor = cfg.Server.Successor
127127+ if _, err := s.PDS.UpdateCaptainRecord(ctx, captain); err != nil {
128128+ slog.Warn("Failed to sync successor from config", "error", err)
129129+ } else {
130130+ slog.Info("Synced successor from config", "successor", cfg.Server.Successor)
131131+ }
132132+ }
133133+ }
134134+123135 // Bootstrap events from existing repo records (one-time migration)
124136 if err := s.broadcaster.BootstrapFromRepo(s.PDS); err != nil {
125137 slog.Warn("Failed to bootstrap events from repo", "error", err)