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

feat(audit): log pre-receive policy rejections

feat(audit): log pre-receive policy rejections

When the push-policy gate (src/lib/push-policy.ts) rejects a push
the request never reaches the post-receive hook — meaning until now
those rejections were invisible in the audit log. Owners couldn't
see "Bob tried to push v1.0 last Tuesday and got blocked."

This commit adds a single fire-and-forget audit() call at the
rejection point in src/routes/git.ts:
  action:        "push.rejected"
  userId:        resolved pusher (null when anonymous)
  repositoryId:  the repo
  ip / userAgent: from request headers
  metadata: {
    violations:    [...the full violation strings...],
    refs:          [refs/heads/foo, refs/tags/v1.0, ...],
    pusherSource:  "pat" | "oauth" | "anonymous",
  }

Wrapped in .catch(() => {}) so an audit-table hiccup can't promote
to a 500. The 403 response and policy semantics are unchanged.

The per-repo audit log UI (/:owner/:repo/settings/audit) renders
audit_log entries by `action` so this row appears alongside the
existing push.success / merge / branch_protection.update entries.

Suite: 1146 pass / 8 skip / 0 fail (no new tests — exercises only
DB-coupled paths; existing route auth tests cover the smoke).

https://claude.ai/code/session_0163vJChUuZqqtBBznUW6xBU
Claude committed on April 30, 2026Parent: b584e52
1 file changed+210c3402481a285acef99f82487f6a26000c4eadfe9
1 changed file+21−0
Modifiedsrc/routes/git.ts+21−0View fileUnifiedSplit
1818 formatPolicyError,
1919} from "../lib/push-policy";
2020import { resolvePusher } from "../lib/git-push-auth";
21import { audit } from "../lib/notify";
2122
2223const git = new Hono();
2324
9899 pusherUserId: pusher?.userId || null,
99100 });
100101 if (!decision.allowed) {
102 // Audit the rejection so owners can see blocked-push attempts
103 // even though the request never reached the post-receive hook.
104 // Fire-and-forget — never block the 403.
105 audit({
106 userId: pusher?.userId || null,
107 repositoryId: repoRow.id,
108 action: "push.rejected",
109 targetType: "repository",
110 targetId: repoRow.id,
111 ip:
112 c.req.header("x-forwarded-for") ||
113 c.req.header("x-real-ip") ||
114 undefined,
115 userAgent: c.req.header("user-agent") || undefined,
116 metadata: {
117 violations: decision.violations,
118 refs: refs.map((r) => r.refName),
119 pusherSource: pusher?.source || "anonymous",
120 },
121 }).catch(() => {});
101122 // Returning 403 with a plain-text body — git smart-HTTP clients
102123 // surface the body to the user (`remote: ` prefix). Existing
103124 // behaviour for repos with no policy is unchanged.
104125