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

private-repo-subpage-gate.test.ts

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

private-repo-subpage-gate.test.tsBlame87 lines · 1 contributor
8ed1b31ccantynz-alt1/**
2 * Platform-wide private-repo gate for /:owner/:repo/... subpages.
3 *
4 * Every repo subpage owned its own privacy check and it drifted. A live probe
5 * of a private repo on 2026-07-28 found 24 subpages returning 200 to an
6 * anonymous visitor while the repo root correctly 404'd:
7 *
8 * /actions /archaeology /cloud-deployments /contributors /coupling
9 * /dependencies /deployments /discussions /explain /gates /health
10 * /insights /packages /previews /projects /pushes /queue /wiki
11 * /insights/engineering /ai/changelog /ai/tests /docs/tracking
12 * /search/nl /search/semantic
13 *
14 * These leaked branch and workflow names, CI logs, deployment history,
15 * dependency manifests and wiki content for repositories the caller could not
16 * open.
17 *
18 * The gate is registered once in app.tsx. What must not regress: it runs
19 * before the repo routers, after softAuth, denies with 404 rather than 403,
20 * and falls through for anything that is not a resolvable private repo — so
21 * /settings/tokens, /orgs/new and the git Smart HTTP paths are untouched.
22 */
23
24import { describe, expect, it } from "bun:test";
25import { readFileSync } from "fs";
26
27const SRC = readFileSync("src/app.tsx", "utf8");
28const gate = SRC.slice(
29 SRC.indexOf('app.use("/:owner/:repo/*"'),
30 SRC.indexOf('app.route("/", gitRoutes)')
31);
32
33describe("gate placement", () => {
34 it("is registered before every repo router", () => {
35 const gateIdx = SRC.indexOf('app.use("/:owner/:repo/*"');
36 expect(gateIdx).toBeGreaterThan(-1);
37 // gitRoutes is the first router mounted; webRoutes the last.
38 expect(gateIdx).toBeLessThan(SRC.indexOf('app.route("/", gitRoutes)'));
39 expect(gateIdx).toBeLessThan(SRC.indexOf('app.route("/", webRoutes)'));
40 });
41
42 it("runs after softAuth so the viewer is resolved", () => {
43 // Registered earlier and c.get("user") would always be undefined, which
44 // would 404 private repos even for their own owner.
45 expect(SRC.indexOf('app.use("*", softAuth)')).toBeLessThan(
46 SRC.indexOf('app.use("/:owner/:repo/*"')
47 );
48 });
49});
50
51describe("deny behaviour", () => {
52 it("denies with 404, never 403", () => {
53 expect(gate).toContain('access !== "none"');
54 expect(gate).toContain("404");
55 expect(gate).not.toMatch(/,\s*403\s*\)/);
56 });
57
58 it("uses resolveRepoAccess so collaborators and org members still pass", () => {
59 expect(gate).toContain("resolveRepoAccess");
60 // An ownerId equality check would lock out collaborators.
61 expect(gate).not.toMatch(/user\.id\s*!==/);
62 });
63
64 it("answers JSON callers with JSON", () => {
65 expect(gate).toContain("application/json");
66 });
67});
68
69describe("false positives fall through", () => {
70 it("passes through when the repo does not resolve", () => {
71 // /settings/tokens, /orgs/new, /:owner/:repo.git/* all resolve to null.
72 expect(gate).toContain("if (!row || !row.isPrivate) return next()");
73 });
74
75 it("passes through public repos untouched", () => {
76 // Same line: a non-private row short-circuits before any access lookup,
77 // so public browsing costs nothing extra beyond the namespace read.
78 expect(gate).toContain("!row.isPrivate");
79 });
80
81 it("fails OPEN on a namespace lookup error", () => {
82 // A DB blip must not 404 the entire public site. The handler downstream
83 // surfaces its own error instead.
84 const cat = gate.slice(gate.indexOf("} catch {"), gate.indexOf("if (!row"));
85 expect(cat).toContain("return next()");
86 });
87});