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.

add hero banner, fix up css styles

+423 -109
-2
CLAUDE.md
··· 492 492 493 493 **OAuth implementation:** 494 494 - Client (`pkg/auth/oauth/client.go`) encapsulates all OAuth configuration 495 - - Uses `authelia.com/client/oauth2` for PAR support 496 - - DPoP proofs generated with `github.com/AxisCommunications/go-dpop` (auto-handles JWK) 497 495 - Token validation via `com.atproto.server.getSession` ensures no trust in client-provided identity 498 496 - All ATCR components use standardized `/auth/oauth/callback` path 499 497 - Client ID generation (localhost query-based vs production metadata URL) handled internally
+2 -2
deploy/README.md
··· 466 466 ## Support 467 467 468 468 - Documentation: https://tangled.org/@evan.jarrett.net/at-container-registry 469 - - Issues: https://github.com/your-org/atcr.io/issues 470 - - Bluesky: @yourhandle.bsky.social 469 + - Issues: https://tangled.org/@evan.jarrett.net/at-container-registry/issues 470 + - Bluesky: @evan.jarrett.net
+1 -1
pkg/appview/handlers/home.go
··· 60 60 } 61 61 62 62 func (h *RecentPushesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 63 - limit := 50 63 + limit := 20 64 64 offset := 0 65 65 66 66 if o := r.URL.Query().Get("offset"); o != "" {
+316 -13
pkg/appview/static/css/style.css
··· 1 1 :root { 2 2 --primary: #0066cc; 3 + --primary-dark: #0052a3; 3 4 --secondary: #6c757d; 4 5 --success: #28a745; 6 + --success-bg: #d4edda; 7 + --warning: #ffc107; 8 + --warning-bg: #fff3cd; 5 9 --danger: #dc3545; 10 + --danger-bg: #f8d7da; 6 11 --bg: #ffffff; 7 12 --fg: #1a1a1a; 8 13 --border-dark: #666; ··· 10 15 --code-bg: #f5f5f5; 11 16 --hover-bg: #f9f9f9; 12 17 --star: #fbbf24; 18 + 19 + /* Hero section colors */ 20 + --hero-bg-start: #f8f9fa; 21 + --hero-bg-end: #e9ecef; 22 + 23 + /* Terminal colors */ 24 + --terminal-bg: var(--fg); 25 + --terminal-header-bg: #2d2d2d; 26 + --terminal-text: var(--border); 27 + --terminal-prompt: #4ec9b0; 28 + --terminal-comment: #6a9955; 13 29 } 14 30 15 31 * { ··· 694 710 padding: 1rem; 695 711 } 696 712 697 - /* Status Messages */ 713 + /* Status Messages / Callouts */ 714 + .note { 715 + background: var(--warning-bg); 716 + border-left: 4px solid var(--warning); 717 + padding: 1rem; 718 + margin: 1rem 0; 719 + } 720 + 698 721 .success { 699 - color: var(--success); 700 - padding: 0.5rem; 701 - background: #d4edda; 702 - border: 1px solid #c3e6cb; 703 - border-radius: 4px; 704 - margin-top: 1rem; 722 + background: var(--success-bg); 723 + border-left: 4px solid var(--success); 724 + padding: 1rem; 725 + margin: 1rem 0; 705 726 } 706 727 707 728 .error { 708 - color: var(--danger); 709 - padding: 0.5rem; 710 - background: #f8d7da; 711 - border: 1px solid #f5c6cb; 712 - border-radius: 4px; 713 - margin-top: 1rem; 729 + background: var(--danger-bg); 730 + border-left: 4px solid var(--danger); 731 + padding: 1rem; 732 + margin: 1rem 0; 714 733 } 715 734 716 735 /* Load More Button */ ··· 1167 1186 color: var(--fg); 1168 1187 } 1169 1188 1189 + /* Hero Section */ 1190 + .hero-section { 1191 + background: linear-gradient(135deg, var(--hero-bg-start) 0%, var(--hero-bg-end) 100%); 1192 + padding: 4rem 2rem; 1193 + border-bottom: 1px solid var(--border); 1194 + } 1195 + 1196 + .hero-content { 1197 + max-width: 900px; 1198 + margin: 0 auto; 1199 + text-align: center; 1200 + } 1201 + 1202 + .hero-title { 1203 + font-size: 3rem; 1204 + font-weight: 700; 1205 + margin-bottom: 1.5rem; 1206 + color: var(--fg); 1207 + line-height: 1.2; 1208 + } 1209 + 1210 + .hero-subtitle { 1211 + font-size: 1.2rem; 1212 + color: var(--border-dark); 1213 + margin-bottom: 3rem; 1214 + line-height: 1.6; 1215 + } 1216 + 1217 + .hero-terminal { 1218 + max-width: 600px; 1219 + margin: 0 auto 2.5rem; 1220 + background: var(--terminal-bg); 1221 + border-radius: 8px; 1222 + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); 1223 + overflow: hidden; 1224 + } 1225 + 1226 + .terminal-header { 1227 + background: var(--terminal-header-bg); 1228 + padding: 0.75rem 1rem; 1229 + display: flex; 1230 + gap: 0.5rem; 1231 + align-items: center; 1232 + } 1233 + 1234 + .terminal-dot { 1235 + width: 12px; 1236 + height: 12px; 1237 + border-radius: 50%; 1238 + background: var(--border-dark); 1239 + } 1240 + 1241 + .terminal-dot:nth-child(1) { 1242 + background: #ff5f56; 1243 + } 1244 + 1245 + .terminal-dot:nth-child(2) { 1246 + background: #ffbd2e; 1247 + } 1248 + 1249 + .terminal-dot:nth-child(3) { 1250 + background: #27c93f; 1251 + } 1252 + 1253 + .terminal-content { 1254 + padding: 1.5rem; 1255 + margin: 0; 1256 + font-family: 'Monaco', 'Courier New', monospace; 1257 + font-size: 0.95rem; 1258 + line-height: 1.8; 1259 + color: var(--terminal-text); 1260 + overflow-x: auto; 1261 + } 1262 + 1263 + .terminal-prompt { 1264 + color: var(--terminal-prompt); 1265 + font-weight: bold; 1266 + } 1267 + 1268 + .terminal-comment { 1269 + color: var(--terminal-comment); 1270 + font-style: italic; 1271 + } 1272 + 1273 + .hero-actions { 1274 + display: flex; 1275 + gap: 1rem; 1276 + justify-content: center; 1277 + margin-bottom: 4rem; 1278 + } 1279 + 1280 + .btn-hero-primary, 1281 + .btn-hero-secondary { 1282 + padding: 0.9rem 2rem; 1283 + font-size: 1.1rem; 1284 + font-weight: 600; 1285 + border-radius: 6px; 1286 + text-decoration: none; 1287 + transition: all 0.2s ease; 1288 + display: inline-block; 1289 + } 1290 + 1291 + .btn-hero-primary { 1292 + background: var(--primary); 1293 + color: var(--bg); 1294 + border: 2px solid var(--primary); 1295 + } 1296 + 1297 + .btn-hero-primary:hover { 1298 + background: var(--primary-dark); 1299 + border-color: var(--primary-dark); 1300 + transform: translateY(-2px); 1301 + box-shadow: 0 4px 12px rgba(0, 102, 204, 0.3); 1302 + } 1303 + 1304 + .btn-hero-secondary { 1305 + background: transparent; 1306 + color: var(--primary); 1307 + border: 2px solid var(--primary); 1308 + } 1309 + 1310 + .btn-hero-secondary:hover { 1311 + background: var(--primary); 1312 + color: var(--bg); 1313 + transform: translateY(-2px); 1314 + } 1315 + 1316 + .hero-benefits { 1317 + max-width: 1000px; 1318 + margin: 0 auto; 1319 + display: grid; 1320 + grid-template-columns: repeat(3, 1fr); 1321 + gap: 2rem; 1322 + } 1323 + 1324 + .benefit-card { 1325 + background: var(--bg); 1326 + border: 1px solid var(--border); 1327 + border-radius: 8px; 1328 + padding: 2rem 1.5rem; 1329 + text-align: center; 1330 + transition: all 0.2s ease; 1331 + } 1332 + 1333 + .benefit-card:hover { 1334 + border-color: var(--primary); 1335 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 1336 + transform: translateY(-4px); 1337 + } 1338 + 1339 + .benefit-icon { 1340 + font-size: 3rem; 1341 + margin-bottom: 1rem; 1342 + line-height: 1; 1343 + } 1344 + 1345 + .benefit-card h3 { 1346 + font-size: 1.2rem; 1347 + margin-bottom: 0.75rem; 1348 + color: var(--fg); 1349 + } 1350 + 1351 + .benefit-card p { 1352 + color: var(--border-dark); 1353 + font-size: 0.95rem; 1354 + line-height: 1.5; 1355 + margin: 0; 1356 + } 1357 + 1358 + /* Install Page */ 1359 + .install-page { 1360 + max-width: 800px; 1361 + margin: 0 auto; 1362 + padding: 2rem 1rem; 1363 + } 1364 + 1365 + .install-section { 1366 + margin: 2rem 0; 1367 + } 1368 + 1369 + .install-section h2 { 1370 + margin-bottom: 1rem; 1371 + color: var(--fg); 1372 + } 1373 + 1374 + .install-section h3 { 1375 + margin: 1.5rem 0 0.5rem; 1376 + color: var(--border-dark); 1377 + font-size: 1.1rem; 1378 + } 1379 + 1380 + .code-block { 1381 + background: var(--code-bg); 1382 + border: 1px solid var(--border); 1383 + border-radius: 4px; 1384 + padding: 1rem; 1385 + margin: 0.5rem 0 1rem; 1386 + overflow-x: auto; 1387 + } 1388 + 1389 + .code-block code { 1390 + font-family: 'Monaco', 'Menlo', monospace; 1391 + font-size: 0.9rem; 1392 + line-height: 1.5; 1393 + white-space: pre-wrap; 1394 + } 1395 + 1396 + .platform-tabs { 1397 + display: flex; 1398 + gap: 0.5rem; 1399 + border-bottom: 2px solid var(--border); 1400 + margin-bottom: 1rem; 1401 + } 1402 + 1403 + .platform-tab { 1404 + padding: 0.5rem 1rem; 1405 + cursor: pointer; 1406 + border: none; 1407 + background: none; 1408 + font-size: 1rem; 1409 + color: var(--border-dark); 1410 + transition: all 0.2s; 1411 + } 1412 + 1413 + .platform-tab:hover { 1414 + color: var(--fg); 1415 + } 1416 + 1417 + .platform-tab.active { 1418 + color: var(--primary); 1419 + border-bottom: 2px solid var(--primary); 1420 + margin-bottom: -2px; 1421 + } 1422 + 1423 + .platform-content { 1424 + display: none; 1425 + } 1426 + 1427 + .platform-content.active { 1428 + display: block; 1429 + } 1430 + 1170 1431 /* Responsive */ 1171 1432 @media (max-width: 768px) { 1172 1433 .navbar { ··· 1219 1480 .featured-card { 1220 1481 min-height: auto; 1221 1482 } 1483 + 1484 + .hero-section { 1485 + padding: 3rem 1.5rem; 1486 + } 1487 + 1488 + .hero-title { 1489 + font-size: 2rem; 1490 + } 1491 + 1492 + .hero-subtitle { 1493 + font-size: 1rem; 1494 + margin-bottom: 2rem; 1495 + } 1496 + 1497 + .hero-terminal { 1498 + margin-bottom: 2rem; 1499 + } 1500 + 1501 + .terminal-content { 1502 + font-size: 0.85rem; 1503 + padding: 1rem; 1504 + } 1505 + 1506 + .hero-actions { 1507 + flex-direction: column; 1508 + margin-bottom: 3rem; 1509 + } 1510 + 1511 + .btn-hero-primary, 1512 + .btn-hero-secondary { 1513 + width: 100%; 1514 + text-align: center; 1515 + } 1516 + 1517 + .hero-benefits { 1518 + grid-template-columns: 1fr; 1519 + gap: 1.5rem; 1520 + } 1222 1521 } 1223 1522 1224 1523 @media (max-width: 1024px) and (min-width: 769px) { 1225 1524 .featured-grid { 1226 1525 grid-template-columns: repeat(2, 1fr); 1526 + } 1527 + 1528 + .hero-benefits { 1529 + grid-template-columns: repeat(3, 1fr); 1227 1530 } 1228 1531 }
+49
pkg/appview/templates/pages/home.html
··· 11 11 <body> 12 12 {{ template "nav" . }} 13 13 14 + {{ if not .User }} 15 + <!-- Hero Section for Non-Logged-In Users --> 16 + <section class="hero-section"> 17 + <div class="hero-content"> 18 + <h1 class="hero-title">ship containers on the open web.</h1> 19 + <p class="hero-subtitle"> 20 + Push and pull Docker images on the AT Protocol.<br> 21 + Browse public registries or control your data. 22 + </p> 23 + 24 + <div class="hero-terminal"> 25 + <div class="terminal-header"> 26 + <span class="terminal-dot"></span> 27 + <span class="terminal-dot"></span> 28 + <span class="terminal-dot"></span> 29 + </div> 30 + <pre class="terminal-content"><span class="terminal-prompt">$</span> docker login atcr.io 31 + <span class="terminal-prompt">$</span> docker push atcr.io/you/app 32 + 33 + <span class="terminal-comment"># same docker, decentralized</span></pre> 34 + </div> 35 + 36 + <div class="hero-actions"> 37 + <a href="/auth/oauth/login?return_to=/" class="btn-hero-primary">Get Started</a> 38 + <a href="/install" class="btn-hero-secondary">Learn More</a> 39 + </div> 40 + </div> 41 + 42 + <!-- Benefit Cards --> 43 + <div class="hero-benefits"> 44 + <div class="benefit-card"> 45 + <div class="benefit-icon">🐳</div> 46 + <h3>Works with Docker</h3> 47 + <p>Use docker push & pull. No new tools to learn.</p> 48 + </div> 49 + <div class="benefit-card"> 50 + <div class="benefit-icon">⚓</div> 51 + <h3>Your Data</h3> 52 + <p>Join shared holds or captain your own storage.</p> 53 + </div> 54 + <div class="benefit-card"> 55 + <div class="benefit-icon">🧭</div> 56 + <h3>Discover Images</h3> 57 + <p>Browse and star public container registries.</p> 58 + </div> 59 + </div> 60 + </section> 61 + {{ end }} 62 + 14 63 <main class="container"> 15 64 <div class="home-page"> 16 65 <!-- Featured Repositories Section -->
+2 -80
pkg/appview/templates/pages/install.html
··· 7 7 <title>Install ATCR Credential Helper - ATCR</title> 8 8 <link rel="stylesheet" href="/static/css/style.css"> 9 9 <script src="https://unpkg.com/htmx.org@1.9.10"></script> 10 - <style> 11 - .install-page { 12 - max-width: 800px; 13 - margin: 0 auto; 14 - padding: 2rem 1rem; 15 - } 16 - .install-section { 17 - margin: 2rem 0; 18 - } 19 - .install-section h2 { 20 - margin-bottom: 1rem; 21 - color: #1a1a1a; 22 - } 23 - .install-section h3 { 24 - margin: 1.5rem 0 0.5rem; 25 - color: #4a4a4a; 26 - font-size: 1.1rem; 27 - } 28 - .code-block { 29 - background: #f5f5f5; 30 - border: 1px solid #ddd; 31 - border-radius: 4px; 32 - padding: 1rem; 33 - margin: 0.5rem 0 1rem; 34 - overflow-x: auto; 35 - } 36 - .code-block code { 37 - font-family: 'Monaco', 'Menlo', monospace; 38 - font-size: 0.9rem; 39 - line-height: 1.5; 40 - } 41 - .platform-tabs { 42 - display: flex; 43 - gap: 0.5rem; 44 - border-bottom: 2px solid #e0e0e0; 45 - margin-bottom: 1rem; 46 - } 47 - .platform-tab { 48 - padding: 0.5rem 1rem; 49 - cursor: pointer; 50 - border: none; 51 - background: none; 52 - font-size: 1rem; 53 - color: #666; 54 - transition: all 0.2s; 55 - } 56 - .platform-tab:hover { 57 - color: #000; 58 - } 59 - .platform-tab.active { 60 - color: #0066cc; 61 - border-bottom: 2px solid #0066cc; 62 - margin-bottom: -2px; 63 - } 64 - .platform-content { 65 - display: none; 66 - } 67 - .platform-content.active { 68 - display: block; 69 - } 70 - .note { 71 - background: #fff3cd; 72 - border-left: 4px solid #ffc107; 73 - padding: 1rem; 74 - margin: 1rem 0; 75 - } 76 - .success { 77 - background: #d4edda; 78 - border-left: 4px solid #28a745; 79 - padding: 1rem; 80 - margin: 1rem 0; 81 - } 82 - </style> 83 10 </head> 84 11 <body> 85 12 {{ template "nav" . }} ··· 137 64 <h2>Authentication</h2> 138 65 <p>The credential helper will automatically prompt for authentication when you push or pull:</p> 139 66 140 - <div class="code-block"><code>export ATCR_AUTO_AUTH=1 141 - docker push {{ .RegistryURL }}/yourhandle/myapp:latest</code></div> 67 + <div class="code-block"><code>docker push {{ .RegistryURL }}/yourhandle/myapp:latest</code></div> 142 68 143 69 <p>This will:</p> 144 70 <ol> ··· 180 106 # Add to PATH if needed 181 107 export PATH="/usr/local/bin:$PATH"</code></div> 182 108 183 - <h3>Authentication failed</h3> 184 - <p>Make sure auto-auth is enabled:</p> 185 - <div class="code-block"><code>export ATCR_AUTO_AUTH=1</code></div> 186 - 187 109 <h3>Still having issues?</h3> 188 - <p>Check the <a href="https://github.com/atcr-io/atcr/blob/main/INSTALLATION.md">full documentation</a> or <a href="https://github.com/atcr-io/atcr/issues">open an issue</a>.</p> 110 + <p>Check the <a href="https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/INSTALLATION.md">full documentation</a> or <a href="https://tangled.org/@evan.jarrett.net/at-container-registry/issues">open an issue</a>.</p> 189 111 </div> 190 112 191 113 <div class="install-section">
+16
pkg/hold/authorization.go
··· 106 106 return false, fmt.Errorf("no PDS endpoint found for owner") 107 107 } 108 108 109 + // Build this hold's URI for filtering 110 + publicURL := s.config.Server.PublicURL 111 + if publicURL == "" { 112 + return false, fmt.Errorf("hold public URL not configured") 113 + } 114 + holdName, err := extractHostname(publicURL) 115 + if err != nil { 116 + return false, fmt.Errorf("failed to extract hold name: %w", err) 117 + } 118 + holdURI := fmt.Sprintf("at://%s/%s/%s", ownerDID, atproto.HoldCollection, holdName) 119 + 109 120 // Create unauthenticated client to read public records 110 121 client := atproto.NewClient(pdsEndpoint, ownerDID, "") 111 122 ··· 124 135 for _, record := range records { 125 136 var crewRecord atproto.HoldCrewRecord 126 137 if err := json.Unmarshal(record.Value, &crewRecord); err != nil { 138 + continue 139 + } 140 + 141 + // Only check crew records for THIS hold (prevents cross-hold access) 142 + if crewRecord.Hold != holdURI { 127 143 continue 128 144 } 129 145
+37 -11
pkg/hold/registration.go
··· 256 256 return nil 257 257 } 258 258 259 - // hasAllowAllCrewRecord checks if the allow-all crew record exists in the PDS 259 + // hasAllowAllCrewRecord checks if the allow-all crew record exists in the PDS for THIS hold 260 260 func (s *HoldService) hasAllowAllCrewRecord() (bool, error) { 261 261 ownerDID := s.config.Registration.OwnerDID 262 + publicURL := s.config.Server.PublicURL 262 263 if ownerDID == "" { 263 264 return false, fmt.Errorf("hold owner DID not configured") 265 + } 266 + if publicURL == "" { 267 + return false, fmt.Errorf("hold public URL not configured") 264 268 } 265 269 266 270 ctx := context.Background() ··· 282 286 return false, fmt.Errorf("no PDS endpoint found for owner") 283 287 } 284 288 289 + // Build hold-specific rkey 290 + holdName, err := extractHostname(publicURL) 291 + if err != nil { 292 + return false, fmt.Errorf("failed to extract hostname: %w", err) 293 + } 294 + crewRKey := fmt.Sprintf("allow-all-%s", holdName) 295 + 285 296 // Create unauthenticated client to read public records 286 297 client := atproto.NewClient(pdsEndpoint, ownerDID, "") 287 298 288 - // Query for specific rkey "allow-all" 289 - record, err := client.GetRecord(ctx, atproto.HoldCrewCollection, "allow-all") 299 + // Query for hold-specific allow-all record 300 + record, err := client.GetRecord(ctx, atproto.HoldCrewCollection, crewRKey) 290 301 if err != nil { 291 302 // Record doesn't exist 292 303 if errors.Is(err, atproto.ErrRecordNotFound) { ··· 302 313 } 303 314 304 315 // Check if it's the exact wildcard pattern 305 - return crewRecord.MemberPattern != nil && *crewRecord.MemberPattern == "*", nil 316 + if crewRecord.MemberPattern == nil || *crewRecord.MemberPattern != "*" { 317 + return false, nil 318 + } 319 + 320 + // Verify it's for this hold (defensive check) 321 + expectedHoldURI := fmt.Sprintf("at://%s/%s/%s", ownerDID, atproto.HoldCollection, holdName) 322 + return crewRecord.Hold == expectedHoldURI, nil 306 323 } 307 324 308 325 // createAllowAllCrewRecord creates a wildcard crew record allowing all authenticated users ··· 329 346 // Create wildcard crew record 330 347 crewRecord := atproto.NewHoldCrewRecordWithPattern(holdURI, "*", "write") 331 348 332 - _, err = client.PutRecord(ctx, atproto.HoldCrewCollection, "allow-all", crewRecord) 349 + // Use hold-specific rkey to support multiple holds with different allow-all settings 350 + crewRKey := fmt.Sprintf("allow-all-%s", holdName) 351 + _, err = client.PutRecord(ctx, atproto.HoldCrewCollection, crewRKey, crewRecord) 333 352 if err != nil { 334 353 return fmt.Errorf("failed to create allow-all crew record: %w", err) 335 354 } ··· 338 357 return nil 339 358 } 340 359 341 - // deleteAllowAllCrewRecord deletes the wildcard crew record 360 + // deleteAllowAllCrewRecord deletes the wildcard crew record for this hold 342 361 func (s *HoldService) deleteAllowAllCrewRecord(callbackHandler *http.HandlerFunc) error { 343 - // Safety check: only delete if it's the exact wildcard pattern 362 + // Safety check: only delete if it's the exact wildcard pattern for THIS hold 344 363 isWildcard, err := s.hasAllowAllCrewRecord() 345 364 if err != nil { 346 365 return fmt.Errorf("failed to check allow-all crew record: %w", err) 347 366 } 348 367 349 368 if !isWildcard { 350 - log.Printf("Warning: 'allow-all' crew record exists but is not wildcard - skipping deletion") 369 + log.Printf("Note: 'allow-all' crew record not found for this hold (may exist for other holds)") 351 370 return nil 352 371 } 353 372 373 + // Get hold name for rkey 374 + holdName, err := extractHostname(s.config.Server.PublicURL) 375 + if err != nil { 376 + return fmt.Errorf("failed to extract hostname: %w", err) 377 + } 378 + crewRKey := fmt.Sprintf("allow-all-%s", holdName) 379 + 354 380 // Run OAuth flow to get authenticated client 355 381 client, err := s.runOAuthFlow(callbackHandler, "Deleting allow-all crew record") 356 382 if err != nil { ··· 359 385 360 386 ctx := context.Background() 361 387 362 - // Delete the record 363 - err = client.DeleteRecord(ctx, atproto.HoldCrewCollection, "allow-all") 388 + // Delete the hold-specific allow-all record 389 + err = client.DeleteRecord(ctx, atproto.HoldCrewCollection, crewRKey) 364 390 if err != nil { 365 391 return fmt.Errorf("failed to delete allow-all crew record: %w", err) 366 392 } 367 393 368 - log.Printf("✓ Deleted allow-all crew record") 394 + log.Printf("✓ Deleted allow-all crew record for this hold") 369 395 return nil 370 396 } 371 397