| 87 | 87 | const adminToken = process.env.FORGEJO_ADMIN_TOKEN; |
| 88 | 88 | if (adminToken) { |
| 89 | 89 | const r = await fetch(`${FORGEJO_INTERNAL}/api/v1/admin/users?limit=200`, { |
| 90 | | - headers: { Authorization: `token ${adminToken}` }, |
| 90 | + headers: adminApiHeaders(), |
| 91 | 91 | }); |
| 92 | 92 | if (r.ok) users = (await r.json()) as ForgejoUserSummary[]; |
| 93 | 93 | } |
| 269 | 269 | // exposing git.tdd.md externally. |
| 270 | 270 | const FORGEJO_INTERNAL = process.env.FORGEJO_URL ?? "https://git.tdd.md"; |
| 271 | 271 | |
| 272 | +// Admin-token-authenticated headers for API calls. Agent repos are |
| 273 | +// private by default; rendering the verdict page must still work. We |
| 274 | +// proxy the data through the admin identity, never exposing the source |
| 275 | +// or push protocol publicly. |
| 276 | +const adminApiHeaders = (): HeadersInit => { |
| 277 | + const token = process.env.FORGEJO_ADMIN_TOKEN; |
| 278 | + return token ? { Authorization: `token ${token}` } : {}; |
| 279 | +}; |
| 280 | + |
| 272 | 281 | const HOP_BY_HOP = [ |
| 273 | 282 | "host", |
| 274 | 283 | "connection", |
| 329 | 338 | description: string; |
| 330 | 339 | clone_url: string; |
| 331 | 340 | empty: boolean; |
| 341 | + private: boolean; |
| 332 | 342 | } |
| 333 | 343 | |
| 334 | 344 | interface ForgejoCommit { |
| 351 | 361 | |
| 352 | 362 | const renderRepoView = async (owner: string, repo: string): Promise<Response> => { |
| 353 | 363 | const repoApi = `${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; |
| 354 | | - const repoRes = await fetch(repoApi); |
| 364 | + const repoRes = await fetch(repoApi, { headers: adminApiHeaders() }); |
| 355 | 365 | if (repoRes.status === 404) { |
| 356 | 366 | const html = await renderNotFound(`/${owner}/${repo}`); |
| 357 | 367 | return htmlResponse(html, 404); |
| 365 | 375 | } |
| 366 | 376 | const info = (await repoRes.json()) as ForgejoRepoSummary; |
| 367 | 377 | const cloneUrl = info.clone_url || `https://tdd.md/${owner}/${repo}.git`; |
| 378 | + const isPrivate = info.private === true; |
| 368 | 379 | |
| 369 | 380 | // The repo name is by convention the kata id. If the kata exists, the |
| 370 | 381 | // header link is meaningful and we know the total step count. |
| 380 | 391 | |
| 381 | 392 | let commits: ForgejoCommit[] = []; |
| 382 | 393 | if (!info.empty) { |
| 383 | | - const commitsRes = await fetch(`${repoApi}/commits?limit=50&stat=false`); |
| 394 | + const commitsRes = await fetch(`${repoApi}/commits?limit=50&stat=false`, { |
| 395 | + headers: adminApiHeaders(), |
| 396 | + }); |
| 384 | 397 | if (commitsRes.ok) commits = (await commitsRes.json()) as ForgejoCommit[]; |
| 385 | 398 | } |
| 386 | 399 | const progress = computeProgress(commits); |
| 415 | 428 | const kataLink = kataExists |
| 416 | 429 | ? `[\`${repo}\` →](/games/${repo})` |
| 417 | 430 | : `\`${repo}\``; |
| 431 | + const privateBadge = isPrivate ? ` <span class="muted">[private]</span>` : ""; |
| 418 | 432 | |
| 419 | 433 | const verdict = latestRun(owner, repo); |
| 420 | 434 | const headSha = commits[0]?.sha ?? null; |
| 463 | 477 | scoreSection = `${modeLine}**total: ${sign}${verdict.totalScore}** · judged ${relativeTime(new Date(verdict.judgedAt).toISOString())}${stale}\n\n${rows}${refactorRows}`; |
| 464 | 478 | } |
| 465 | 479 | |
| 466 | | - const body = `# ${owner} · playing ${kataLink} |
| 480 | + const body = `# ${owner} · playing ${kataLink}${privateBadge} |
| 467 | 481 | |
| 468 | 482 | > ${status} |
| 469 | 483 | > **${stepCounter}** steps verified |
| 561 | 575 | "/agents/register": htmlResponse(REGISTER_HTML), |
| 562 | 576 | "/agents/:name": async (req) => { |
| 563 | 577 | const name = req.params.name; |
| 564 | | - const userRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}`); |
| 578 | + const userRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}`, { |
| 579 | + headers: adminApiHeaders(), |
| 580 | + }); |
| 565 | 581 | if (userRes.status === 404) { |
| 566 | 582 | const html = await renderPage({ |
| 567 | 583 | title: `${name} — agents — tdd.md`, |
| 571 | 587 | }); |
| 572 | 588 | return htmlResponse(html, 404); |
| 573 | 589 | } |
| 574 | | - const reposRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}/repos?limit=50`); |
| 590 | + const reposRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}/repos?limit=50`, { |
| 591 | + headers: adminApiHeaders(), |
| 592 | + }); |
| 575 | 593 | const repos = reposRes.ok ? ((await reposRes.json()) as { name: string; description: string }[]) : []; |
| 576 | 594 | |
| 577 | 595 | const progressByRepo = await Promise.all( |
| 578 | 596 | repos.map(async (r) => { |
| 579 | | - const cRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`); |
| 597 | + const cRes = await fetch( |
| 598 | + `${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`, |
| 599 | + { headers: adminApiHeaders() }, |
| 600 | + ); |
| 580 | 601 | const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : []; |
| 581 | 602 | return { repo: r, progress: computeProgress(commits) }; |
| 582 | 603 | }), |