Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepfix/audit-sweep-2026-07-26gatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
no-unauthenticated-writes.test.ts8.6 KB · 184 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
 * Every state-changing route is either guarded or explicitly declared public.
 *
 * POST /api/repos took the target namespace from the request body and had no
 * auth middleware at all, so anyone could create repositories in anyone's
 * account. POST /api/setup upserted a user with the fixed password
 * "changeme". Both shipped because nothing enumerated the write surface —
 * there are 413 state-changing routes and no list of which ones are meant to
 * be reachable anonymously.
 *
 * This test does not try to prove a route is *correctly* authorized; static
 * analysis can't, and there are at least five guard idioms in this codebase
 * (inline middleware, router-level `.use("*")`, router prefix `.use("/x/*")`,
 * an in-handler `gate(c)` returning a Response, and bespoke helpers like
 * `requireSandboxAdmin`). What it does is force a decision: a write route
 * either matches a recognised guard idiom, or it appears in PUBLIC_WRITES
 * with a stated reason. A newly added unguarded write fails the build.
 *
 * When this test fails on a route you added: if it should be authenticated,
 * add a guard. If it genuinely must be public — an auth entry point, a
 * third-party webhook receiver, the git protocol — add it to PUBLIC_WRITES
 * with the reason.
 */

import { describe, expect, it } from "bun:test";
import { readdirSync, readFileSync, statSync } from "fs";
import { join } from "path";

const ROOT = "src/routes";

/** Guards that appear in middleware position on the route registration. */
const MIDDLEWARE_GUARD =
  /requireAuth|requireApiAuth|requireAdmin|requireScope|requireRepoAccess|requireOrgRole|agentAuth|adminOnly/;

/**
 * Guards that appear inside the handler body and return a Response on
 * failure before any state is touched.
 */
const HANDLER_GUARD =
  /\bgate\(c\)|require[A-Z]\w*\(c\)|isSiteAdmin|assertAdmin|assertRepoReadable|assertRepoWritable|resolveRepoAccess|c\.get\("user"\)!|const viewer = c\.get\("user"\)|const user = c\.get\("user"\)|if \(!c\.get\("user"\)\)|verifyBearer|authenticateBearer|verifyPatAuth|verifySignature|EMERGENCY_PAT_SECRET|scimAuth|verifyScimToken/;

/**
 * Write routes that are reachable anonymously ON PURPOSE. Each needs a
 * reason, because "it was already like that" is how POST /api/setup survived.
 */
const PUBLIC_WRITES: Record<string, string> = {
  // Authentication entry points — cannot require an existing session.
  "POST /register": "signup",
  "POST /login": "sign-in",
  "POST /login/2fa": "second factor; the session exists but is not yet valid",
  "POST /login/magic": "magic-link request",
  "POST /api/auth/register": "signup (API)",
  "POST /api/auth/login": "sign-in (API)",
  "POST /oauth/token": "OAuth token exchange; authenticates via client creds",
  "POST /api/passkeys/auth/options": "WebAuthn sign-in challenge; username optional, no session yet",
  "POST /api/passkeys/auth/verify": "WebAuthn sign-in; authenticates via the signed assertion",
  "POST /forgot-password": "password reset request",
  "POST /reset-password": "password reset completion; authenticates via token",

  // Git Smart HTTP — authenticates itself via lib/git-push-auth.ts.
  "POST /:owner/:repo.git/git-upload-pack": "git protocol, own auth",
  "POST /:owner/:repo.git/git-receive-pack": "git protocol, own auth",

  // Third-party webhook receivers — authenticate by signature, not session.
  "POST /api/hooks/gatetest": "GateTest callback",
  "POST /hooks/pagerduty": "incident webhook",
  "POST /hooks/datadog": "incident webhook",
  "POST /hooks/opsgenie": "incident webhook",
  "POST /hooks/incident": "incident webhook",
  "POST /api/v2/integrations/slack/events": "Slack Events API",
  "POST /api/v2/integrations/discord/interactions": "Discord interactions",
  "POST /api/webhooks/stripe": "Stripe webhook; verified by stripe-signature",
  "POST /sso/saml/:orgSlug/callback": "SAML assertion consumer; authenticates via the signed assertion",

  // Bearer-token surfaces whose guard helper this scan does not recognise.
  // Each was read and confirmed to reject an unauthenticated caller.
  "POST /api/v1/runs/:runId/artifacts": "CI artifact upload; glc_ PAT bearer with repo/write scope",
  "DELETE /api/v1/artifacts/:artifactId": "CI artifact delete; glc_ PAT bearer",
  "POST /deploy/started": "deploy event; Authorization: Bearer DEPLOY_EVENT_TOKEN",
  "POST /deploy/finished": "deploy event; Authorization: Bearer DEPLOY_EVENT_TOKEN",
  "POST /deploy/step": "deploy event; Authorization: Bearer DEPLOY_EVENT_TOKEN",
  "POST /v2/*": "OCI registry; Docker Basic auth validated against api_tokens",
  "PUT /v2/*": "OCI registry; Docker Basic auth validated against api_tokens",
  "PATCH /v2/*": "OCI registry; Docker Basic auth validated against api_tokens",
  "DELETE /v2/*": "OCI registry; Docker Basic auth validated against api_tokens",

  // OAuth endpoints that are public by specification.
  "POST /oauth/register": "RFC 7591 dynamic client registration",
  "POST /oauth/revoke": "RFC 7009 revocation; authenticates via the token itself",
  "POST /api/v2/integrations/slack/install": "OAuth install redirect start",
  "POST /api/v2/integrations/discord/install": "OAuth install redirect start",

  // Deliberately public product surfaces.
  "POST /loops/:slug/invoke": "public hosted loop; gated on loop.isPublic",
  "POST /enterprise/contact": "contact form",
  "POST /markdown/preview": "stateless render, touches no state",
  "POST /play": "playground account creation; the point is to need no account",
  "POST /status/subscribe": "status-page email subscription",
  "POST /flywheel-telemetry/feedback": "anonymous product telemetry",
  "POST /api/claude/session": "fire-and-forget session telemetry, documented as no-auth",
  "POST /setup": "first-run bootstrap; refuses once the users table is non-empty",
};

