Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit03e6f9b

security: fix the remaining 11 files with the broken glued-wildcard auth-gate pattern

security: fix the remaining 11 files with the broken glued-wildcard auth-gate pattern

Same root cause as 7b0a81b (admin-security.tsx) and the org-secrets.tsx
fix earlier this session: `router.use("/some/path*", middleware)` --
no slash before the `*` -- does not match the bare path in this Hono
version, so the middleware silently never runs for it.

Genuinely broken auth gates (requireAuth never applied -- pages were
reachable unauthenticated, crashing 500 on `c.get("user")!` rather than
leaking data, since all six force-unwrap the user):
  - tokens.tsx: /settings/tokens, /api/user/tokens (personal access tokens)
  - settings-sessions.tsx: /settings/sessions (active session list/revoke)
  - settings-integrations.tsx: /settings/integrations (Slack/Discord/Teams
    webhook URLs + signing secrets)
  - settings-agents.tsx: /settings/agents (AI agent session budgets)
  - deploy-targets.tsx: /settings/deploy-targets (SSH deploy target mgmt)

Lower-impact instances (softAuth only -- app.tsx's global
`app.use("*", softAuth)` already populates the user context for every
request, so these were redundant-but-broken rather than exploitable;
fixed for correctness/consistency, not because they were leaking):
  - cross-repo-search.tsx, debt-map.tsx, pulse.tsx,
    push-notifications.tsx, stale-branches.tsx

Fix is the same verified pattern throughout: register the guard on the
exact path AND the "/path/*" sub-path form separately, confirmed
against this repo's actual Hono version via a standalone reproduction
before it was applied anywhere.

