···158158 middleware.SetGlobalAuthorizer(holdAuthorizer)
159159 slog.Info("Hold authorizer initialized with database caching")
160160161161+ // Clear all denial caches on startup for a clean slate (non-blocking)
162162+ if remote, ok := holdAuthorizer.(*auth.RemoteHoldAuthorizer); ok {
163163+ go func() {
164164+ if err := remote.ClearAllDenials(); err != nil {
165165+ slog.Warn("Failed to clear denial caches on startup", "error", err)
166166+ }
167167+ }()
168168+ }
169169+161170 // Initialize Jetstream workers (background services before HTTP routes)
162171 initializeJetstream(uiDatabase, &cfg.Jetstream, defaultHoldDID, testMode, refresher)
163172···303312 // Run in background to avoid blocking OAuth callback if hold is offline
304313 // Use background context - don't inherit request context which gets canceled on response
305314 slog.Debug("Attempting crew registration", "component", "appview/callback", "did", did, "hold_did", holdDID)
306306- go func(client *atproto.Client, refresher *oauth.Refresher, holdDID string) {
315315+ go func(client *atproto.Client, refresher *oauth.Refresher, holdDID string, authorizer auth.HoldAuthorizer) {
307316 ctx := context.Background()
308308- storage.EnsureCrewMembership(ctx, client, refresher, holdDID)
309309- }(client, refresher, holdDID)
317317+ storage.EnsureCrewMembership(ctx, client, refresher, holdDID, authorizer)
318318+ }(client, refresher, holdDID, holdAuthorizer)
310319311320 }
312321
+9-1
pkg/appview/middleware/registry.go
···196196 globalAuthorizer = authorizer
197197}
198198199199+// GetGlobalAuthorizer returns the global authorizer instance
200200+// Used by components that need to clear denial cache (e.g., EnsureCrewMembership)
201201+func GetGlobalAuthorizer() auth.HoldAuthorizer {
202202+ return globalAuthorizer
203203+}
204204+199205func init() {
200206 // Register the name resolution middleware
201207 registrymw.Register("atproto-resolver", initATProtoResolver)
···298304 if holdDID != "" && nr.refresher != nil {
299305 slog.Debug("Auto-reconciling crew membership", "component", "registry/middleware", "did", did, "hold_did", holdDID)
300306 client := atproto.NewClient(pdsEndpoint, did, "")
301301- storage.EnsureCrewMembership(ctx, client, nr.refresher, holdDID)
307307+ storage.EnsureCrewMembership(ctx, client, nr.refresher, holdDID, nr.authorizer)
302308 }
303309304310 // Get service token for hold authentication (only if authenticated)
···345351 "pullerDID", pullerDID,
346352 "holdDID", holdDID,
347353 "pullerPDSEndpoint", pullerPDSEndpoint,
354354+ "denial_reason", "service_token_app_password_failed",
348355 "error", err)
349356 return "", err
350357 }
···363370 "pullerDID", pullerDID,
364371 "holdDID", holdDID,
365372 "pullerPDSEndpoint", pullerPDSEndpoint,
373373+ "denial_reason", "service_token_oauth_failed",
366374 "error", err)
367375 return "", err
368376 }
+12-1
pkg/appview/storage/crew.go
···15151616// EnsureCrewMembership attempts to register the user as a crew member on their default hold.
1717// The hold's requestCrew endpoint handles all authorization logic (checking allowAllCrew, existing membership, etc).
1818+// On success, clears any cached denial to ensure immediate access.
1819// This is best-effort and does not fail on errors.
1919-func EnsureCrewMembership(ctx context.Context, client *atproto.Client, refresher *oauth.Refresher, defaultHoldDID string) {
2020+func EnsureCrewMembership(ctx context.Context, client *atproto.Client, refresher *oauth.Refresher, defaultHoldDID string, authorizer auth.HoldAuthorizer) {
2021 if defaultHoldDID == "" {
2122 return
2223 }
···5556 }
56575758 slog.Info("successfully registered as crew member", "holdDID", holdDID, "userDID", client.DID())
5959+6060+ // Clear any cached denial to ensure immediate access
6161+ if authorizer != nil {
6262+ if err := authorizer.ClearCrewDenial(ctx, holdDID, client.DID()); err != nil {
6363+ slog.Warn("failed to clear denial cache after crew registration",
6464+ "holdDID", holdDID,
6565+ "userDID", client.DID(),
6666+ "error", err)
6767+ }
6868+ }
5869}
59706071// requestCrewMembership calls the hold's requestCrew endpoint
+1-1
pkg/appview/storage/crew_test.go
···7788func TestEnsureCrewMembership_EmptyHoldDID(t *testing.T) {
99 // Test that empty hold DID returns early without error (best-effort function)
1010- EnsureCrewMembership(context.Background(), nil, nil, "")
1010+ EnsureCrewMembership(context.Background(), nil, nil, "", nil)
1111 // If we get here without panic, test passes
1212}
1313