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

feat: A1 write-gate + app.tsx mounts + DEPLOY_CHECKLIST + legal-audit refresh

feat: A1 write-gate + app.tsx mounts + DEPLOY_CHECKLIST + legal-audit refresh

Permissions hardening (A1 batch):
- src/routes/editor.tsx  — write on create/edit form + POST
- src/routes/issues.tsx  — read on list/view, write on new/comment/close/reopen
- src/routes/pulls.tsx   — read on list/view, write on create/comment/ready/
                           draft/close, write+comment on merge (admin noted
                           as a v2 tightening)

App.tsx mounts:
- /:owner/:repo/settings/collaborators/teams — team-based collab invites
- /invites/:token                              — accept invite flow
- /live-events/:topic                          — SSE topic subscriptions

Launch polish:
- DEPLOY_CHECKLIST.md   — four-section scannable pre/during/post-deploy list
- docs/legal-audit.md   — strike-through for shipped legal docs + DPA entry

Tests: 183 pass / 58 fail (up from 171 pass baseline; all failures are the
known sandbox hono/jsx/jsx-dev-runtime install gap, zero real regressions).

https://claude.ai/code/session_017Do52tMX1P9jPTWXhEohXH
Claude committed on April 21, 2026Parent: febd4f0
7 files changed+1251304f6b7fec4b15ad5f1e3494699012a99a9f61491
7 changed files+125−13
AddedDEPLOY_CHECKLIST.md+45−0View fileUnifiedSplit
1# Deploy checklist
2
3Scannable go-live runbook. For detailed rationale + per-variable docs, see `DEPLOY.md`.
4
5## Pre-deploy
6
7- [ ] `DATABASE_URL` set to a live Neon PostgreSQL connection string (hard-required)
8- [ ] `GIT_REPOS_PATH` points at a persistent volume (Fly: `/app/repos` via `gluecron_repos`)
9- [ ] Secrets reviewed in `.env.example`; optional keys (`ANTHROPIC_API_KEY`, `RESEND_API_KEY`, `GATETEST_API_KEY`, `VOYAGE_API_KEY`) set or consciously skipped
10- [ ] `ERROR_WEBHOOK_URL` **or** `SENTRY_DSN` set as a Fly secret (without this, errors log to stderr only)
11- [ ] `AUTOPILOT_DISABLED` decision made (default: enabled)
12- [ ] `DEMO_SEED_ON_BOOT` decision made (default: off)
13- [ ] Run `bun run preflight` locally — green before shipping
14- [ ] `bun test` clean on the deploy commit
15- [ ] `CHANGELOG.md` has an `[Unreleased]` entry for this deploy
16
17## Deploy
18
19- [ ] `flyctl deploy` from the repo root
20- [ ] Release command runs `bun run db:migrate` automatically (configured in `fly.toml`); confirm it succeeded in the release logs
21- [ ] Fly machine reaches healthy state; no boot-loop
22- [ ] Persistent volume mounted and writable at the configured `GIT_REPOS_PATH`
23
24## Post-deploy smoke
25
26- [ ] `GET /healthz` → 200
27- [ ] `GET /readyz` → 200 (DB + git reachable)
28- [ ] `GET /status` renders; `GET /status.svg` returns a shields badge
29- [ ] Register a new user via `/register`
30- [ ] First user auto-promoted to site admin (check `/admin`)
31- [ ] Create a repo via `/new`
32- [ ] `git clone` over HTTPS succeeds
33- [ ] `git push` succeeds; post-receive pipeline runs (GateTest callback + webhooks)
34- [ ] AI review path exercised if `ANTHROPIC_API_KEY` is set
35- [ ] Sentry/webhook sink receives a forced test error
36
37## First-day operations
38
39- [ ] Admin bootstrap: oldest row in `users` is the intended admin — register that account **first** (see `src/routes/admin.ts` bootstrap rule)
40- [ ] Site banner / motd configured in `/admin` if needed for launch
41- [ ] Billing plans seeded (free/pro/team/enterprise) — verify in `/admin`
42- [ ] Autopilot ticker heartbeat visible (unless `AUTOPILOT_DISABLED=1`)
43- [ ] `docs/LAUNCH_ANNOUNCEMENT.md` queued for Show HN / social
44- [ ] Add a dated entry to `CHANGELOG.md` and tag a release
45- [ ] Monitor `/metrics`, `/healthz`, and the error sink for the first hour
Modifieddocs/legal-audit.md+29−0View fileUnifiedSplit
44**Branch:** `claude/setup-multi-repo-dev-BCwNQ`
55**Scope:** Inventory of user-facing legal pages on gluecron.com ahead of attorney review.
66
7## Update — 2026-04-21
8
9Since the original 2026-04-16 audit, the following legal documents have
10landed in-tree under `legal/`:
11
12- ~~Terms of Service~~ — shipped at `legal/TERMS.md`
13- ~~Privacy Policy~~ — shipped at `legal/PRIVACY.md`
14- ~~Acceptable Use Policy~~ — shipped at `legal/AUP.md`
15- ~~Data Processing Agreement template~~ — shipped at `legal/DPA.md`
16 (enterprise/SSO customers, GDPR Art. 28 structure)
17
18Items below that were framed as missing pre-launch requirements are
19addressed by these drafts, subject to counsel review. The umbrella-vs-
20standalone posture question (Scenario A vs B) is still open and must be
21resolved by the attorney before public launch.
22
23### Outstanding
24
25- Attorney review and sign-off on `legal/TERMS.md`, `legal/PRIVACY.md`,
26 `legal/AUP.md`, and the new `legal/DPA.md` template.
27- Footer wiring: `src/views/layout.tsx` still needs links to the legal
28 documents so users have notice at every page.
29- Scenario A vs B decision (umbrella under Crontech or standalone).
30- DMCA designated agent — the placeholder `[DMCA AGENT EMAIL]` in
31 `legal/TERMS.md` must be filled and registered with the U.S. Copyright
32 Office for 17 U.S.C. § 512 safe harbour.
33- Sub-processor list in `legal/DPA.md` should be reconciled with any
34 additional vendors counsel identifies.
35
736## Summary
837
938**No user-facing legal pages exist** in the Gluecron codebase as of this audit.
Modifiedsrc/app.tsx+12−0View fileUnifiedSplit
1616import issueRoutes from "./routes/issues";
1717import repoSettings from "./routes/repo-settings";
1818import collaboratorRoutes from "./routes/collaborators";
19import teamCollaboratorRoutes from "./routes/team-collaborators";
20import invitesRoutes from "./routes/invites";
21import liveEventsRoutes from "./routes/live-events";
1922import compareRoutes from "./routes/compare";
2023import pullRoutes from "./routes/pulls";
2124import editorRoutes from "./routes/editor";
189192// Repo collaborators (add/list/remove)
190193app.route("/", collaboratorRoutes);
191194
195// Team-based repo collaborators (invite a whole team)
196app.route("/", teamCollaboratorRoutes);
197
198// Collaborator invite accept flow (token-based)
199app.route("/", invitesRoutes);
200
201// Real-time SSE endpoint (topic-based live updates)
202app.route("/", liveEventsRoutes);
203
192204// Webhooks management
193205app.route("/", webhookRoutes);
194206
Modifiedsrc/lib/sse-client.ts+11−5View fileUnifiedSplit
1212 * - no-ops gracefully if EventSource is not supported
1313 */
1414
15// U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) are valid
16// whitespace in HTML but not in JS string literals. Referenced via
17// String.fromCharCode so we never embed the raw codepoints in source.
18const LS = String.fromCharCode(0x2028);
19const PS = String.fromCharCode(0x2029);
20
1521/**
1622 * JSON-encode a value for safe inlining inside an HTML <script> block.
1723 *
2228 */
2329function safeJsonForScript(v: unknown): string {
2430 return JSON.stringify(v)
25 .replace(/</g, "\\u003C")
26 .replace(/>/g, "\\u003E")
27 .replace(/&/g, "\\u0026")
28 .replace(/
/g, "\\u2028")
29 .replace(/
/g, "\\u2029");
31 .split("<").join("\\u003C")
32 .split(">").join("\\u003E")
33 .split("&").join("\\u0026")
34 .split(LS).join("\\u2028")
35 .split(PS).join("\\u2029");
3036}
3137
3238export function liveSubscribeScript(args: {
Modifiedsrc/routes/editor.tsx+5−4View fileUnifiedSplit
2525} from "../git/repository";
2626import { softAuth, requireAuth } from "../middleware/auth";
2727import type { AuthEnv } from "../middleware/auth";
28import { requireRepoAccess } from "../middleware/repo-access";
2829import { join } from "path";
2930
3031const editor = new Hono<AuthEnv>();
3233editor.use("*", softAuth);
3334
3435// New file form
35editor.get("/:owner/:repo/new/:ref{.+$}", requireAuth, async (c) => {
36editor.get("/:owner/:repo/new/:ref{.+$}", requireAuth, requireRepoAccess("write"), async (c) => {
3637 const { owner, repo } = c.req.param();
3738 const user = c.get("user")!;
3839 const refAndPath = c.req.param("ref");
9293});
9394
9495// Create file via commit
95editor.post("/:owner/:repo/new/:ref", requireAuth, async (c) => {
96editor.post("/:owner/:repo/new/:ref", requireAuth, requireRepoAccess("write"), async (c) => {
9697 const { owner, repo } = c.req.param();
9798 const user = c.get("user")!;
9899 const ref = c.req.param("ref");
189190});
190191
191192// Edit file form
192editor.get("/:owner/:repo/edit/:ref{.+$}", requireAuth, async (c) => {
193editor.get("/:owner/:repo/edit/:ref{.+$}", requireAuth, requireRepoAccess("write"), async (c) => {
193194 const { owner, repo } = c.req.param();
194195 const user = c.get("user")!;
195196 const refAndPath = c.req.param("ref");
248249});
249250
250251// Save edited file
251editor.post("/:owner/:repo/edit/:ref{.+$}", requireAuth, async (c) => {
252editor.post("/:owner/:repo/edit/:ref{.+$}", requireAuth, requireRepoAccess("write"), async (c) => {
252253 const { owner, repo } = c.req.param();
253254 const user = c.get("user")!;
254255 const refAndPath = c.req.param("ref");
Modifiedsrc/routes/issues.tsx+8−2View fileUnifiedSplit
2121import { renderMarkdown } from "../lib/markdown";
2222import { softAuth, requireAuth } from "../middleware/auth";
2323import type { AuthEnv } from "../middleware/auth";
24import { requireRepoAccess } from "../middleware/repo-access";
2425import {
2526 Flex,
2627 Container,
6768}
6869
6970// Issue list
70issueRoutes.get("/:owner/:repo/issues", softAuth, async (c) => {
71issueRoutes.get("/:owner/:repo/issues", softAuth, requireRepoAccess("read"), async (c) => {
7172 const { owner: ownerName, repo: repoName } = c.req.param();
7273 const user = c.get("user");
7374 const state = c.req.query("state") || "open";
180181 "/:owner/:repo/issues/new",
181182 softAuth,
182183 requireAuth,
184 requireRepoAccess("write"),
183185 async (c) => {
184186 const { owner: ownerName, repo: repoName } = c.req.param();
185187 const user = c.get("user")!;
228230 "/:owner/:repo/issues/new",
229231 softAuth,
230232 requireAuth,
233 requireRepoAccess("write"),
231234 async (c) => {
232235 const { owner: ownerName, repo: repoName } = c.req.param();
233236 const user = c.get("user")!;
267270);
268271
269272// View single issue
270issueRoutes.get("/:owner/:repo/issues/:number", softAuth, async (c) => {
273issueRoutes.get("/:owner/:repo/issues/:number", softAuth, requireRepoAccess("read"), async (c) => {
271274 const { owner: ownerName, repo: repoName } = c.req.param();
272275 const issueNum = parseInt(c.req.param("number"), 10);
273276 const user = c.get("user");
417420 "/:owner/:repo/issues/:number/comment",
418421 softAuth,
419422 requireAuth,
423 requireRepoAccess("write"),
420424 async (c) => {
421425 const { owner: ownerName, repo: repoName } = c.req.param();
422426 const issueNum = parseInt(c.req.param("number"), 10);
463467 "/:owner/:repo/issues/:number/close",
464468 softAuth,
465469 requireAuth,
470 requireRepoAccess("write"),
466471 async (c) => {
467472 const { owner: ownerName, repo: repoName } = c.req.param();
468473 const issueNum = parseInt(c.req.param("number"), 10);
491496 "/:owner/:repo/issues/:number/reopen",
492497 softAuth,
493498 requireAuth,
499 requireRepoAccess("write"),
494500 async (c) => {
495501 const { owner: ownerName, repo: repoName } = c.req.param();
496502 const issueNum = parseInt(c.req.param("number"), 10);
Modifiedsrc/routes/pulls.tsx+15−2View fileUnifiedSplit
2121import { renderMarkdown } from "../lib/markdown";
2222import { softAuth, requireAuth } from "../middleware/auth";
2323import type { AuthEnv } from "../middleware/auth";
24import { requireRepoAccess } from "../middleware/repo-access";
2425import { isAiReviewEnabled, triggerAiReview } from "../lib/ai-review";
2526import { triggerPrTriage } from "../lib/pr-triage";
2627import { runAllGateChecks } from "../lib/gate";
104105);
105106
106107// List PRs
107pulls.get("/:owner/:repo/pulls", softAuth, async (c) => {
108pulls.get("/:owner/:repo/pulls", softAuth, requireRepoAccess("read"), async (c) => {
108109 const { owner: ownerName, repo: repoName } = c.req.param();
109110 const user = c.get("user");
110111 const state = c.req.query("state") || "open";
204205 "/:owner/:repo/pulls/new",
205206 softAuth,
206207 requireAuth,
208 requireRepoAccess("write"),
207209 async (c) => {
208210 const { owner: ownerName, repo: repoName } = c.req.param();
209211 const user = c.get("user")!;
271273 "/:owner/:repo/pulls/new",
272274 softAuth,
273275 requireAuth,
276 requireRepoAccess("write"),
274277 async (c) => {
275278 const { owner: ownerName, repo: repoName } = c.req.param();
276279 const user = c.get("user")!;
335338);
336339
337340// View single PR
338pulls.get("/:owner/:repo/pulls/:number", softAuth, async (c) => {
341pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), async (c) => {
339342 const { owner: ownerName, repo: repoName } = c.req.param();
340343 const prNum = parseInt(c.req.param("number"), 10);
341344 const user = c.get("user");
607610 "/:owner/:repo/pulls/:number/comment",
608611 softAuth,
609612 requireAuth,
613 requireRepoAccess("write"),
610614 async (c) => {
611615 const { owner: ownerName, repo: repoName } = c.req.param();
612616 const prNum = parseInt(c.req.param("number"), 10);
645649);
646650
647651// Merge PR — with green gate enforcement and auto conflict resolution
652// NOTE: Merging is a high-impact action that arguably warrants "admin" access,
653// but we keep it at "write" for v1 so trusted collaborators can ship.
654// Revisit when we introduce a distinct "maintain" / "admin" collaborator role
655// surface. Branch-protection rules (evaluated below) are the current mechanism
656// for locking down merges further on specific branches.
648657pulls.post(
649658 "/:owner/:repo/pulls/:number/merge",
650659 softAuth,
651660 requireAuth,
661 requireRepoAccess("write"),
652662 async (c) => {
653663 const { owner: ownerName, repo: repoName } = c.req.param();
654664 const prNum = parseInt(c.req.param("number"), 10);
864874 "/:owner/:repo/pulls/:number/ready",
865875 softAuth,
866876 requireAuth,
877 requireRepoAccess("write"),
867878 async (c) => {
868879 const { owner: ownerName, repo: repoName } = c.req.param();
869880 const prNum = parseInt(c.req.param("number"), 10);
917928 "/:owner/:repo/pulls/:number/draft",
918929 softAuth,
919930 requireAuth,
931 requireRepoAccess("write"),
920932 async (c) => {
921933 const { owner: ownerName, repo: repoName } = c.req.param();
922934 const prNum = parseInt(c.req.param("number"), 10);
957969 "/:owner/:repo/pulls/:number/close",
958970 softAuth,
959971 requireAuth,
972 requireRepoAccess("write"),
960973 async (c) => {
961974 const { owner: ownerName, repo: repoName } = c.req.param();
962975 const prNum = parseInt(c.req.param("number"), 10);
963976