Added src/__tests__/wildcard-auth-gate-regression.test.ts covering the
requireAuth-protected pages that had zero route-level test coverage
before this (every one of the 13 affected files had none -- that's how
this survived). bun test: 3107 pass (was 3098), same 4 pre-existing
unrelated failures, no regressions.
ccantynz-alt committed on July 20, 2026Parent: 7b0a81b
12 files changed+1071303e6f9b835c80b5234fa2ccdf50d79cb1d0d6823
12 changed files+107−13
Addedsrc/__tests__/wildcard-auth-gate-regression.test.ts+58−0View fileUnifiedSplit
1/**
2 * Regression coverage for a systemic bug found via a full production HTTP
3 * sweep: 13 route files registered their auth middleware with a glued
4 * wildcard pattern (e.g. `router.use("/settings/tokens*", requireAuth)` --
5 * no slash before the `*`). That pattern does not match the bare path at
6 * all in this Hono version (confirmed via a standalone reproduction), so
7 * the middleware silently never ran.
8 *
9 * Two of these (admin-security.tsx's /admin/security and /admin/soc2) were
10 * a live, unauthenticated information-disclosure bug in production --
11 * confirmed via `curl` returning a real 200 with the SOC 2 evidence
12 * dashboard rendered, no login required. The rest crashed with a raw 500
13 * (`c.get("user")!` dereferencing null) instead of leaking data, but were
14 * equally broken as an auth gate.
15 *
16 * This file covers the requireAuth-gated ones not already covered by a
17 * dedicated test file (admin-security.test.ts covers /admin/security +
18 * /admin/soc2; ai-standup.test.ts covers /standups; admin.test.ts covers
19 * /admin/autopilot/health, a related-but-distinct routing bug).
20 */
21import { describe, it, expect } from "bun:test";
22
23async function expectLoginRedirect(path: string, init?: RequestInit) {
24 const app = (await import("../app")).default;
25 const res = await app.request(path, init);
26 expect(res.status).toBe(302);
27 expect(res.headers.get("location") || "").toContain("/login");
28}
29
30describe("wildcard auth-gate regression — requireAuth-protected settings pages", () => {
31 it("GET /settings/tokens requires auth", async () => {
32 await expectLoginRedirect("/settings/tokens");
33 });
34
35 it("GET /api/user/tokens requires auth", async () => {
36 await expectLoginRedirect("/api/user/tokens");
37 });
38
39 it("GET /settings/sessions requires auth", async () => {
40 await expectLoginRedirect("/settings/sessions");
41 });
42
43 it("GET /settings/integrations requires auth", async () => {
44 await expectLoginRedirect("/settings/integrations");
45 });
46
47 it("GET /settings/agents requires auth", async () => {
48 await expectLoginRedirect("/settings/agents");
49 });
50
51 it("GET /settings/deploy-targets requires auth", async () => {
52 await expectLoginRedirect("/settings/deploy-targets");
53 });
54
55 it("GET /orgs/:slug/settings/secrets requires auth", async () => {
56 await expectLoginRedirect("/orgs/test/settings/secrets");
57 });
58});
Modifiedsrc/routes/cross-repo-search.tsx+8−2View fileUnifiedSplit
3535// ---------------------------------------------------------------------------
3636
3737const crossRepoSearch = new Hono<AuthEnv>();
38crossRepoSearch.use("/search/code*", softAuth);
39crossRepoSearch.use("/api/search/code*", softAuth);
38// "path*" (no slash before the *) doesn't match the bare path in this Hono
39// version -- see admin-security.tsx's fix for the confirmed repro. Low
40// practical impact here (app.tsx's global `app.use("*", softAuth)` already
41// populates the user context for every request), but fixing for correctness.
42crossRepoSearch.use("/search/code", softAuth);
43crossRepoSearch.use("/search/code/*", softAuth);
44crossRepoSearch.use("/api/search/code", softAuth);
45crossRepoSearch.use("/api/search/code/*", softAuth);
4046
4147// ---------------------------------------------------------------------------
4248// Types
Modifiedsrc/routes/debt-map.tsx+4−1View fileUnifiedSplit
3838
3939const debtMapRoutes = new Hono<AuthEnv>();
4040
41debtMapRoutes.use("/:owner/:repo/debt-map*", softAuth);
41// "path*" (no slash before the *) doesn't match the bare path in this Hono
42// version -- see admin-security.tsx's fix for the confirmed repro.
43debtMapRoutes.use("/:owner/:repo/debt-map", softAuth);
44debtMapRoutes.use("/:owner/:repo/debt-map/*", softAuth);
4245
4346// ─── CSS ──────────────────────────────────────────────────────────────────────
4447
Modifiedsrc/routes/deploy-targets.tsx+4−1View fileUnifiedSplit
2929
3030const deployTargets = new Hono<AuthEnv>();
3131
32deployTargets.use("/settings/deploy-targets*", requireAuth);
32// SECURITY: "path*" (no slash before the *) doesn't match the bare path in
33// this Hono version -- see admin-security.tsx's fix for the confirmed repro.
34deployTargets.use("/settings/deploy-targets", requireAuth);
35deployTargets.use("/settings/deploy-targets/*", requireAuth);
3336
3437// ─── Scoped styles ────────────────────────────────────────────────────────────
3538
Modifiedsrc/routes/org-secrets.tsx+2−1View fileUnifiedSplit
2626
2727// ── Auth guard ────────────────────────────────────────────────────────────────
2828
29orgSecretsRoutes.use("/orgs/:slug/settings/secrets*", softAuth, requireAuth);
29orgSecretsRoutes.use("/orgs/:slug/settings/secrets", softAuth, requireAuth);
30orgSecretsRoutes.use("/orgs/:slug/settings/secrets/*", softAuth, requireAuth);
3031
3132// ── Scoped CSS (.osec-*) ──────────────────────────────────────────────────────
3233
Modifiedsrc/routes/pulse.tsx+4−1View fileUnifiedSplit
3939const pulseRoutes = new Hono<AuthEnv>();
4040
4141// Path-scoped middleware — NEVER use("*", ...)
42pulseRoutes.use("/:owner/:repo/pulse*", softAuth);
42// "path*" (no slash before the *) doesn't match the bare path in this Hono
43// version -- see admin-security.tsx's fix for the confirmed repro.
44pulseRoutes.use("/:owner/:repo/pulse", softAuth);
45pulseRoutes.use("/:owner/:repo/pulse/*", softAuth);
4346
4447// ─── helpers ─────────────────────────────────────────────────────────────────
4548
Modifiedsrc/routes/push-notifications.tsx+4−1View fileUnifiedSplit
3838// Middleware — scoped; never "*"
3939// ---------------------------------------------------------------------------
4040
41pushNotifRoutes.use("/settings/notifications/push*", softAuth);
41// "path*" (no slash before the *) doesn't match the bare path in this Hono
42// version -- see admin-security.tsx's fix for the confirmed repro.
43pushNotifRoutes.use("/settings/notifications/push", softAuth);
44pushNotifRoutes.use("/settings/notifications/push/*", softAuth);
4245pushNotifRoutes.use("/api/push/*", requireAuth);
4346
4447// ---------------------------------------------------------------------------
Modifiedsrc/routes/settings-agents.tsx+4−1View fileUnifiedSplit
2222
2323const settingsAgents = new Hono<AuthEnv>();
2424
25settingsAgents.use("/settings/agents*", requireAuth);
25// SECURITY: "path*" (no slash before the *) doesn't match the bare path in
26// this Hono version -- see admin-security.tsx's fix for the confirmed repro.
27settingsAgents.use("/settings/agents", requireAuth);
28settingsAgents.use("/settings/agents/*", requireAuth);
2629
2730const styles = `
2831 .sa-wrap { max-width: 1320px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
Modifiedsrc/routes/settings-integrations.tsx+4−1View fileUnifiedSplit
2727import { notifyChatChannels } from "../lib/chat-notifier";
2828
2929const r = new Hono<AuthEnv>();
30r.use("/settings/integrations*", softAuth, requireAuth);
30// SECURITY: "path*" (no slash before the *) doesn't match the bare path in
31// this Hono version -- see admin-security.tsx's fix for the confirmed repro.
32r.use("/settings/integrations", softAuth, requireAuth);
33r.use("/settings/integrations/*", softAuth, requireAuth);
3134
3235// ---------------------------------------------------------------------------
3336// Scoped CSS (.chati-*). Mirrors the tokens / webhooks polish.
Modifiedsrc/routes/settings-sessions.tsx+4−1View fileUnifiedSplit
1717import { SettingsSubnav } from "./settings";
1818
1919const settingsSessions = new Hono<AuthEnv>();
20settingsSessions.use("/settings/sessions*", requireAuth);
20// SECURITY: "path*" (no slash before the *) doesn't match the bare path in
21// this Hono version -- see admin-security.tsx's fix for the confirmed repro.
22settingsSessions.use("/settings/sessions", requireAuth);
23settingsSessions.use("/settings/sessions/*", requireAuth);
2124
2225// ── Helper: parse a friendly device/browser hint from a user-agent string ──
2326function parseBrowserHint(ua: string | null | undefined): string {
Modifiedsrc/routes/stale-branches.tsx+4−1View fileUnifiedSplit
4545
4646// Path-scoped middleware (must NOT use `use("*", ...)` — see CLAUDE.md rule).
4747// softAuth for the GET (public repos visible to all), requireAuth for the POST.
48staleBranchRoutes.use("/:owner/:repo/branches/stale*", softAuth);
48// "path*" (no slash before the *) doesn't match the bare path in this Hono
49// version -- see admin-security.tsx's fix for the confirmed repro.
50staleBranchRoutes.use("/:owner/:repo/branches/stale", softAuth);
51staleBranchRoutes.use("/:owner/:repo/branches/stale/*", softAuth);
4952
5053// ---------------------------------------------------------------------------
5154// Helpers
Modifiedsrc/routes/tokens.tsx+7−2View fileUnifiedSplit
1212
1313const tokens = new Hono<AuthEnv>();
1414
15tokens.use("/settings/tokens*", softAuth, requireAuth);
16tokens.use("/api/user/tokens*", softAuth, requireAuth);
15// SECURITY: "path*" (no slash before the *) doesn't match the bare path in
16// this Hono version -- see the admin-security.tsx fix for the confirmed
17// repro. Registering on the exact path AND "/path/*" is the verified fix.
18tokens.use("/settings/tokens", softAuth, requireAuth);
19tokens.use("/settings/tokens/*", softAuth, requireAuth);
20tokens.use("/api/user/tokens", softAuth, requireAuth);
21tokens.use("/api/user/tokens/*", softAuth, requireAuth);
1722
1823function generateToken(): string {
1924 const bytes = crypto.getRandomValues(new Uint8Array(32));
2025