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

feat(BLOCK-J): J5 profile READMEs

feat(BLOCK-J): J5 profile READMEs

User profile page at /:owner now renders the contents of the user's
<username>/<username>/README.md (GitHub convention) above the repo list.
Falls back to <username>/.github/README.md if the self-named repo is
absent. Uses the existing getReadme + renderMarkdown + repoExists
helpers — no schema or new dependencies. Failures are silent; missing
repo just hides the panel.

2 route smoke tests. 712 tests total.

https://claude.ai/code/session_01U6sj9ezKCpmiwx7cXGZmS6
Claude committed on April 15, 2026Parent: 7aa8b99
3 files changed+530d41258683b612157a1a890bd9c81abe39d8648a4
3 changed files+53−0
ModifiedBUILD_BIBLE.md+1−0View fileUnifiedSplit
286286- **J2** — Security advisories (Dependabot-style) → ✅ shipped. `drizzle/0029_security_advisories.sql` adds `security_advisories` (GHSA + CVE IDs, severity, affected range, fixed version) + `repo_advisory_alerts` (per-repo match state with open/dismissed/fixed, unique on `(repo, advisory, manifest_path)`). `src/lib/advisories.ts` ships a 12-entry seed list (log4shell, lodash, minimist, vm2, urllib3, jwt-go, etc.), a minimal version-range matcher (`satisfiesRange` + `rangeMatches`) that handles `<`/`<=`/`>`/`>=`/`=` clauses including compound ranges, and `scanRepositoryForAlerts(repoId)` which cross-references J1 dep rows against the advisory list — inserts new alerts, reopens fixed-then-regressed ones, auto-closes alerts whose dep went away. `src/routes/advisories.tsx` serves `/:owner/:repo/security/advisories` (open), `/all` (everything), owner-only `POST /scan`, and per-alert `POST /:id/dismiss` + `POST /:id/reopen` with audit-log entries. 27 new tests (version parser, range matcher, seed shape, route auth).
287287- **J3** — Commit signature verification (GPG + SSH "Verified" badge) → ✅ shipped. `drizzle/0030_signing_keys.sql` adds `signing_keys` (per-user GPG/SSH pubkeys, unique on `(key_type, fingerprint)`) + `commit_verifications` (memoised per-commit result, unique on `(repo, sha)`). `src/lib/signatures.ts` extracts `gpgsig` / `gpgsig-sha256` from raw commit objects (`getRawCommitObject` added to `src/git/repository.ts`), unarmors PGP + SSH signature blobs, walks OpenPGP packet streams for Issuer Fingerprint (subpacket 33) / Issuer Key ID (subpacket 16), parses the SSHSIG inner publickey field, and SHA-256 fingerprints SSH wire-format keys. Identity matching via fingerprint → optional email check → cached. `src/routes/signing-keys.tsx` serves `GET/POST /settings/signing-keys` + `POST /settings/signing-keys/:id/delete`, audit-logged. `CommitList` + single commit view render a green "Verified" badge when cached `verified=true`. 29 new tests (extraction, unarmor, packet walker, SSH fp, end-to-end fast paths, route auth).
288288- **J4** — User following + personalised feed → ✅ shipped. `drizzle/0031_user_follows.sql` adds `user_follows` (composite PK on `(follower_id, following_id)`, CHECK constraint rejecting self-follows, reverse-lookup index on `following_id`). `src/lib/follows.ts` exposes `followUser/unfollowUser/isFollowing/listFollowers/listFollowing/followCounts` + `feedForUser(userId, limit)` which joins `activity_feed` against the follow set (bounded to 200 edges) and filters out private repos the viewer doesn't own. `src/routes/follows.tsx` serves `POST /:user/follow` + `/:user/unfollow` (auth-gated, audit-logged), public `GET /:user/followers` + `/:user/following`, and `GET /feed` (personalised timeline). Follow button + follower/following counts added to the user profile page via `src/routes/web.tsx`. Reserved-name set protects fixed paths (`login`, `settings`, `feed`, etc.). 8 new tests (verb table + route auth).
289- **J5** — Profile READMEs → ✅ shipped. User profile page at `/:owner` now attempts to render `<user>/<user>/README.md` (GitHub convention) or `<user>/.github/README.md` (org-style fallback) as the hero panel above the repo list. No schema changes — reuses `getReadme` / `renderMarkdown` + `repoExists` from the git layer. Failures are silent; missing repo just hides the panel. 2 smoke tests.
289290
290291### BLOCK H — Marketplace
291292- **H1** — App marketplace → ✅ shipped. `src/routes/marketplace.tsx` + `src/lib/marketplace.ts` + `drizzle/0021_marketplace_and_apps.sql` (5 tables: `apps`, `app_installations`, `app_bots`, `app_install_tokens`, `app_events`). Routes: `GET /marketplace` (public directory with search), `GET /marketplace/:slug` (detail + install CTA), `POST /marketplace/:slug/install` (user-target install in v1), `POST /marketplace/installations/:id/uninstall`, `GET /settings/apps` (personal list), `GET+POST /developer/apps-new` (register), `GET /developer/apps/:slug/manage` (event log + install count), `POST /developer/apps/:slug/tokens/new` (show-once token). Install idempotent via soft-update on existing non-uninstalled row.
Addedsrc/__tests__/profile-readme.test.ts+25−0View fileUnifiedSplit
1/**
2 * Block J5 — Profile README smoke.
3 *
4 * We can't test the full "user has a user/user repo with a README" path
5 * without a real git checkout, but we can verify the profile route still
6 * responds on the happy + missing paths.
7 */
8
9import { describe, it, expect } from "bun:test";
10import app from "../app";
11
12describe("profile README — route smoke", () => {
13 it("GET /<unknown-user> renders without blowing up on missing profile repo", async () => {
14 // Non-existent user → 200 page (renders with empty ownerUser) or 500 when
15 // DB is unreachable. Either way, the profile-readme block must not crash.
16 const res = await app.request("/does-not-exist-xyz");
17 expect([200, 404, 500]).toContain(res.status);
18 });
19
20 it("GET /login stays a fixed route, not captured by /:owner", async () => {
21 const res = await app.request("/login");
22 // login page renders 200
23 expect([200, 302]).toContain(res.status);
24 });
25});
Modifiedsrc/routes/web.tsx+27−0View fileUnifiedSplit
252252 }
253253 const canFollow = !!user && !!ownerUser && user.id !== ownerUser.id;
254254
255 // Block J5 — profile README. Render owner/owner repo's README on the
256 // profile page (GitHub convention). Tries "<user>/<user>" first, falling
257 // back to "<user>/.github" for org-style profile repos.
258 let profileReadmeHtml: string | null = null;
259 try {
260 const candidates = [ownerName, ".github"];
261 for (const rname of candidates) {
262 if (await repoExists(ownerName, rname)) {
263 const ref = (await getDefaultBranch(ownerName, rname)) || "main";
264 const md = await getReadme(ownerName, rname, ref);
265 if (md) {
266 profileReadmeHtml = renderMarkdown(md);
267 break;
268 }
269 }
270 }
271 } catch {
272 profileReadmeHtml = null;
273 }
274
255275 return c.html(
256276 <Layout title={ownerName} user={user}>
257277 <div class="user-profile">
303323 </div>
304324 </div>
305325 </div>
326 {profileReadmeHtml && (
327 <div
328 class="markdown-body"
329 style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:20px 24px;margin-bottom:24px"
330 dangerouslySetInnerHTML={{ __html: profileReadmeHtml }}
331 />
332 )}
306333 <h3 style="margin-bottom: 16px">Repositories</h3>
307334 {repos.length === 0 ? (
308335 <p style="color: var(--text-muted)">No repositories yet.</p>
309336