A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
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 }