function walk(d: string): string[] {
  const out: string[] = [];
  for (const e of readdirSync(d)) {
    const p = join(d, e);
    if (statSync(p).isDirectory()) out.push(...walk(p));
    else if (e.endsWith(".ts") || e.endsWith(".tsx")) out.push(p);
  }
  return out;
}

type Route = { method: string; path: string; file: string; guard: string };

function analyze(): Route[] {
  const routes: Route[] = [];
  for (const f of walk(ROOT)) {
    const src = readFileSync(f, "utf8");
    const rel = f.replace(/\\/g, "/").split("/routes/")[1];

    const prefixGuards: string[] = [];
    let guardAll = false;
    for (const m of src.matchAll(/\.use\(\s*(["'`])([^"'`]*)\1\s*,([\s\S]{0,200}?)\)\s*;/g)) {
      if (!MIDDLEWARE_GUARD.test(m[3])) continue;
      if (m[2] === "*") guardAll = true;
      else prefixGuards.push(m[2]);
    }

    const re = /\.(post|put|patch|delete)\(\s*(["'`])([^"'`\n]*)\2/g;
    let m;
    while ((m = re.exec(src))) {
      const path = m[3];
      if (!path.startsWith("/")) continue;
      const window = src.slice(m.index, m.index + 1200);
      // Middleware sits between the path literal and the handler arrow.
      const arrow = window.indexOf("=>");
      const sig = arrow > -1 ? window.slice(0, arrow) : window.slice(0, 300);

      let guard = "NONE";
      if (MIDDLEWARE_GUARD.test(sig)) guard = "inline";
      else if (guardAll) guard = "router-*";
      else if (prefixGuards.some((g) => { const p = g.replace(/\*$/, ""); return p && path.startsWith(p); })) guard = "router-prefix";
      else if (HANDLER_GUARD.test(window)) guard = "in-handler";
      routes.push({ method: m[1].toUpperCase(), path, file: rel, guard });
    }
  }
  return routes;
}

const ROUTES = analyze();

describe("state-changing routes", () => {
  it("finds a substantial write surface (guards against the scan silently breaking)", () => {
    // If a refactor breaks the regex this test would vacuously pass, so
    // assert the scan still sees roughly the surface we know exists.
    expect(ROUTES.length).toBeGreaterThan(300);
  });

  it("every unguarded write is explicitly declared public", () => {
    const unexplained = ROUTES
      .filter((r) => r.guard === "NONE")
      .filter((r) => !(`${r.method} ${r.path}` in PUBLIC_WRITES))
      .map((r) => `${r.method} ${r.path}  (${r.file})`)
      .sort();
    expect(unexplained).toEqual([]);
  });

  it("every PUBLIC_WRITES entry carries a reason", () => {
    for (const [route, reason] of Object.entries(PUBLIC_WRITES)) {
      expect(reason.length, `${route} needs a reason`).toBeGreaterThan(3);
    }
  });

  it("PUBLIC_WRITES has no stale entries", () => {
    // A route that gained a guard, or was deleted, should leave the list —
    // otherwise the allowlist rots into a blanket exemption.
    const live = new Set(
      ROUTES.filter((r) => r.guard === "NONE").map((r) => `${r.method} ${r.path}`)
    );
    const stale = Object.keys(PUBLIC_WRITES).filter((k) => !live.has(k));
    expect(stale).toEqual([]);
  });
});