Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

deps.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

deps.tsxBlame440 lines · 1 contributor
8098672Claude1/**
2 * Block J1 — Dependency graph routes.
3 *
4 * GET /:owner/:repo/dependencies — grouped list + summary
5 * POST /:owner/:repo/dependencies/reindex — owner-only, walk manifests
6 */
7
8import { Hono } from "hono";
9import { and, eq } from "drizzle-orm";
10import { db } from "../db";
11import { repositories, users } from "../db/schema";
12import { Layout } from "../views/layout";
13import { RepoHeader, RepoNav } from "../views/components";
14import { softAuth, requireAuth } from "../middleware/auth";
15import type { AuthEnv } from "../middleware/auth";
16import {
17 indexRepositoryDependencies,
18 listDependenciesForRepo,
19 summarizeDependencies,
20} from "../lib/deps";
b426348Claude21import { generateSpdx, generateCycloneDx } from "../lib/sbom";
7a1c00eClaude22import { scanLicenses, type LicenseReport, type LicenseRisk } from "../lib/license-scan";
8098672Claude23
24const deps = new Hono<AuthEnv>();
25deps.use("*", softAuth);
26
27async function loadRepo(ownerName: string, repoName: string) {
28 const [owner] = await db
29 .select()
30 .from(users)
31 .where(eq(users.username, ownerName))
32 .limit(1);
33 if (!owner) return null;
34 const [repo] = await db
35 .select()
36 .from(repositories)
37 .where(
38 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
39 )
40 .limit(1);
41 if (!repo) return null;
42 return { owner, repo };
43}
44
45// ---------- Overview ----------
46
47deps.get("/:owner/:repo/dependencies", async (c) => {
48 const user = c.get("user");
49 const { owner: ownerName, repo: repoName } = c.req.param();
50 const ctx = await loadRepo(ownerName, repoName);
51 if (!ctx) return c.notFound();
52 const { repo } = ctx;
53 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
54 return c.notFound();
55 }
56
57 const all = await listDependenciesForRepo(repo.id);
58 const summary = await summarizeDependencies(repo.id);
59 const isOwner = !!user && user.id === repo.ownerId;
60 const message = c.req.query("message");
61 const error = c.req.query("error");
62
63 // Group by ecosystem
64 const grouped = new Map<string, typeof all>();
65 for (const d of all) {
66 const list = grouped.get(d.ecosystem) || [];
67 list.push(d);
68 grouped.set(d.ecosystem, list);
69 }
70
71 return c.html(
72 <Layout
73 title={`Dependencies — ${ownerName}/${repoName}`}
74 user={user}
75 >
76 <RepoHeader owner={ownerName} repo={repoName} />
77 <RepoNav owner={ownerName} repo={repoName} active="code" />
78 <div class="settings-container">
79 <div style="display:flex;justify-content:space-between;align-items:center">
80 <h2 style="margin:0">Dependencies</h2>
b426348Claude81 <div style="display:flex;gap:8px;align-items:center">
82 {all.length > 0 && (
83 <>
7a1c00eClaude84 <a href={`/${ownerName}/${repoName}/dependencies/licenses`} class="btn btn-sm" style="text-decoration:none">
85 License Scan
86 </a>
b426348Claude87 <a href={`/${ownerName}/${repoName}/dependencies/sbom/spdx`} class="btn btn-sm" style="text-decoration:none">
88 SPDX
89 </a>
90 <a href={`/${ownerName}/${repoName}/dependencies/sbom/cyclonedx`} class="btn btn-sm" style="text-decoration:none">
91 CycloneDX
92 </a>
93 </>
94 )}
95 {isOwner && (
96 <form
9837657Claude97 method="post"
b426348Claude98 action={`/${ownerName}/${repoName}/dependencies/reindex`}
99 style="margin:0"
100 >
101 <button type="submit" class="btn btn-primary btn-sm">
102 Reindex
103 </button>
104 </form>
105 )}
106 </div>
8098672Claude107 </div>
108 {message && (
109 <div class="auth-success" style="margin-top:12px">
110 {decodeURIComponent(message)}
111 </div>
112 )}
113 {error && (
114 <div class="auth-error" style="margin-top:12px">
115 {decodeURIComponent(error)}
116 </div>
117 )}
118 <p style="color:var(--text-muted);margin-top:8px">
119 Parsed from <code>package.json</code>, <code>requirements.txt</code>,{" "}
120 <code>pyproject.toml</code>, <code>go.mod</code>,{" "}
121 <code>Cargo.toml</code>, <code>Gemfile</code>, and{" "}
122 <code>composer.json</code> on the default branch. Transitive
123 dependencies are not resolved.
124 </p>
125
126 {all.length === 0 ? (
127 <div class="panel-empty" style="padding:24px">
128 No dependencies indexed yet.
129 {isOwner && " Click Reindex to scan the repository."}
130 </div>
131 ) : (
132 <>
133 <div
134 style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin:16px 0"
135 >
136 <div class="panel" style="padding:12px;text-align:center">
137 <div style="font-size:20px;font-weight:700">
138 {all.length}
139 </div>
140 <div
141 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
142 >
143 Dependencies
144 </div>
145 </div>
146 {summary.map((s) => (
147 <div class="panel" style="padding:12px;text-align:center">
148 <div style="font-size:20px;font-weight:700">
149 {s.count}
150 </div>
151 <div
152 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
153 >
154 {s.ecosystem}
155 </div>
156 </div>
157 ))}
158 </div>
159
160 {Array.from(grouped.entries()).map(([ecosystem, list]) => (
161 <>
162 <h3 style="margin-top:20px">
163 {ecosystem}{" "}
164 <span
165 style="font-size:12px;color:var(--text-muted);font-weight:400"
166 >
167 ({list.length})
168 </span>
169 </h3>
170 <div class="panel" style="margin-bottom:12px">
171 {list.map((d) => (
172 <div
173 class="panel-item"
174 style="justify-content:space-between;flex-wrap:wrap;gap:6px"
175 >
176 <div>
177 <span
178 style="font-family:var(--font-mono);font-weight:600"
179 >
180 {d.name}
181 </span>
182 {d.versionSpec && (
183 <span
184 style="margin-left:8px;font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
185 >
186 {d.versionSpec}
187 </span>
188 )}
189 {d.isDev && (
190 <span
191 style="margin-left:8px;font-size:10px;padding:2px 6px;background:var(--bg-subtle);border-radius:3px;color:var(--text-muted);text-transform:uppercase"
192 >
193 dev
194 </span>
195 )}
196 </div>
197 <a
198 href={`/${ownerName}/${repoName}/blob/HEAD/${d.manifestPath}`}
199 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
200 >
201 {d.manifestPath}
202 </a>
203 </div>
204 ))}
205 </div>
206 </>
207 ))}
208 </>
209 )}
210 </div>
211 </Layout>
212 );
213});
214
215// ---------- Reindex (owner-only) ----------
216
217deps.post("/:owner/:repo/dependencies/reindex", requireAuth, async (c) => {
218 const user = c.get("user");
219 const { owner: ownerName, repo: repoName } = c.req.param();
220 const ctx = await loadRepo(ownerName, repoName);
221 if (!ctx) return c.notFound();
222 const { repo } = ctx;
223 if (!user || user.id !== repo.ownerId) {
224 return c.html(
225 <Layout title="Forbidden" user={user}>
226 <div class="empty-state">
227 <h2>403</h2>
228 <p>Only the repository owner can reindex dependencies.</p>
229 </div>
230 </Layout>,
231 403
232 );
233 }
234
235 const result = await indexRepositoryDependencies(repo.id);
236 const to = `/${ownerName}/${repoName}/dependencies`;
237 if (!result) {
238 return c.redirect(
239 `${to}?error=${encodeURIComponent(
240 "Reindex failed — is the default branch empty?"
241 )}`
242 );
243 }
244 return c.redirect(
245 `${to}?message=${encodeURIComponent(
246 `Indexed ${result.indexed} dependencies across ${result.manifests} manifests.`
247 )}`
248 );
249});
250
7a1c00eClaude251// ---------- License Compliance ----------
252
253deps.get("/:owner/:repo/dependencies/licenses", async (c) => {
254 const user = c.get("user");
255 const { owner: ownerName, repo: repoName } = c.req.param();
256 const ctx = await loadRepo(ownerName, repoName);
257 if (!ctx) return c.notFound();
258 const { repo } = ctx;
259 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) return c.notFound();
260
261 const report = await scanLicenses(repo.id);
262
263 const riskColor = (risk: LicenseRisk): string => {
264 if (risk === "high") return "#f85149";
265 if (risk === "medium") return "#d29922";
266 if (risk === "low") return "#58a6ff";
267 if (risk === "unknown") return "#8b949e";
268 return "#3fb950";
269 };
270
271 const riskBadge = (risk: LicenseRisk) => (
272 <span style={`font-size:11px;padding:2px 8px;border-radius:3px;background:${riskColor(risk)}22;color:${riskColor(risk)};font-weight:600;text-transform:uppercase`}>
273 {risk}
274 </span>
275 );
276
277 return c.html(
278 <Layout title={`License Compliance — ${ownerName}/${repoName}`} user={user}>
279 <RepoHeader owner={ownerName} repo={repoName} />
280 <RepoNav owner={ownerName} repo={repoName} active="code" />
281 <div class="settings-container">
282 <div style="display:flex;justify-content:space-between;align-items:center">
283 <h2 style="margin:0">License Compliance</h2>
284 <a href={`/${ownerName}/${repoName}/dependencies`} class="btn btn-sm" style="text-decoration:none">
285 Back to Dependencies
286 </a>
287 </div>
288
289 <div class={`panel ${report.compliant ? "" : "auth-error"}`} style="padding:16px;margin-top:16px">
290 <div style="font-weight:600;font-size:16px;margin-bottom:4px">
291 {report.compliant ? "Compliant" : "Action Required"}
292 </div>
293 <div style="color:var(--text-muted)">{report.summary}</div>
294 <div style="font-size:12px;color:var(--text-muted);margin-top:8px">
295 {report.scanned} of {report.totalDeps} dependencies scanned
296 </div>
297 </div>
298
299 {report.risks.high.length > 0 && (
300 <>
301 <h3 style="margin-top:24px;color:#f85149">High Risk ({report.risks.high.length})</h3>
302 <div class="panel">
303 {report.risks.high.map((d) => (
304 <div class="panel-item" style="justify-content:space-between;flex-wrap:wrap;gap:6px">
305 <div>
306 <span style="font-family:var(--font-mono);font-weight:600">{d.name}</span>
307 <span style="margin-left:8px;font-size:12px;color:var(--text-muted)">{d.version}</span>
308 {riskBadge(d.risk)}
309 </div>
310 <div style="font-size:12px;color:var(--text-muted)">{d.license} — {d.reason}</div>
311 </div>
312 ))}
313 </div>
314 </>
315 )}
316
317 {report.risks.medium.length > 0 && (
318 <>
319 <h3 style="margin-top:24px;color:#d29922">Medium Risk ({report.risks.medium.length})</h3>
320 <div class="panel">
321 {report.risks.medium.map((d) => (
322 <div class="panel-item" style="justify-content:space-between;flex-wrap:wrap;gap:6px">
323 <div>
324 <span style="font-family:var(--font-mono);font-weight:600">{d.name}</span>
325 <span style="margin-left:8px;font-size:12px;color:var(--text-muted)">{d.version}</span>
326 {riskBadge(d.risk)}
327 </div>
328 <div style="font-size:12px;color:var(--text-muted)">{d.license} — {d.reason}</div>
329 </div>
330 ))}
331 </div>
332 </>
333 )}
334
335 {report.risks.unknown.length > 0 && (
336 <>
337 <h3 style="margin-top:24px;color:#8b949e">Unknown License ({report.risks.unknown.length})</h3>
338 <div class="panel">
339 {report.risks.unknown.map((d) => (
340 <div class="panel-item" style="justify-content:space-between;flex-wrap:wrap;gap:6px">
341 <div>
342 <span style="font-family:var(--font-mono);font-weight:600">{d.name}</span>
343 <span style="margin-left:8px;font-size:12px;color:var(--text-muted)">{d.version}</span>
344 {riskBadge(d.risk)}
345 </div>
346 <div style="font-size:12px;color:var(--text-muted)">{d.ecosystem} — {d.reason}</div>
347 </div>
348 ))}
349 </div>
350 </>
351 )}
352
353 {report.risks.low.length > 0 && (
354 <>
355 <h3 style="margin-top:24px">Other ({report.risks.low.length})</h3>
356 <div class="panel">
357 {report.risks.low.map((d) => (
358 <div class="panel-item" style="justify-content:space-between;flex-wrap:wrap;gap:6px">
359 <div>
360 <span style="font-family:var(--font-mono);font-weight:600">{d.name}</span>
361 <span style="margin-left:8px;font-size:12px;color:var(--text-muted)">{d.version}</span>
362 {riskBadge(d.risk)}
363 </div>
364 <div style="font-size:12px;color:var(--text-muted)">{d.license} — {d.reason}</div>
365 </div>
366 ))}
367 </div>
368 </>
369 )}
370 </div>
371 </Layout>
372 );
373});
374
375// ---------- License API ----------
376
377deps.get("/api/repos/:owner/:repo/licenses", softAuth, async (c) => {
378 const user = c.get("user");
379 const { owner: ownerName, repo: repoName } = c.req.param();
380 const ctx = await loadRepo(ownerName, repoName);
381 if (!ctx) return c.json({ error: "Not found" }, 404);
382 const { repo } = ctx;
383 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
384 return c.json({ error: "Not found" }, 404);
385 }
386 return c.json(await scanLicenses(repo.id));
387});
388
b426348Claude389// ---------- SBOM Export (SPDX) ----------
390
391deps.get("/:owner/:repo/dependencies/sbom/spdx", async (c) => {
392 const user = c.get("user");
393 const { owner: ownerName, repo: repoName } = c.req.param();
394 const ctx = await loadRepo(ownerName, repoName);
395 if (!ctx) return c.notFound();
396 const { repo } = ctx;
397 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) return c.notFound();
398
399 const spdx = await generateSpdx(repo.id, `${ownerName}/${repoName}`);
400 return c.json(spdx, 200, {
401 "Content-Disposition": `attachment; filename="${repoName}-sbom-spdx.json"`,
402 });
403});
404
405// ---------- SBOM Export (CycloneDX) ----------
406
407deps.get("/:owner/:repo/dependencies/sbom/cyclonedx", async (c) => {
408 const user = c.get("user");
409 const { owner: ownerName, repo: repoName } = c.req.param();
410 const ctx = await loadRepo(ownerName, repoName);
411 if (!ctx) return c.notFound();
412 const { repo } = ctx;
413 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) return c.notFound();
414
415 const bom = await generateCycloneDx(repo.id, `${ownerName}/${repoName}`);
416 return c.json(bom, 200, {
417 "Content-Disposition": `attachment; filename="${repoName}-sbom-cyclonedx.json"`,
418 });
419});
420
421// ---------- SBOM API (JSON) ----------
422
423deps.get("/api/repos/:owner/:repo/sbom", softAuth, async (c) => {
424 const user = c.get("user");
425 const { owner: ownerName, repo: repoName } = c.req.param();
426 const format = c.req.query("format") || "cyclonedx";
427 const ctx = await loadRepo(ownerName, repoName);
428 if (!ctx) return c.json({ error: "Not found" }, 404);
429 const { repo } = ctx;
430 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
431 return c.json({ error: "Not found" }, 404);
432 }
433
434 if (format === "spdx") {
435 return c.json(await generateSpdx(repo.id, `${ownerName}/${repoName}`));
436 }
437 return c.json(await generateCycloneDx(repo.id, `${ownerName}/${repoName}`));
438});
439
8098672Claude440export default deps;