From b8358cb56f77da4b261fbe76a0e2ffcb0f7df921 Mon Sep 17 00:00:00 2001 From: Shaan Satsangi Date: Thu, 28 May 2026 14:01:53 +0530 Subject: [PATCH 1/2] =?UTF-8?q?fix(v0.9.5):=20security=20hardening=20?= =?UTF-8?q?=E2=80=94=20read-only=20OAuth=20scope=20+=20HTTP=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-launch audit fixes (no high/critical found): - OAuth scope read:user public_repo -> read:user. public_repo grants write to public repos despite its name; reading public data needs no repo scope. Shrinks a leaked stored token's blast radius. New logins only. - Add HTTP security headers in next.config.ts: X-Frame-Options, nosniff, Referrer-Policy, Permissions-Policy (enforced) + report-only CSP baseline. --- ARCHITECTURE.md | 2 +- backend/app/auth/oauth.py | 12 ++++++++++-- backend/tests/auth/test_oauth.py | 12 ++++++++++++ docs/TECH_STACK.md | 2 +- frontend/next.config.ts | 31 +++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 89bf099..39326cd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -154,7 +154,7 @@ Two thin cross-cutting layers. Both fail open — telemetry is never a correctne ### Auth — GitHub OAuth (v0.5.0) - Server-side OAuth code flow (no PKCE — GitHub OAuth Apps don't support it). State in a short-lived httpOnly cookie. -- We request `read:user` and `public_repo` only. Never `repo` (we do not need private data) and never `admin:*`. +- We request `read:user` only (tightened in v0.9.5). We exclusively read public data, which needs no repo scope; `public_repo` was dropped because — despite its name — it grants *write* access to public repos and needlessly widened a leaked token's blast radius. Never `repo`, never `admin:*`. - Token storage: **server-side opaque session** cookie (`secrets.token_urlsafe(32)`); the GitHub access token is **AES-GCM encrypted at rest** in the `sessions` row with a per-environment `SESSION_TOKEN_ENC_KEY`. - Signed-in `/analyze` uses the user's GitHub token for ingestion, giving each user a dedicated 5000/hr rate-limit budget. diff --git a/backend/app/auth/oauth.py b/backend/app/auth/oauth.py index d4d85ab..ed911b3 100644 --- a/backend/app/auth/oauth.py +++ b/backend/app/auth/oauth.py @@ -27,10 +27,18 @@ def verify_state_token(*, cookie: str | None, query: str) -> None: def build_authorize_url(state: str) -> str: - """Construct the GitHub authorize URL we 302 to.""" + """Construct the GitHub authorize URL we 302 to. + + Scope is `read:user` only. We exclusively READ public GitHub data, which + needs no repo scope at all — an authenticated token reads public repos/ + commits/contents regardless of scope, and `read:user` covers the profile. + `public_repo` was dropped in v0.9.5: despite its name it grants *write* + access to public repositories, so it needlessly widened the blast radius of + a leaked stored token. Never `repo`, never `admin:*`. + """ params = { "client_id": settings.github_oauth_client_id or "", - "scope": "read:user public_repo", + "scope": "read:user", "state": state, "redirect_uri": settings.oauth_redirect_url or "", } diff --git a/backend/tests/auth/test_oauth.py b/backend/tests/auth/test_oauth.py index ecb16a5..e84e758 100644 --- a/backend/tests/auth/test_oauth.py +++ b/backend/tests/auth/test_oauth.py @@ -1,14 +1,26 @@ import secrets +from urllib.parse import parse_qs, urlparse import pytest from app.auth.oauth import ( InvalidOAuthState, + build_authorize_url, generate_state_token, verify_state_token, ) +def test_authorize_url_requests_read_only_scope(): + """v0.9.5: scope must be `read:user` only — no write scope (`public_repo`), + never `repo`/`admin:*`. The app only reads public data.""" + qs = parse_qs(urlparse(build_authorize_url("state123")).query) + assert qs["scope"] == ["read:user"] + assert "public_repo" not in qs["scope"][0] + assert "repo" not in qs["scope"][0].split() + assert qs["state"] == ["state123"] + + def test_state_token_is_url_safe_high_entropy(): s = generate_state_token() assert len(s) >= 32 diff --git a/docs/TECH_STACK.md b/docs/TECH_STACK.md index 1b57ac0..4d7db2a 100644 --- a/docs/TECH_STACK.md +++ b/docs/TECH_STACK.md @@ -67,7 +67,7 @@ | Tool | Role | | --- | --- | -| **GitHub OAuth (App)** | Sign-in + higher API rate limits. Scopes: `read:user`, `public_repo`. Never `repo` or `admin:*`. | +| **GitHub OAuth (App)** | Sign-in + higher API rate limits. Scope: `read:user` only (v0.9.5 dropped the write-granting `public_repo`; reading public data needs no repo scope). Never `repo` or `admin:*`. | | **Server-side opaque sessions** | Cookie value is `secrets.token_urlsafe(32)`; server looks the row up directly. Chosen over JWT in v0.5.0 to keep revocation cheap and access tokens server-side. | | **AES-GCM at rest** | GitHub access tokens encrypted in the `sessions` table with `SESSION_TOKEN_ENC_KEY`. Fresh 12-byte nonce per row; key rotation invalidates every session by design. | diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 2f55369..ceae95f 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,6 +1,37 @@ import type { NextConfig } from "next"; +// v0.9.5 security headers. The first four are enforced (zero-risk). CSP ships +// Report-Only: it logs violations to the browser console without blocking, so +// it can be tuned against real reports before being promoted to enforcing — +// a wrong directive would otherwise silently break PostHog/Sentry/Next. +const CSP_REPORT_ONLY = [ + "default-src 'self'", + "base-uri 'self'", + "object-src 'none'", + "frame-ancestors 'self'", + "img-src 'self' data: https://avatars.githubusercontent.com", + "font-src 'self' data:", + "style-src 'self' 'unsafe-inline'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.posthog.com https://*.i.posthog.com", + "connect-src 'self' https://*.posthog.com https://*.i.posthog.com https://*.sentry.io https://*.ingest.sentry.io", + "form-action 'self'", +].join("; "); + +const SECURITY_HEADERS = [ + { key: "X-Frame-Options", value: "SAMEORIGIN" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=(), interest-cohort=()", + }, + { key: "Content-Security-Policy-Report-Only", value: CSP_REPORT_ONLY }, +]; + const nextConfig: NextConfig = { + async headers() { + return [{ source: "/:path*", headers: SECURITY_HEADERS }]; + }, // v0.8.6: enable Next 16 Cache Components for `/share/[slug]` ISR with // tag-based invalidation via the new `/api/revalidate` route. cacheComponents: true, From b7888e2e28e4079e95b3b332b9c1582eb1016d8a Mon Sep 17 00:00:00 2001 From: Shaan Satsangi Date: Thu, 28 May 2026 14:01:53 +0530 Subject: [PATCH 2/2] chore(v0.9.5): bump version + docs ritual (security review) --- CHANGELOG.md | 11 ++++++++ PLAN.md | 33 +++++++++++++++++++----- README.md | 4 +-- backend/app/settings.py | 2 +- backend/pyproject.toml | 2 +- backend/uv.lock | 2 +- docs/DEPLOY.md | 2 ++ docs/PROGRESS_LOG.md | 29 +++++++++++++++++++++ frontend/package.json | 2 +- frontend/src/app/page.tsx | 2 +- frontend/src/components/results-view.tsx | 2 +- 11 files changed, 77 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2af344..f06fd58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ Every version listed here must correspond to a slice in [`PLAN.md`](./PLAN.md) w --- +## [0.9.5] — 2026-05-28 + +### Security +- **Full pre-launch security review — no high or critical findings.** Authorization (ownership checks on every mutation), session encryption, OAuth CSRF protection, SQL-injection safety, output escaping, and SSRF protection on user-supplied input were all verified sound. + +### Changed +- **Tightened the GitHub sign-in permission to read-only.** Sign-in previously requested a scope that technically allowed writing to your public repositories; it now requests read-only access only, since Skill Issue exclusively reads public data. (Existing sessions are unaffected; the narrower permission applies on next sign-in.) +- **Added HTTP security headers** (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`) plus a report-only Content-Security-Policy as a baseline for hardening before public launch. + +--- + ## [0.9.4] — 2026-05-28 ### Changed diff --git a/PLAN.md b/PLAN.md index 58928a8..e36138a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -43,8 +43,9 @@ | **v0.9.2** | Rate limiting (IP + user) on `/analyze` + `/narrative` | ✅ shipped | | **v0.9.3** | Deletable `/me` history + back-nav loading fix + creator flair | ✅ shipped | | **v0.9.4** | DB pool size env-tunable + real back-nav spinner fix | ✅ shipped | -| **v0.9.5** | `/security-review` pass + load test to 100 RPS | pending | -| **v0.9.6** | Privacy policy + terms (legal docs) | pending | +| **v0.9.5** | Security review + hardening (OAuth scope ↓ `read:user`, HTTP security headers) | ✅ shipped | +| **v0.9.6** | Load test to 100 RPS | pending | +| **v0.9.7** | Privacy policy + terms (legal docs) | pending | | **v1.0.0** | Public launch | pending | --- @@ -175,7 +176,7 @@ **Design spec:** [`docs/superpowers/specs/2026-05-16-v0.5.0-auth-persistence-design.md`](./docs/superpowers/specs/2026-05-16-v0.5.0-auth-persistence-design.md). **Slice scope:** -- GitHub **OAuth App** (not GitHub App), server-side flow, scopes `read:user public_repo`. State token in a short-lived httpOnly cookie. Server-side opaque sessions in Postgres (no JWT). +- GitHub **OAuth App** (not GitHub App), server-side flow, scope `read:user` (v0.9.5 dropped `public_repo`). State token in a short-lived httpOnly cookie. Server-side opaque sessions in Postgres (no JWT). - **Neon Postgres** schema (5 tables): `users`, `sessions`, `analyses`, `analysis_runs`, `narratives`. Cascade deletes from `users` clean everything up. - **SQLAlchemy 2.0 async + asyncpg** against Neon's pooled host (port 6543, `statement_cache_size=0`). Direct host (port 5432) only for Alembic migrations. - **Alembic** migrations, hand-edited, reversibility tested. @@ -649,15 +650,35 @@ The narrative-mode CHECK constraint was a third drift in the same family — the --- -## v0.9.5 — `/security-review` pass + load test (deferred) +## v0.9.5 — Security review + hardening (shipped 2026-05-28) -**Goal:** Run `/security-review` against the codebase; resolve any high/critical findings. Load-test to 100 RPS sustained, verify error budget holds. +**Goal:** Full pre-launch security audit of the whole app; resolve any high/critical findings. (The load test was split out to v0.9.6 — it needs a deliberate target/cost/rate-limit-bypass design and is independently shippable.) + +**Audit result:** No high or critical findings. Verified sound: authorization (every mutation ownership-checked via `_owned_analysis`, no IDOR), AES-GCM session-token encryption, OAuth `state` CSRF with constant-time compare, no SQL injection (SQLAlchemy constructs only), no XSS (no `dangerouslySetInnerHTML`; LLM narrative renders as escaped text), no SSRF (username regex-validated server-side; GitHub URLs built only from validated input + trusted API responses), server-only secrets. + +**Fixes shipped (two Mediums):** +- **OAuth scope `read:user public_repo` → `read:user`.** `public_repo` is a *write* scope; reading public data needs none. Reduces a leaked stored token's blast radius. New logins only; existing sessions unaffected. +- **HTTP security headers** in `frontend/next.config.ts`: enforced `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`; plus a **report-only** Content-Security-Policy (logs violations without blocking — to be tuned against real reports before enforcing). + +**Operator follow-ups (config, no code):** verify `COOKIE_SECURE=true` in prod; confirm `CORS_ALLOW_ORIGIN_REGEX` is scoped to our own origins (not `*.vercel.app`). + +**Exit criteria:** +- [x] Whole-app security audit completed; findings severity-ranked. +- [x] All high/critical findings resolved (none found). +- [x] OAuth scope tightened + test; security headers added; `next build` clean. +- [x] Docs ritual + version bump to 0.9.5; tag + release. + +--- + +## v0.9.6 — Load test to 100 RPS (deferred) + +**Goal:** Load-test to 100 RPS sustained and verify the error budget holds. Needs a deliberate design: target (prod vs preview vs local), cost ceiling (Vercel Active-CPU pricing), and how to handle the v0.9.2 rate limits (a naive test from one IP just measures 429s — raise limits for the window, test `/health` + a warm-cached path, or use a bypass). **Exit criteria:** TBD when the slice begins. --- -## v0.9.6 — Legal docs (deferred) +## v0.9.7 — Legal docs (deferred) **Goal:** Privacy policy + terms in `docs/legal/`. Link from frontend footer. diff --git a/README.md b/README.md index e131bc1..af75a36 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Engineering insight first. AI flavor second. Scoring is deterministic and explai ## Status -Pre-alpha. Latest shipped release is **v0.9.4** (the database connection pool size is now tunable via environment variables without a redeploy, and the search spinner that could stick after pressing browser Back is genuinely fixed). v0.9.3 before it added deletable `/me` history with undo, a golden "creator" scorecard for the project's creator account, and a first (incomplete) attempt at the back-nav spinner fix. Live at https://skill-issue-tau.vercel.app — GitHub OAuth sign-in, Neon Postgres persistence, `/me` history, opt-in `/share/[slug]` public links. The AI narrative layer (Roast + Mentor) runs on **Groq** (`llama-3.3-70b-versatile`). v0.7.0 added Upstash Redis caching (warm `/analyze` ≤ 200 ms); v0.7.2 prod-certified the perf budget (CLS 0.080 → **0** structurally, perf 90 → 94, LCP 2,804 → 2,773 ms); v0.8.0 shipped Sentry (FE+BE), PostHog (events + web vitals), structlog JSON logging, on-voice 404, and a full axe a11y pass; v0.8.1 ships the nightly cron with bearer auth; v0.8.2 pairs it with the manual force-refresh button on `/me`; v0.8.3 hotfixes the empty-repo crash; v0.8.4 fixes the silent narrative misattribution; v0.8.5 closes the post-deploy-Sentry loop with a pre-merge CI gate; v0.8.6 closes v0.7.1's deferred share-page caching; v0.8.7 modernizes project config; v0.9.0 opens Beta hardening with bounded GH fan-out; v0.9.1 closes the /me N+1 + adds per-namespace Report cache versioning; v0.9.2 adds rate limiting (per-IP for anonymous, higher per-user caps for signed-in) on `/analyze` and `/narrative`; v0.9.3 adds deletable `/me` history with undo, attempts the back-nav search-spinner fix, and gilds the creator's scorecard. v0.9.4 makes the DB connection pool size env-tunable (defaults unchanged — RUM showed no pool exhaustion) and lands the real back-nav spinner fix (the v0.9.3 attempt addressed the wrong mechanism). **v0.9.5 — security review + load test** is next. See [`CHANGELOG.md`](./CHANGELOG.md) for shipped slices, [`PLAN.md`](./PLAN.md) for the full roadmap, and [`docs/PROGRESS_LOG.md`](./docs/PROGRESS_LOG.md) for the most recent session handoff. +Pre-alpha. Latest shipped release is **v0.9.5** (a full pre-launch security audit — no high/critical findings — that tightened the GitHub OAuth scope to read-only and added HTTP security headers). v0.9.4 before it made the DB connection pool size env-tunable and genuinely fixed the back-nav search spinner; v0.9.3 added deletable `/me` history with undo, a golden "creator" scorecard for the project's creator account, and a first (incomplete) attempt at the back-nav spinner fix. Live at https://skill-issue-tau.vercel.app — GitHub OAuth sign-in, Neon Postgres persistence, `/me` history, opt-in `/share/[slug]` public links. The AI narrative layer (Roast + Mentor) runs on **Groq** (`llama-3.3-70b-versatile`). v0.7.0 added Upstash Redis caching (warm `/analyze` ≤ 200 ms); v0.7.2 prod-certified the perf budget (CLS 0.080 → **0** structurally, perf 90 → 94, LCP 2,804 → 2,773 ms); v0.8.0 shipped Sentry (FE+BE), PostHog (events + web vitals), structlog JSON logging, on-voice 404, and a full axe a11y pass; v0.8.1 ships the nightly cron with bearer auth; v0.8.2 pairs it with the manual force-refresh button on `/me`; v0.8.3 hotfixes the empty-repo crash; v0.8.4 fixes the silent narrative misattribution; v0.8.5 closes the post-deploy-Sentry loop with a pre-merge CI gate; v0.8.6 closes v0.7.1's deferred share-page caching; v0.8.7 modernizes project config; v0.9.0 opens Beta hardening with bounded GH fan-out; v0.9.1 closes the /me N+1 + adds per-namespace Report cache versioning; v0.9.2 adds rate limiting (per-IP for anonymous, higher per-user caps for signed-in) on `/analyze` and `/narrative`; v0.9.3 adds deletable `/me` history with undo, attempts the back-nav search-spinner fix, and gilds the creator's scorecard. v0.9.4 makes the DB connection pool size env-tunable (defaults unchanged — RUM showed no pool exhaustion) and lands the real back-nav spinner fix (the v0.9.3 attempt addressed the wrong mechanism); v0.9.5 runs a full pre-launch security audit (no high/critical findings), tightens the OAuth scope to `read:user`, and adds HTTP security headers. **v0.9.6 — load test to 100 RPS** is next. See [`CHANGELOG.md`](./CHANGELOG.md) for shipped slices, [`PLAN.md`](./PLAN.md) for the full roadmap, and [`docs/PROGRESS_LOG.md`](./docs/PROGRESS_LOG.md) for the most recent session handoff. --- @@ -76,7 +76,7 @@ cp .env.example .env # then edit .env and add your GITHUB_TOKEN and OPENA uv run uvicorn app.main:app --reload --port 8000 ``` -Verify: `curl http://localhost:8000/health` → `{"status":"ok","version":"0.9.4","db":"up"|"down","cache":"up"|"down"|"unconfigured"}`. The `db` field reports DB reachability when `DATABASE_URL` is configured; the `cache` field reports Upstash reachability (`unconfigured` when `UPSTASH_REDIS_REST_URL` isn't set — perfectly fine for local dev, the in-process fallback covers it). +Verify: `curl http://localhost:8000/health` → `{"status":"ok","version":"0.9.5","db":"up"|"down","cache":"up"|"down"|"unconfigured"}`. The `db` field reports DB reachability when `DATABASE_URL` is configured; the `cache` field reports Upstash reachability (`unconfigured` when `UPSTASH_REDIS_REST_URL` isn't set — perfectly fine for local dev, the in-process fallback covers it). Hit the analyzer: `curl http://localhost:8000/analyze/octocat`. ### Frontend (`:3000`) diff --git a/backend/app/settings.py b/backend/app/settings.py index 6de2cf1..25b9af7 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict -VERSION = "0.9.4" +VERSION = "0.9.5" class Settings(BaseSettings): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index ec91a86..8418b8c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "skill-issue-backend" -version = "0.9.4" +version = "0.9.5" description = "Skill Issue backend — FastAPI service that ingests a GitHub profile and returns a deterministic engineering report." readme = "README.md" authors = [ diff --git a/backend/uv.lock b/backend/uv.lock index 630b123..86305b7 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -906,7 +906,7 @@ fastapi = [ [[package]] name = "skill-issue-backend" -version = "0.9.4" +version = "0.9.5" source = { virtual = "." } dependencies = [ { name = "alembic" }, diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 7da0153..d4a2d2c 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -81,6 +81,8 @@ In Vercel → **Settings** → **Environment Variables**, add (Production + Prev > **DB pool ceiling.** The Neon compute exposes ~105 usable connections (`max_connections` 112 - 7 `superuser_reserved_connections` on the current ~0.25 CU compute). The app connects through the PgBouncer pooler (`statement_cache_size=0`), which multiplexes many client connections onto few server ones — so the ceiling is heavily buffered. If ever switched to a direct connection, keep `(DB_POOL_SIZE + DB_MAX_OVERFLOW) × peak_instances < 105`. +> **Security pre-launch checklist (v0.9.5).** The v0.9.5 audit found no high/critical issues, but confirm these *config* items before public launch: (1) `COOKIE_SECURE=true` is set in prod; (2) `CORS_ALLOW_ORIGIN_REGEX` is scoped to our own origins only (never a blanket `*.vercel.app`); (3) promote the report-only Content-Security-Policy in `frontend/next.config.ts` to enforcing once it's been tuned against real violation reports. + ### 5. Run the initial Alembic migration The DB schema lives in `backend/migrations/`. Pulling the prod `DATABASE_DIRECT_URL` locally (or pasting it once into a shell session — don't persist it) is the safe way to run migrations: diff --git a/docs/PROGRESS_LOG.md b/docs/PROGRESS_LOG.md index 82af025..eb0a9a1 100644 --- a/docs/PROGRESS_LOG.md +++ b/docs/PROGRESS_LOG.md @@ -19,6 +19,35 @@ Format: --- +## 2026-05-28 — Claude (Opus 4.7) — v0.9.5 shipped (pre-launch security audit + hardening) + +**Slice:** v0.9.5. Full pre-launch security audit of the whole app + two Medium hardening fixes. The load test originally bundled here was split to v0.9.6 (needs target/cost/rate-limit design); legal docs shifted to v0.9.7. + +**Done:** +- **Whole-app security audit — no high/critical findings.** Reviewed: authz/IDOR, session crypto, OAuth CSRF, SQLi, XSS, SSRF, secrets, CORS, rate limiting, security headers. All core surfaces sound (see Decisions for specifics). +- **Fix #1 — OAuth scope `read:user public_repo` → `read:user`** (`app/auth/oauth.py`). Added `test_authorize_url_requests_read_only_scope`. Updated current-state docs (ARCHITECTURE/TECH_STACK/PLAN); left historical changelog/spec entries as-is. +- **Fix #2 — HTTP security headers** (`frontend/next.config.ts` `headers()`): enforced `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`; plus a **report-only** CSP (logs, doesn't block) as a tunable baseline. +- Docs ritual + version bump 0.9.5; DEPLOY security pre-launch checklist note. + +**Decisions:** +- **Verified-clean surfaces:** every mutation ownership-checked via `_owned_analysis` (`id AND user_id` → 403; no IDOR); AES-GCM token encryption (fail-fast 32-byte key, random nonce); OAuth `state` CSRF constant-time; no raw SQL (SQLAlchemy constructs only; `text()` only for `SELECT 1`); no `dangerouslySetInnerHTML` (LLM narrative escaped by React); username regex-validated server-side so GitHub URLs never take a user-controlled host (no SSRF); `INTERNAL_PROXY_SECRET`/`REVALIDATE_SECRET` server-only + constant-time compared. +- **`public_repo` is a write scope** — a common misconception treated it as "read public." Reading public data needs no repo scope at all; dropping it shrinks a leaked token's blast radius with zero functional loss (new logins only). +- **CSP report-only, not enforcing** — a wrong CSP silently breaks PostHog/Sentry/Next and I can't browser-test enforcement headlessly. Report-only ships the signal safely; promote after tuning. +- **Load test split out (v0.9.6)** — our own v0.9.2 rate limits would make a naive 100 RPS test just measure 429s; it needs a deliberate target + cost design, and is independently shippable. + +**Operator follow-ups (config, no code):** confirm `COOKIE_SECURE=true` in prod; ensure `CORS_ALLOW_ORIGIN_REGEX` is scoped to our origins (not `*.vercel.app`). Both noted in DEPLOY.md. + +**Verified:** +- Backend `ruff` clean; `pytest tests/auth/test_oauth.py` 6 passed (incl. new scope test). Full suite to re-confirm pre-commit. +- Frontend `lint` + `tsc` clean; `next build` clean (headers config valid). +- Security headers verifiable via `curl -I` on prod after deploy. + +**Blocked / open:** push/PR/CI/merge/tag pending (confirm-before-tag per the session norm). + +**Next:** v0.9.6 — load test to 100 RPS (design the target + rate-limit handling first). + +--- + ## 2026-05-28 — Claude (Opus 4.7) — v0.9.4 shipped (DB pool size env-tunable + real back-nav spinner fix) **Slice:** v0.9.4. Two changes: the planned DB-pool work, plus a genuine fix for the back-nav search spinner that v0.9.3 only *appeared* to fix. diff --git a/frontend/package.json b/frontend/package.json index 70887ba..c5d8efb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.9.4", + "version": "0.9.5", "private": true, "scripts": { "dev": "next dev", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f4ed922..1d47093 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -23,7 +23,7 @@ export default function Home() { transition={{ delay: 0.2, duration: 0.5 }} className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-medium uppercase tracking-wider text-muted-foreground" > - Deterministic engineering reports · v0.9.4 + Deterministic engineering reports · v0.9.5

diff --git a/frontend/src/components/results-view.tsx b/frontend/src/components/results-view.tsx index 531db0c..e491277 100644 --- a/frontend/src/components/results-view.tsx +++ b/frontend/src/components/results-view.tsx @@ -352,7 +352,7 @@ export function ResultsView({