A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

more slog

+801 -124
+12 -66
cmd/appview/serve.go
··· 72 72 73 73 // Initialize UI database first (required for all stores) 74 74 fmt.Println("Initializing UI database...") 75 - uiEnabled := os.Getenv("ATCR_UI_ENABLED") != "false" 76 - dbPath := os.Getenv("ATCR_UI_DATABASE_PATH") 77 - if dbPath == "" { 78 - dbPath = "/var/lib/atcr/ui.db" 79 - } 75 + uiEnabled := appview.GetUIEnabled() 76 + dbPath := appview.GetUIDatabasePath() 80 77 uiDatabase, uiReadOnlyDB, uiSessionStore := db.InitializeDatabase(uiEnabled, dbPath) 81 78 if uiDatabase == nil { 82 79 return fmt.Errorf("failed to initialize UI database - required for session storage") ··· 84 81 85 82 // Initialize hold health checker 86 83 fmt.Println("Initializing hold health checker...") 87 - 88 - // Parse health check cache TTL from environment (default: 15m) 89 - cacheTTL := 15 * time.Minute 90 - if cacheTTLStr := os.Getenv("ATCR_HEALTH_CACHE_TTL"); cacheTTLStr != "" { 91 - if parsed, err := time.ParseDuration(cacheTTLStr); err == nil { 92 - cacheTTL = parsed 93 - } else { 94 - fmt.Printf("Warning: Invalid ATCR_HEALTH_CACHE_TTL '%s', using default 15m\n", cacheTTLStr) 95 - } 96 - } 97 - 84 + cacheTTL := appview.GetHealthCacheTTL() 98 85 healthChecker := holdhealth.NewChecker(cacheTTL) 99 86 100 87 // Initialize README cache 101 88 fmt.Println("Initializing README cache...") 102 - readmeCacheTTL := 1 * time.Hour // Default: 1 hour 103 - if readmeTTLStr := os.Getenv("ATCR_README_CACHE_TTL"); readmeTTLStr != "" { 104 - if parsed, err := time.ParseDuration(readmeTTLStr); err == nil { 105 - readmeCacheTTL = parsed 106 - } else { 107 - fmt.Printf("Warning: Invalid ATCR_README_CACHE_TTL '%s', using default 1h\n", readmeTTLStr) 108 - } 109 - } 89 + readmeCacheTTL := appview.GetReadmeCacheTTL() 110 90 readmeCache := readme.NewCache(uiDatabase, readmeCacheTTL) 111 91 112 92 // Start background health check worker 113 - // Parse refresh interval from environment (default: 15m) 114 - refreshInterval := 15 * time.Minute 115 - if refreshIntervalStr := os.Getenv("ATCR_HEALTH_CHECK_INTERVAL"); refreshIntervalStr != "" { 116 - if parsed, err := time.ParseDuration(refreshIntervalStr); err == nil { 117 - refreshInterval = parsed 118 - } else { 119 - fmt.Printf("Warning: Invalid ATCR_HEALTH_CHECK_INTERVAL '%s', using default 15m\n", refreshIntervalStr) 120 - } 121 - } 122 - 93 + refreshInterval := appview.GetHealthCheckInterval() 123 94 startupDelay := 5 * time.Second // Wait for hold services to start (Docker compose) 124 95 dbAdapter := holdhealth.NewDBAdapter(uiDatabase) 125 96 healthWorker := holdhealth.NewWorkerWithStartupDelay(healthChecker, dbAdapter, refreshInterval, startupDelay) ··· 142 113 fmt.Println("Using SQLite for device storage") 143 114 144 115 // Get base URL from config or environment 145 - baseURL := os.Getenv("ATCR_BASE_URL") 146 - if baseURL == "" { 147 - // If addr is just a port (e.g., ":5000"), prepend localhost 148 - addr := config.HTTP.Addr 149 - if addr[0] == ':' { 150 - baseURL = fmt.Sprintf("http://127.0.0.1%s", addr) 151 - } else { 152 - baseURL = fmt.Sprintf("http://%s", addr) 153 - } 154 - } 155 - 116 + baseURL := appview.GetBaseURL(config.HTTP.Addr) 156 117 fmt.Printf("DEBUG: Base URL for OAuth: %s\n", baseURL) 157 118 158 119 // Extract default hold DID for OAuth server and backfill worker ··· 535 496 // healthChecker: hold endpoint health checker 536 497 func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.SessionStore, oauthApp *oauth.App, oauthStore *db.OAuthStore, refresher *oauth.Refresher, baseURL string, deviceStore *db.DeviceStore, defaultHoldDID string, healthChecker *holdhealth.Checker, readmeCache *readme.Cache) (*template.Template, *mux.Router) { 537 498 // Check if UI is enabled 538 - uiEnabled := os.Getenv("ATCR_UI_ENABLED") 539 - if uiEnabled == "false" { 499 + if !appview.GetUIEnabled() { 540 500 return nil, nil 541 501 } 542 502 ··· 723 683 }).Methods("GET", "POST") 724 684 725 685 // Start Jetstream worker 726 - jetstreamURL := os.Getenv("JETSTREAM_URL") 727 - if jetstreamURL == "" { 728 - jetstreamURL = "wss://jetstream2.us-west.bsky.network/subscribe" 729 - } 686 + jetstreamURL := appview.GetJetstreamURL() 730 687 731 688 // Start real-time Jetstream worker with cursor tracking for reconnects 732 689 go func() { ··· 744 701 fmt.Println("Jetstream: Real-time worker started") 745 702 746 703 // Start backfill worker (enabled by default, set ATCR_BACKFILL_ENABLED=false to disable) 747 - if backfillEnabled := os.Getenv("ATCR_BACKFILL_ENABLED"); backfillEnabled != "false" { 704 + if appview.GetBackfillEnabled() { 748 705 // Get relay endpoint for sync API (defaults to Bluesky's relay) 749 - relayEndpoint := os.Getenv("ATCR_RELAY_ENDPOINT") 750 - if relayEndpoint == "" { 751 - relayEndpoint = "https://relay1.us-east.bsky.network" 752 - } 706 + relayEndpoint := appview.GetRelayEndpoint() 753 707 754 708 // Check test mode 755 - testMode := os.Getenv("TEST_MODE") == "true" 709 + testMode := appview.GetTestMode() 756 710 757 711 backfillWorker, err := jetstream.NewBackfillWorker(database, relayEndpoint, defaultHoldDID, testMode) 758 712 if err != nil { ··· 774 728 }() 775 729 776 730 // Start periodic backfill scheduler 777 - backfillInterval := os.Getenv("ATCR_BACKFILL_INTERVAL") 778 - if backfillInterval == "" { 779 - backfillInterval = "1h" // Default to 1 hour 780 - } 781 - interval, err := time.ParseDuration(backfillInterval) 782 - if err != nil { 783 - fmt.Printf("Warning: Invalid ATCR_BACKFILL_INTERVAL '%s', using default 1h: %v\n", backfillInterval, err) 784 - interval = time.Hour 785 - } 731 + interval := appview.GetBackfillInterval() 786 732 787 733 go func() { 788 734 ticker := time.NewTicker(interval)
+100
pkg/appview/config.go
··· 320 320 321 321 return false 322 322 } 323 + 324 + // GetDurationOrDefault parses a duration from environment variable or returns default 325 + // Logs a warning if parsing fails 326 + func GetDurationOrDefault(envKey string, defaultValue time.Duration) time.Duration { 327 + envVal := os.Getenv(envKey) 328 + if envVal == "" { 329 + return defaultValue 330 + } 331 + 332 + parsed, err := time.ParseDuration(envVal) 333 + if err != nil { 334 + fmt.Printf("Warning: Invalid %s '%s', using default %s\n", envKey, envVal, defaultValue) 335 + return defaultValue 336 + } 337 + 338 + return parsed 339 + } 340 + 341 + // GetBoolOrDefault returns a boolean from environment variable or returns default 342 + // Treats "false" as false, everything else (including empty) as the default value 343 + func GetBoolOrDefault(envKey string, defaultValue bool) bool { 344 + envVal := os.Getenv(envKey) 345 + if envVal == "" { 346 + return defaultValue 347 + } 348 + 349 + // Explicit false check 350 + if envVal == "false" { 351 + return false 352 + } 353 + 354 + // Explicit true check 355 + if envVal == "true" { 356 + return true 357 + } 358 + 359 + // For any other value, return default 360 + return defaultValue 361 + } 362 + 363 + // UI Configuration 364 + 365 + // GetUIEnabled returns whether the UI is enabled (default: true) 366 + func GetUIEnabled() bool { 367 + // UI is enabled unless explicitly set to "false" 368 + return os.Getenv("ATCR_UI_ENABLED") != "false" 369 + } 370 + 371 + // GetUIDatabasePath returns the path to the UI database (default: /var/lib/atcr/ui.db) 372 + func GetUIDatabasePath() string { 373 + return GetEnvOrDefault("ATCR_UI_DATABASE_PATH", "/var/lib/atcr/ui.db") 374 + } 375 + 376 + // Health & Cache Configuration 377 + 378 + // GetHealthCacheTTL returns the hold health check cache TTL (default: 15m) 379 + func GetHealthCacheTTL() time.Duration { 380 + return GetDurationOrDefault("ATCR_HEALTH_CACHE_TTL", 15*time.Minute) 381 + } 382 + 383 + // GetReadmeCacheTTL returns the README cache TTL (default: 1h) 384 + func GetReadmeCacheTTL() time.Duration { 385 + return GetDurationOrDefault("ATCR_README_CACHE_TTL", 1*time.Hour) 386 + } 387 + 388 + // GetHealthCheckInterval returns the hold health check refresh interval (default: 15m) 389 + func GetHealthCheckInterval() time.Duration { 390 + return GetDurationOrDefault("ATCR_HEALTH_CHECK_INTERVAL", 15*time.Minute) 391 + } 392 + 393 + // Jetstream Configuration 394 + 395 + // GetJetstreamURL returns the Jetstream WebSocket URL (default: wss://jetstream2.us-west.bsky.network/subscribe) 396 + func GetJetstreamURL() string { 397 + return GetEnvOrDefault("JETSTREAM_URL", "wss://jetstream2.us-west.bsky.network/subscribe") 398 + } 399 + 400 + // GetBackfillEnabled returns whether backfill is enabled (default: true) 401 + func GetBackfillEnabled() bool { 402 + // Backfill is enabled unless explicitly set to "false" 403 + return os.Getenv("ATCR_BACKFILL_ENABLED") != "false" 404 + } 405 + 406 + // GetRelayEndpoint returns the relay endpoint for sync API (default: https://relay1.us-east.bsky.network) 407 + func GetRelayEndpoint() string { 408 + return GetEnvOrDefault("ATCR_RELAY_ENDPOINT", "https://relay1.us-east.bsky.network") 409 + } 410 + 411 + // GetBackfillInterval returns the backfill interval (default: 1h) 412 + func GetBackfillInterval() time.Duration { 413 + return GetDurationOrDefault("ATCR_BACKFILL_INTERVAL", 1*time.Hour) 414 + } 415 + 416 + // Test Mode Configuration 417 + 418 + // GetTestMode returns whether test mode is enabled (default: false) 419 + // Test mode enables HTTP for local DID resolution and transition:generic scope 420 + func GetTestMode() bool { 421 + return os.Getenv("TEST_MODE") == "true" 422 + }
+613
pkg/appview/config_test.go
··· 3 3 import ( 4 4 "os" 5 5 "testing" 6 + "time" 6 7 7 8 "github.com/distribution/distribution/v3/configuration" 8 9 ) ··· 968 969 }) 969 970 } 970 971 } 972 + 973 + func TestGetDurationOrDefault(t *testing.T) { 974 + tests := []struct { 975 + name string 976 + envKey string 977 + envValue string 978 + setEnv bool 979 + defaultValue string 980 + want string 981 + }{ 982 + { 983 + name: "env var not set", 984 + envKey: "TEST_DURATION", 985 + setEnv: false, 986 + defaultValue: "5m", 987 + want: "5m", 988 + }, 989 + { 990 + name: "env var set to valid duration", 991 + envKey: "TEST_DURATION", 992 + envValue: "10m", 993 + setEnv: true, 994 + defaultValue: "5m", 995 + want: "10m", 996 + }, 997 + { 998 + name: "env var set to invalid duration", 999 + envKey: "TEST_DURATION", 1000 + envValue: "invalid", 1001 + setEnv: true, 1002 + defaultValue: "5m", 1003 + want: "5m", // Falls back to default 1004 + }, 1005 + { 1006 + name: "env var set to empty string", 1007 + envKey: "TEST_DURATION", 1008 + envValue: "", 1009 + setEnv: true, 1010 + defaultValue: "15m", 1011 + want: "15m", 1012 + }, 1013 + } 1014 + 1015 + for _, tt := range tests { 1016 + t.Run(tt.name, func(t *testing.T) { 1017 + if tt.setEnv { 1018 + t.Setenv(tt.envKey, tt.envValue) 1019 + } else { 1020 + os.Unsetenv(tt.envKey) 1021 + } 1022 + 1023 + defaultDur := parseDuration(t, tt.defaultValue) 1024 + wantDur := parseDuration(t, tt.want) 1025 + 1026 + got := GetDurationOrDefault(tt.envKey, defaultDur) 1027 + if got != wantDur { 1028 + t.Errorf("GetDurationOrDefault() = %v, want %v", got, wantDur) 1029 + } 1030 + }) 1031 + } 1032 + } 1033 + 1034 + func TestGetBoolOrDefault(t *testing.T) { 1035 + tests := []struct { 1036 + name string 1037 + envKey string 1038 + envValue string 1039 + setEnv bool 1040 + defaultValue bool 1041 + want bool 1042 + }{ 1043 + { 1044 + name: "env var not set - default true", 1045 + envKey: "TEST_BOOL", 1046 + setEnv: false, 1047 + defaultValue: true, 1048 + want: true, 1049 + }, 1050 + { 1051 + name: "env var not set - default false", 1052 + envKey: "TEST_BOOL", 1053 + setEnv: false, 1054 + defaultValue: false, 1055 + want: false, 1056 + }, 1057 + { 1058 + name: "env var set to true", 1059 + envKey: "TEST_BOOL", 1060 + envValue: "true", 1061 + setEnv: true, 1062 + defaultValue: false, 1063 + want: true, 1064 + }, 1065 + { 1066 + name: "env var set to false", 1067 + envKey: "TEST_BOOL", 1068 + envValue: "false", 1069 + setEnv: true, 1070 + defaultValue: true, 1071 + want: false, 1072 + }, 1073 + { 1074 + name: "env var set to invalid value - use default true", 1075 + envKey: "TEST_BOOL", 1076 + envValue: "invalid", 1077 + setEnv: true, 1078 + defaultValue: true, 1079 + want: true, 1080 + }, 1081 + { 1082 + name: "env var set to invalid value - use default false", 1083 + envKey: "TEST_BOOL", 1084 + envValue: "invalid", 1085 + setEnv: true, 1086 + defaultValue: false, 1087 + want: false, 1088 + }, 1089 + { 1090 + name: "env var set to empty string - use default", 1091 + envKey: "TEST_BOOL", 1092 + envValue: "", 1093 + setEnv: true, 1094 + defaultValue: true, 1095 + want: true, 1096 + }, 1097 + } 1098 + 1099 + for _, tt := range tests { 1100 + t.Run(tt.name, func(t *testing.T) { 1101 + if tt.setEnv { 1102 + t.Setenv(tt.envKey, tt.envValue) 1103 + } else { 1104 + os.Unsetenv(tt.envKey) 1105 + } 1106 + 1107 + got := GetBoolOrDefault(tt.envKey, tt.defaultValue) 1108 + if got != tt.want { 1109 + t.Errorf("GetBoolOrDefault() = %v, want %v", got, tt.want) 1110 + } 1111 + }) 1112 + } 1113 + } 1114 + 1115 + func TestGetUIEnabled(t *testing.T) { 1116 + tests := []struct { 1117 + name string 1118 + envValue string 1119 + setEnv bool 1120 + want bool 1121 + }{ 1122 + { 1123 + name: "env var not set - enabled by default", 1124 + setEnv: false, 1125 + want: true, 1126 + }, 1127 + { 1128 + name: "env var set to false", 1129 + envValue: "false", 1130 + setEnv: true, 1131 + want: false, 1132 + }, 1133 + { 1134 + name: "env var set to true", 1135 + envValue: "true", 1136 + setEnv: true, 1137 + want: true, 1138 + }, 1139 + { 1140 + name: "env var set to empty string - enabled by default", 1141 + envValue: "", 1142 + setEnv: true, 1143 + want: true, 1144 + }, 1145 + { 1146 + name: "env var set to any other value - enabled", 1147 + envValue: "yes", 1148 + setEnv: true, 1149 + want: true, 1150 + }, 1151 + } 1152 + 1153 + for _, tt := range tests { 1154 + t.Run(tt.name, func(t *testing.T) { 1155 + if tt.setEnv { 1156 + t.Setenv("ATCR_UI_ENABLED", tt.envValue) 1157 + } else { 1158 + os.Unsetenv("ATCR_UI_ENABLED") 1159 + } 1160 + 1161 + got := GetUIEnabled() 1162 + if got != tt.want { 1163 + t.Errorf("GetUIEnabled() = %v, want %v", got, tt.want) 1164 + } 1165 + }) 1166 + } 1167 + } 1168 + 1169 + func TestGetUIDatabasePath(t *testing.T) { 1170 + tests := []struct { 1171 + name string 1172 + envValue string 1173 + setEnv bool 1174 + want string 1175 + }{ 1176 + { 1177 + name: "env var not set - use default", 1178 + setEnv: false, 1179 + want: "/var/lib/atcr/ui.db", 1180 + }, 1181 + { 1182 + name: "env var set to custom path", 1183 + envValue: "/custom/path/ui.db", 1184 + setEnv: true, 1185 + want: "/custom/path/ui.db", 1186 + }, 1187 + { 1188 + name: "env var set to empty string - use default", 1189 + envValue: "", 1190 + setEnv: true, 1191 + want: "/var/lib/atcr/ui.db", 1192 + }, 1193 + } 1194 + 1195 + for _, tt := range tests { 1196 + t.Run(tt.name, func(t *testing.T) { 1197 + if tt.setEnv { 1198 + t.Setenv("ATCR_UI_DATABASE_PATH", tt.envValue) 1199 + } else { 1200 + os.Unsetenv("ATCR_UI_DATABASE_PATH") 1201 + } 1202 + 1203 + got := GetUIDatabasePath() 1204 + if got != tt.want { 1205 + t.Errorf("GetUIDatabasePath() = %v, want %v", got, tt.want) 1206 + } 1207 + }) 1208 + } 1209 + } 1210 + 1211 + func TestGetHealthCacheTTL(t *testing.T) { 1212 + tests := []struct { 1213 + name string 1214 + envValue string 1215 + setEnv bool 1216 + want string 1217 + }{ 1218 + { 1219 + name: "env var not set - use default 15m", 1220 + setEnv: false, 1221 + want: "15m", 1222 + }, 1223 + { 1224 + name: "env var set to custom duration", 1225 + envValue: "30m", 1226 + setEnv: true, 1227 + want: "30m", 1228 + }, 1229 + { 1230 + name: "env var set to invalid duration - use default", 1231 + envValue: "invalid", 1232 + setEnv: true, 1233 + want: "15m", 1234 + }, 1235 + } 1236 + 1237 + for _, tt := range tests { 1238 + t.Run(tt.name, func(t *testing.T) { 1239 + if tt.setEnv { 1240 + t.Setenv("ATCR_HEALTH_CACHE_TTL", tt.envValue) 1241 + } else { 1242 + os.Unsetenv("ATCR_HEALTH_CACHE_TTL") 1243 + } 1244 + 1245 + wantDur := parseDuration(t, tt.want) 1246 + got := GetHealthCacheTTL() 1247 + if got != wantDur { 1248 + t.Errorf("GetHealthCacheTTL() = %v, want %v", got, wantDur) 1249 + } 1250 + }) 1251 + } 1252 + } 1253 + 1254 + func TestGetReadmeCacheTTL(t *testing.T) { 1255 + tests := []struct { 1256 + name string 1257 + envValue string 1258 + setEnv bool 1259 + want string 1260 + }{ 1261 + { 1262 + name: "env var not set - use default 1h", 1263 + setEnv: false, 1264 + want: "1h", 1265 + }, 1266 + { 1267 + name: "env var set to custom duration", 1268 + envValue: "2h", 1269 + setEnv: true, 1270 + want: "2h", 1271 + }, 1272 + { 1273 + name: "env var set to invalid duration - use default", 1274 + envValue: "invalid", 1275 + setEnv: true, 1276 + want: "1h", 1277 + }, 1278 + } 1279 + 1280 + for _, tt := range tests { 1281 + t.Run(tt.name, func(t *testing.T) { 1282 + if tt.setEnv { 1283 + t.Setenv("ATCR_README_CACHE_TTL", tt.envValue) 1284 + } else { 1285 + os.Unsetenv("ATCR_README_CACHE_TTL") 1286 + } 1287 + 1288 + wantDur := parseDuration(t, tt.want) 1289 + got := GetReadmeCacheTTL() 1290 + if got != wantDur { 1291 + t.Errorf("GetReadmeCacheTTL() = %v, want %v", got, wantDur) 1292 + } 1293 + }) 1294 + } 1295 + } 1296 + 1297 + func TestGetHealthCheckInterval(t *testing.T) { 1298 + tests := []struct { 1299 + name string 1300 + envValue string 1301 + setEnv bool 1302 + want string 1303 + }{ 1304 + { 1305 + name: "env var not set - use default 15m", 1306 + setEnv: false, 1307 + want: "15m", 1308 + }, 1309 + { 1310 + name: "env var set to custom interval", 1311 + envValue: "5m", 1312 + setEnv: true, 1313 + want: "5m", 1314 + }, 1315 + { 1316 + name: "env var set to invalid duration - use default", 1317 + envValue: "invalid", 1318 + setEnv: true, 1319 + want: "15m", 1320 + }, 1321 + } 1322 + 1323 + for _, tt := range tests { 1324 + t.Run(tt.name, func(t *testing.T) { 1325 + if tt.setEnv { 1326 + t.Setenv("ATCR_HEALTH_CHECK_INTERVAL", tt.envValue) 1327 + } else { 1328 + os.Unsetenv("ATCR_HEALTH_CHECK_INTERVAL") 1329 + } 1330 + 1331 + wantDur := parseDuration(t, tt.want) 1332 + got := GetHealthCheckInterval() 1333 + if got != wantDur { 1334 + t.Errorf("GetHealthCheckInterval() = %v, want %v", got, wantDur) 1335 + } 1336 + }) 1337 + } 1338 + } 1339 + 1340 + func TestGetJetstreamURL(t *testing.T) { 1341 + tests := []struct { 1342 + name string 1343 + envValue string 1344 + setEnv bool 1345 + want string 1346 + }{ 1347 + { 1348 + name: "env var not set - use default", 1349 + setEnv: false, 1350 + want: "wss://jetstream2.us-west.bsky.network/subscribe", 1351 + }, 1352 + { 1353 + name: "env var set to custom URL", 1354 + envValue: "wss://custom-jetstream.example.com/subscribe", 1355 + setEnv: true, 1356 + want: "wss://custom-jetstream.example.com/subscribe", 1357 + }, 1358 + { 1359 + name: "env var set to empty string - use default", 1360 + envValue: "", 1361 + setEnv: true, 1362 + want: "wss://jetstream2.us-west.bsky.network/subscribe", 1363 + }, 1364 + } 1365 + 1366 + for _, tt := range tests { 1367 + t.Run(tt.name, func(t *testing.T) { 1368 + if tt.setEnv { 1369 + t.Setenv("JETSTREAM_URL", tt.envValue) 1370 + } else { 1371 + os.Unsetenv("JETSTREAM_URL") 1372 + } 1373 + 1374 + got := GetJetstreamURL() 1375 + if got != tt.want { 1376 + t.Errorf("GetJetstreamURL() = %v, want %v", got, tt.want) 1377 + } 1378 + }) 1379 + } 1380 + } 1381 + 1382 + func TestGetBackfillEnabled(t *testing.T) { 1383 + tests := []struct { 1384 + name string 1385 + envValue string 1386 + setEnv bool 1387 + want bool 1388 + }{ 1389 + { 1390 + name: "env var not set - enabled by default", 1391 + setEnv: false, 1392 + want: true, 1393 + }, 1394 + { 1395 + name: "env var set to false", 1396 + envValue: "false", 1397 + setEnv: true, 1398 + want: false, 1399 + }, 1400 + { 1401 + name: "env var set to true", 1402 + envValue: "true", 1403 + setEnv: true, 1404 + want: true, 1405 + }, 1406 + { 1407 + name: "env var set to empty string - enabled by default", 1408 + envValue: "", 1409 + setEnv: true, 1410 + want: true, 1411 + }, 1412 + { 1413 + name: "env var set to any other value - enabled", 1414 + envValue: "yes", 1415 + setEnv: true, 1416 + want: true, 1417 + }, 1418 + } 1419 + 1420 + for _, tt := range tests { 1421 + t.Run(tt.name, func(t *testing.T) { 1422 + if tt.setEnv { 1423 + t.Setenv("ATCR_BACKFILL_ENABLED", tt.envValue) 1424 + } else { 1425 + os.Unsetenv("ATCR_BACKFILL_ENABLED") 1426 + } 1427 + 1428 + got := GetBackfillEnabled() 1429 + if got != tt.want { 1430 + t.Errorf("GetBackfillEnabled() = %v, want %v", got, tt.want) 1431 + } 1432 + }) 1433 + } 1434 + } 1435 + 1436 + func TestGetRelayEndpoint(t *testing.T) { 1437 + tests := []struct { 1438 + name string 1439 + envValue string 1440 + setEnv bool 1441 + want string 1442 + }{ 1443 + { 1444 + name: "env var not set - use default", 1445 + setEnv: false, 1446 + want: "https://relay1.us-east.bsky.network", 1447 + }, 1448 + { 1449 + name: "env var set to custom endpoint", 1450 + envValue: "https://custom-relay.example.com", 1451 + setEnv: true, 1452 + want: "https://custom-relay.example.com", 1453 + }, 1454 + { 1455 + name: "env var set to empty string - use default", 1456 + envValue: "", 1457 + setEnv: true, 1458 + want: "https://relay1.us-east.bsky.network", 1459 + }, 1460 + } 1461 + 1462 + for _, tt := range tests { 1463 + t.Run(tt.name, func(t *testing.T) { 1464 + if tt.setEnv { 1465 + t.Setenv("ATCR_RELAY_ENDPOINT", tt.envValue) 1466 + } else { 1467 + os.Unsetenv("ATCR_RELAY_ENDPOINT") 1468 + } 1469 + 1470 + got := GetRelayEndpoint() 1471 + if got != tt.want { 1472 + t.Errorf("GetRelayEndpoint() = %v, want %v", got, tt.want) 1473 + } 1474 + }) 1475 + } 1476 + } 1477 + 1478 + func TestGetBackfillInterval(t *testing.T) { 1479 + tests := []struct { 1480 + name string 1481 + envValue string 1482 + setEnv bool 1483 + want string 1484 + }{ 1485 + { 1486 + name: "env var not set - use default 1h", 1487 + setEnv: false, 1488 + want: "1h", 1489 + }, 1490 + { 1491 + name: "env var set to custom interval", 1492 + envValue: "30m", 1493 + setEnv: true, 1494 + want: "30m", 1495 + }, 1496 + { 1497 + name: "env var set to invalid duration - use default", 1498 + envValue: "invalid", 1499 + setEnv: true, 1500 + want: "1h", 1501 + }, 1502 + } 1503 + 1504 + for _, tt := range tests { 1505 + t.Run(tt.name, func(t *testing.T) { 1506 + if tt.setEnv { 1507 + t.Setenv("ATCR_BACKFILL_INTERVAL", tt.envValue) 1508 + } else { 1509 + os.Unsetenv("ATCR_BACKFILL_INTERVAL") 1510 + } 1511 + 1512 + wantDur := parseDuration(t, tt.want) 1513 + got := GetBackfillInterval() 1514 + if got != wantDur { 1515 + t.Errorf("GetBackfillInterval() = %v, want %v", got, wantDur) 1516 + } 1517 + }) 1518 + } 1519 + } 1520 + 1521 + func TestGetTestMode(t *testing.T) { 1522 + tests := []struct { 1523 + name string 1524 + envValue string 1525 + setEnv bool 1526 + want bool 1527 + }{ 1528 + { 1529 + name: "env var not set - disabled by default", 1530 + setEnv: false, 1531 + want: false, 1532 + }, 1533 + { 1534 + name: "env var set to true", 1535 + envValue: "true", 1536 + setEnv: true, 1537 + want: true, 1538 + }, 1539 + { 1540 + name: "env var set to false", 1541 + envValue: "false", 1542 + setEnv: true, 1543 + want: false, 1544 + }, 1545 + { 1546 + name: "env var set to empty string - disabled", 1547 + envValue: "", 1548 + setEnv: true, 1549 + want: false, 1550 + }, 1551 + { 1552 + name: "env var set to any other value - disabled", 1553 + envValue: "yes", 1554 + setEnv: true, 1555 + want: false, 1556 + }, 1557 + } 1558 + 1559 + for _, tt := range tests { 1560 + t.Run(tt.name, func(t *testing.T) { 1561 + if tt.setEnv { 1562 + t.Setenv("TEST_MODE", tt.envValue) 1563 + } else { 1564 + os.Unsetenv("TEST_MODE") 1565 + } 1566 + 1567 + got := GetTestMode() 1568 + if got != tt.want { 1569 + t.Errorf("GetTestMode() = %v, want %v", got, tt.want) 1570 + } 1571 + }) 1572 + } 1573 + } 1574 + 1575 + // parseDuration is a helper function to parse duration strings in tests 1576 + func parseDuration(t *testing.T, s string) time.Duration { 1577 + t.Helper() 1578 + d, err := time.ParseDuration(s) 1579 + if err != nil { 1580 + t.Fatalf("parseDuration(%q) failed: %v", s, err) 1581 + } 1582 + return d 1583 + }
+6 -5
pkg/auth/hold_authorizer.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "log/slog" 6 7 7 8 "atcr.io/pkg/atproto" 8 9 ) ··· 55 56 // - Must be authenticated 56 57 // - Must be hold owner OR crew member 57 58 func CheckWriteAccessWithCaptain(captain *atproto.CaptainRecord, userDID string, isCrew bool) bool { 58 - fmt.Printf("[CheckWriteAccessWithCaptain] userDID=%s captain.Owner=%s isCrew=%v\n", userDID, captain.Owner, isCrew) 59 + slog.Debug("Checking write access", "userDID", userDID, "owner", captain.Owner, "isCrew", isCrew) 59 60 60 61 if userDID == "" { 61 62 // Anonymous writes not allowed 62 - fmt.Printf("[CheckWriteAccessWithCaptain] DENIED: Anonymous user\n") 63 + slog.Debug("Write access denied: anonymous user") 63 64 return false 64 65 } 65 66 66 67 // Check if DID is the hold owner 67 68 if userDID == captain.Owner { 68 69 // Owner always has write access 69 - fmt.Printf("[CheckWriteAccessWithCaptain] ALLOWED: User is hold owner\n") 70 + slog.Debug("Write access allowed: user is hold owner") 70 71 return true 71 72 } 72 73 73 74 // Check if DID is a crew member 74 75 if isCrew { 75 - fmt.Printf("[CheckWriteAccessWithCaptain] ALLOWED: User is crew member\n") 76 + slog.Debug("Write access allowed: user is crew member") 76 77 } else { 77 - fmt.Printf("[CheckWriteAccessWithCaptain] DENIED: User is not owner or crew\n") 78 + slog.Debug("Write access denied: user is not owner or crew") 78 79 } 79 80 return isCrew 80 81 }
+8 -7
pkg/auth/hold_remote.go
··· 6 6 "encoding/json" 7 7 "fmt" 8 8 "io" 9 + "log/slog" 9 10 "net/http" 10 11 "net/url" 11 12 "strings" ··· 100 101 if a.db != nil { 101 102 if err := a.setCachedCaptainRecord(holdDID, record); err != nil { 102 103 // Log error but don't fail - caching is best-effort 103 - fmt.Printf("WARNING: Failed to cache captain record: %v\n", err) 104 + slog.Warn("Failed to cache captain record", "error", err, "holdDID", holdDID) 104 105 } 105 106 } 106 107 ··· 265 266 266 267 // Check approval cache first (15min TTL) 267 268 if approved, err := a.getCachedApproval(holdDID, userDID); err == nil && approved { 268 - fmt.Printf("[IsCrewMember] Using cached APPROVAL: holdDID=%s userDID=%s\n", holdDID, userDID) 269 + slog.Debug("Using cached crew approval", "holdDID", holdDID, "userDID", userDID) 269 270 return true, nil 270 271 } 271 272 272 273 // Check denial cache with backoff 273 274 if blocked, err := a.isBlockedByDenialBackoff(holdDID, userDID); err == nil && blocked { 274 275 // Still in backoff period - don't query again 275 - fmt.Printf("[IsCrewMember] BLOCKED by denial backoff cache: holdDID=%s userDID=%s\n", holdDID, userDID) 276 + slog.Debug("Blocked by denial backoff cache", "holdDID", holdDID, "userDID", userDID) 276 277 return false, nil 277 278 } 278 279 279 280 // Cache miss or expired - query XRPC endpoint 280 - fmt.Printf("[IsCrewMember] Cache miss, querying hold: holdDID=%s userDID=%s\n", holdDID, userDID) 281 + slog.Debug("Crew membership cache miss, querying hold", "holdDID", holdDID, "userDID", userDID) 281 282 isCrew, err := a.isCrewMemberNoCache(ctx, holdDID, userDID) 282 283 if err != nil { 283 - fmt.Printf("[IsCrewMember] Query error: %v\n", err) 284 + slog.Warn("Crew membership query error", "error", err, "holdDID", holdDID, "userDID", userDID) 284 285 return false, err 285 286 } 286 287 287 288 // Update cache based on result 288 289 if isCrew { 289 290 // Cache approval for 15 minutes 290 - fmt.Printf("[IsCrewMember] Query result: APPROVED, caching for 15min\n") 291 + slog.Debug("Crew membership approved, caching for 15min", "holdDID", holdDID, "userDID", userDID) 291 292 _ = a.cacheApproval(holdDID, userDID, 15*time.Minute) 292 293 } else { 293 294 // Cache denial with exponential backoff 294 - fmt.Printf("[IsCrewMember] Query result: DENIED, caching with backoff\n") 295 + slog.Debug("Crew membership denied, caching with backoff", "holdDID", holdDID, "userDID", userDID) 295 296 _ = a.cacheDenial(holdDID, userDID) 296 297 } 297 298
+6 -4
pkg/auth/oauth/refresher.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "log/slog" 6 7 "sync" 7 8 "time" 8 9 ··· 114 115 // Validate that session scopes match current desired scopes 115 116 desiredScopes := r.app.GetConfig().Scopes 116 117 if !ScopesMatch(sessionData.Scopes, desiredScopes) { 117 - fmt.Printf("DEBUG [oauth/refresher]: Scope mismatch for DID %s - deleting session\n", did) 118 - fmt.Printf(" Stored scopes: %v\n", sessionData.Scopes) 119 - fmt.Printf(" Desired scopes: %v\n", desiredScopes) 118 + slog.Debug("Scope mismatch, deleting session", 119 + "did", did, 120 + "storedScopes", sessionData.Scopes, 121 + "desiredScopes", desiredScopes) 120 122 121 123 // Delete the session from database since scopes have changed 122 124 if err := r.app.clientApp.Store.DeleteSession(ctx, accountDID, sessionID); err != nil { 123 - fmt.Printf("WARNING [oauth/refresher]: Failed to delete session with mismatched scopes: %v\n", err) 125 + slog.Warn("Failed to delete session with mismatched scopes", "error", err, "did", did) 124 126 } 125 127 126 128 return nil, fmt.Errorf("OAuth scopes changed, re-authentication required")
+8 -7
pkg/auth/oauth/server.go
··· 4 4 "context" 5 5 "fmt" 6 6 "html/template" 7 + "log/slog" 7 8 "net/http" 8 9 "strings" 9 10 "time" ··· 68 69 return 69 70 } 70 71 71 - fmt.Printf("DEBUG [oauth/server]: Starting OAuth flow for handle=%s\n", handle) 72 + slog.Debug("Starting OAuth flow", "handle", handle) 72 73 73 74 // Start auth flow via indigo 74 75 authURL, err := s.app.StartAuthFlow(r.Context(), handle) 75 76 if err != nil { 76 - fmt.Printf("ERROR [oauth/server]: Failed to start auth flow: %v\n", err) 77 + slog.Error("Failed to start auth flow", "error", err, "handle", handle) 77 78 78 79 // Check if error is about invalid_client_metadata (usually means PDS doesn't support required scopes) 79 80 errMsg := err.Error() ··· 86 87 return 87 88 } 88 89 89 - fmt.Printf("DEBUG [oauth/server]: Generated authURL=%s\n", authURL) 90 + slog.Debug("Generated OAuth authorization URL", "authURL", authURL) 90 91 91 92 // Redirect to PDS authorization page 92 93 // Note: indigo handles state internally via the auth store ··· 117 118 did := sessionData.AccountDID.String() 118 119 sessionID := sessionData.SessionID 119 120 120 - fmt.Printf("DEBUG [oauth/server]: OAuth callback successful for DID=%s, sessionID=%s\n", did, sessionID) 121 + slog.Debug("OAuth callback successful", "did", did, "sessionID", sessionID) 121 122 122 123 // Invalidate cached session (if any) since we have a new session with new tokens 123 124 if s.refresher != nil { 124 125 s.refresher.InvalidateSession(did) 125 - fmt.Printf("DEBUG [oauth/server]: Invalidated cached session for DID=%s after creating new session\n", did) 126 + slog.Debug("Invalidated cached session after creating new session", "did", did) 126 127 } 127 128 128 129 // Look up identity 129 130 ident, err := s.app.directory.LookupDID(r.Context(), sessionData.AccountDID) 130 131 handle := ident.Handle.String() 131 132 if err != nil { 132 - fmt.Printf("WARNING [oauth/server]: Failed to resolve DID to handle: %v, using DID as handle\n", err) 133 + slog.Warn("Failed to resolve DID to handle, using DID as fallback", "error", err, "did", did) 133 134 handle = did // Fallback to DID if resolution fails 134 135 } 135 136 ··· 137 138 if s.postAuthCallback != nil { 138 139 if err := s.postAuthCallback(r.Context(), did, handle, sessionData.HostURL, sessionID); err != nil { 139 140 // Log error but don't fail OAuth flow - business logic is non-critical 140 - fmt.Printf("WARNING [oauth/server]: Post-auth callback failed for DID=%s: %v\n", did, err) 141 + slog.Warn("Post-auth callback failed", "error", err, "did", did) 141 142 } 142 143 } 143 144
+9 -8
pkg/auth/session.go
··· 11 11 "encoding/json" 12 12 "fmt" 13 13 "io" 14 + "log/slog" 14 15 "net/http" 15 16 "sync" 16 17 "time" ··· 94 95 // Check cache first 95 96 cacheKey := getCacheKey(identifier, password) 96 97 if cached, ok := v.getCachedSession(cacheKey); ok { 97 - fmt.Printf("DEBUG [atproto/session]: Using cached session for %s (DID=%s)\n", identifier, cached.DID) 98 + slog.Debug("Using cached session", "identifier", identifier, "did", cached.DID) 98 99 return cached.DID, cached.Handle, cached.AccessToken, nil 99 100 } 100 101 101 - fmt.Printf("DEBUG [atproto/session]: No cached session for %s, creating new session\n", identifier) 102 + slog.Debug("No cached session, creating new session", "identifier", identifier) 102 103 103 104 // Resolve identifier to PDS endpoint 104 105 atID, err := syntax.ParseAtIdentifier(identifier) ··· 130 131 AccessToken: sessionResp.AccessJWT, 131 132 ExpiresAt: time.Now().Add(2 * time.Hour), 132 133 }) 133 - fmt.Printf("DEBUG [atproto/session]: Cached session for %s (expires in 2 hours)\n", identifier) 134 + slog.Debug("Cached session (expires in 2 hours)", "identifier", identifier, "did", sessionResp.DID) 134 135 135 136 return sessionResp.DID, sessionResp.Handle, sessionResp.AccessJWT, nil 136 137 } ··· 148 149 } 149 150 150 151 url := fmt.Sprintf("%s%s", pdsEndpoint, atproto.ServerCreateSession) 151 - fmt.Printf("DEBUG [atproto/session]: POST %s\n", url) 152 + slog.Debug("Creating ATProto session", "url", url) 152 153 153 154 req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) 154 155 if err != nil { ··· 159 160 160 161 resp, err := v.httpClient.Do(req) 161 162 if err != nil { 162 - fmt.Printf("DEBUG [atproto/session]: HTTP request failed: %v\n", err) 163 + slog.Debug("Session creation HTTP request failed", "error", err) 163 164 return nil, fmt.Errorf("failed to create session: %w", err) 164 165 } 165 166 defer resp.Body.Close() 166 167 167 - fmt.Printf("DEBUG [atproto/session]: Got HTTP status %d\n", resp.StatusCode) 168 + slog.Debug("Received session creation response", "status", resp.StatusCode) 168 169 169 170 if resp.StatusCode == http.StatusUnauthorized { 170 171 bodyBytes, _ := io.ReadAll(resp.Body) 171 - fmt.Printf("DEBUG [atproto/session]: Unauthorized response: %s\n", string(bodyBytes)) 172 + slog.Debug("Session creation unauthorized", "response", string(bodyBytes)) 172 173 return nil, fmt.Errorf("invalid credentials") 173 174 } 174 175 175 176 if resp.StatusCode != http.StatusOK { 176 177 bodyBytes, _ := io.ReadAll(resp.Body) 177 - fmt.Printf("DEBUG [atproto/session]: Error response: %s\n", string(bodyBytes)) 178 + slog.Debug("Session creation failed", "status", resp.StatusCode, "response", string(bodyBytes)) 178 179 return nil, fmt.Errorf("create session failed with status %d: %s", resp.StatusCode, string(bodyBytes)) 179 180 } 180 181
+7 -5
pkg/auth/token/cache.go
··· 8 8 "encoding/base64" 9 9 "encoding/json" 10 10 "fmt" 11 + "log/slog" 11 12 "strings" 12 13 "sync" 13 14 "time" ··· 63 64 expiry, err := parseJWTExpiry(token) 64 65 if err != nil { 65 66 // If parsing fails, use default 50s TTL (conservative fallback) 66 - fmt.Printf("WARN [token/cache]: Failed to parse JWT expiry, using default 50s: %v\n", err) 67 + slog.Warn("Failed to parse JWT expiry, using default 50s", "error", err, "cacheKey", cacheKey) 67 68 expiry = time.Now().Add(50 * time.Second) 68 69 } else { 69 70 // Apply 10s safety margin to avoid using nearly-expired tokens ··· 77 78 } 78 79 globalServiceTokensMu.Unlock() 79 80 80 - fmt.Printf("DEBUG [token/cache]: Cached service token for %s (expires in %v)\n", 81 - cacheKey, time.Until(expiry).Round(time.Second)) 81 + slog.Debug("Cached service token", 82 + "cacheKey", cacheKey, 83 + "expiresIn", time.Until(expiry).Round(time.Second)) 82 84 83 85 return nil 84 86 } ··· 123 125 delete(globalServiceTokens, cacheKey) 124 126 globalServiceTokensMu.Unlock() 125 127 126 - fmt.Printf("DEBUG [token/cache]: Invalidated service token for %s\n", cacheKey) 128 + slog.Debug("Invalidated service token", "cacheKey", cacheKey) 127 129 } 128 130 129 131 // GetCacheStats returns statistics about the service token cache for debugging ··· 168 170 } 169 171 170 172 if removed > 0 { 171 - fmt.Printf("DEBUG [token/cache]: Cleaned %d expired service tokens\n", removed) 173 + slog.Debug("Cleaned expired service tokens", "count", removed) 172 174 } 173 175 }
+17 -13
pkg/auth/token/handler.go
··· 4 4 "context" 5 5 "encoding/json" 6 6 "fmt" 7 + "log/slog" 7 8 "net/http" 8 9 "strings" 9 10 "time" ··· 83 84 84 85 // ServeHTTP handles the token request 85 86 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 86 - fmt.Printf("DEBUG [token/handler]: Received %s request to %s\n", r.Method, r.URL.Path) 87 + slog.Debug("Received token request", "method", r.Method, "path", r.URL.Path) 87 88 88 89 // Only accept GET requests (per Docker spec) 89 90 if r.Method != http.MethodGet { ··· 94 95 // Extract Basic auth credentials 95 96 username, password, ok := r.BasicAuth() 96 97 if !ok { 97 - fmt.Printf("DEBUG [token/handler]: No Basic auth credentials provided\n") 98 + slog.Debug("No Basic auth credentials provided") 98 99 sendAuthError(w, r, "authentication required") 99 100 return 100 101 } 101 102 102 - fmt.Printf("DEBUG [token/handler]: Got Basic auth for username=%s, password length=%d\n", username, len(password)) 103 + slog.Debug("Got Basic auth credentials", "username", username, "passwordLength", len(password)) 103 104 104 105 // Parse query parameters 105 106 _ = r.URL.Query().Get("service") // service parameter - validated by issuer ··· 125 126 if strings.HasPrefix(password, "atcr_device_") { 126 127 device, err := h.deviceStore.ValidateDeviceSecret(password) 127 128 if err != nil { 128 - fmt.Printf("DEBUG [token/handler]: Device secret validation failed: %v\n", err) 129 + slog.Debug("Device secret validation failed", "error", err) 129 130 sendAuthError(w, r, "authentication failed") 130 131 return 131 132 } ··· 136 137 // OAuth refresher will provide access token when needed via middleware 137 138 } else { 138 139 // 2. Try app password (direct PDS authentication) 139 - fmt.Printf("DEBUG [token/handler]: Not a device secret, trying app password for %s\n", username) 140 + slog.Debug("Trying app password authentication", "username", username) 140 141 did, handle, accessToken, err = h.validator.CreateSessionAndGetToken(r.Context(), username, password) 141 142 if err != nil { 142 - fmt.Printf("DEBUG [token/handler]: App password validation failed: %v\n", err) 143 + slog.Debug("App password validation failed", "error", err, "username", username) 143 144 sendAuthError(w, r, "authentication failed") 144 145 return 145 146 } 146 147 147 - fmt.Printf("DEBUG [token/handler]: App password validated successfully, DID=%s, handle=%s, AccessToken length=%d\n", did, handle, len(accessToken)) 148 + slog.Debug("App password validated successfully", 149 + "did", did, 150 + "handle", handle, 151 + "accessTokenLength", len(accessToken)) 148 152 149 153 // Cache the access token for later use (e.g., when pushing manifests) 150 154 // TTL of 2 hours (ATProto tokens typically last longer) 151 155 auth.GetGlobalTokenCache().Set(did, accessToken, 2*time.Hour) 152 - fmt.Printf("DEBUG [token/handler]: Cached access token for DID=%s\n", did) 156 + slog.Debug("Cached access token", "did", did) 153 157 154 158 // Call post-auth callback for AppView business logic (profile management, etc.) 155 159 if h.postAuthCallback != nil { ··· 160 164 ident, err := directory.Lookup(r.Context(), *atID) 161 165 if err != nil { 162 166 // Log error but don't fail auth - profile management is not critical 163 - fmt.Printf("WARNING: failed to resolve PDS for callback: %v\n", err) 167 + slog.Warn("Failed to resolve PDS for callback", "error", err, "username", username) 164 168 } else { 165 169 pdsEndpoint := ident.PDSEndpoint() 166 170 if pdsEndpoint != "" { 167 171 if err := h.postAuthCallback(r.Context(), did, handle, pdsEndpoint, accessToken); err != nil { 168 172 // Log error but don't fail auth - business logic is non-critical 169 - fmt.Printf("WARNING: post-auth callback failed for DID=%s: %v\n", did, err) 173 + slog.Warn("Post-auth callback failed", "error", err, "did", did) 170 174 } 171 175 } 172 176 } ··· 177 181 // Validate that the user has permission for the requested access 178 182 // Use the actual handle from the validated credentials, not the Basic Auth username 179 183 if err := auth.ValidateAccess(did, handle, access); err != nil { 180 - fmt.Printf("DEBUG [token/handler]: Access validation failed: %v\n", err) 184 + slog.Debug("Access validation failed", "error", err, "did", did) 181 185 http.Error(w, fmt.Sprintf("access denied: %v", err), http.StatusForbidden) 182 186 return 183 187 } ··· 185 189 // Issue JWT token 186 190 tokenString, err := h.issuer.Issue(did, access) 187 191 if err != nil { 188 - fmt.Printf("DEBUG [token/handler]: Failed to issue token: %v\n", err) 192 + slog.Error("Failed to issue token", "error", err, "did", did) 189 193 http.Error(w, fmt.Sprintf("failed to issue token: %v", err), http.StatusInternalServerError) 190 194 return 191 195 } 192 196 193 - fmt.Printf("DEBUG [token/handler]: Issued JWT token (length=%d) for DID=%s\n", len(tokenString), did) 197 + slog.Debug("Issued JWT token", "tokenLength", len(tokenString), "did", did) 194 198 195 199 // Return token response 196 200 now := time.Now()
+7 -3
pkg/auth/token/issuer.go
··· 8 8 "encoding/base64" 9 9 "encoding/pem" 10 10 "fmt" 11 + "log/slog" 11 12 "math/big" 12 13 "os" 13 14 "path/filepath" ··· 62 63 func (i *Issuer) Issue(subject string, access []auth.AccessEntry) (string, error) { 63 64 claims := NewClaims(subject, i.issuer, i.service, i.expiration, access) 64 65 65 - fmt.Printf("DEBUG [token/issuer]: Creating token with issuer=%s, service=%s, subject=%s, access=%v\n", 66 - i.issuer, i.service, subject, access) 66 + slog.Debug("Creating JWT token", 67 + "issuer", i.issuer, 68 + "service", i.service, 69 + "subject", subject, 70 + "access", access) 67 71 68 72 token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 69 73 ··· 79 83 return "", fmt.Errorf("failed to sign token: %w", err) 80 84 } 81 85 82 - fmt.Printf("DEBUG [token/issuer]: Successfully signed token with x5c header\n") 86 + slog.Debug("Successfully signed token with x5c header") 83 87 84 88 return signedToken, nil 85 89 }
+8 -6
pkg/auth/token/servicetoken.go
··· 5 5 "encoding/json" 6 6 "fmt" 7 7 "io" 8 + "log/slog" 8 9 "net/http" 9 10 "net/url" 10 11 "time" ··· 30 31 31 32 // Use cached token if it exists and has > 10s remaining 32 33 if cachedToken != "" && time.Until(expiresAt) > 10*time.Second { 33 - fmt.Printf("DEBUG [atproto/servicetoken]: Using cached service token for DID=%s (expires in %v)\n", 34 - did, time.Until(expiresAt).Round(time.Second)) 34 + slog.Debug("Using cached service token", 35 + "did", did, 36 + "expiresIn", time.Until(expiresAt).Round(time.Second)) 35 37 return cachedToken, nil 36 38 } 37 39 38 40 // Cache miss or expiring soon - validate OAuth and get new service token 39 41 if cachedToken == "" { 40 - fmt.Printf("DEBUG [atproto/servicetoken]: Cache miss, fetching service token for DID=%s\n", did) 42 + slog.Debug("Service token cache miss, fetching new token", "did", did) 41 43 } else { 42 - fmt.Printf("DEBUG [atproto/servicetoken]: Token expiring soon, proactively renewing for DID=%s\n", did) 44 + slog.Debug("Service token expiring soon, proactively renewing", "did", did) 43 45 } 44 46 45 47 session, err := refresher.GetSession(ctx, did) ··· 102 104 103 105 // Cache the token (parses JWT to extract actual expiry) 104 106 if err := SetServiceToken(did, holdDID, serviceToken); err != nil { 105 - fmt.Printf("WARN [atproto/servicetoken]: Failed to cache service token: %v\n", err) 107 + slog.Warn("Failed to cache service token", "error", err, "did", did, "holdDID", holdDID) 106 108 // Non-fatal - we have the token, just won't be cached 107 109 } 108 110 109 - fmt.Printf("DEBUG [atproto/servicetoken]: OAuth validation succeeded for DID=%s\n", did) 111 + slog.Debug("OAuth validation succeeded, service token obtained", "did", did) 110 112 return serviceToken, nil 111 113 }