Skip to content

Phase 1: security and ops hardening (P-01–P-10) - #25

Merged
KafKafrnZ merged 11 commits into
mainfrom
gemini/p-01-email-env-names
Aug 25, 2026
Merged

KafKafrnZ merged 11 commits into
mainfrom
gemini/p-01-email-env-names

Conversation

@KafKafrnZ

Copy link
Copy Markdown
Owner

Summary

Phase 1 hardening for Ascend AI. Makes the stack safe to create accounts and run CI. It is not yet a paid product.

Remaining work to ship and charge: PRODUCTION_READINESS.md.

What landed

ID Change
P-01 EMAIL_* canonical; SMTP_* deprecated fallback
P-02 Shared backend/scripts/start.sh: migrate → ingest → uvicorn
P-03 Server-only PREVIEW_AUTH; production + flag throws
P-04 CSRF fail-closed in production; exact origin match
P-05 Refresh tokens looked up by SHA-256; unique partial index
P-06 /health reports LLM reachability without spending tokens
P-07 Email verification defaults on in production
P-08 One CI workflow; require check Gate
P-09 BFF 502 BACKEND_UNREACHABLE; set-cookie still forwarded
P-10 MEMORY.md / FIXES.md synced

After merge

  1. Branch protection on main: require GitHub check Gate (workflow CI).
  2. Redeploy Railway + Vercel using DEPLOY.md.
  3. Do not start Razorpay / quotas until this is on main and Gate is green.

Test plan

  • CI Gate green on this PR
  • Backend pytest and frontend vitest already passing locally on this branch

Align DEPLOY.md and .env.example on EMAIL_*. Keep deprecated SMTP_*
aliases so existing Railway env still works, warn once, and cover
precedence in tests.
Railway startCommand was overriding the Dockerfile and skipping
data.ingest, so production RAG stayed empty. One script now runs
migrate → ingest → uvicorn for Docker and Railway; ingest failure
aborts the deploy.
Replace NEXT_PUBLIC_PREVIEW_AUTH with PREVIEW_AUTH. Production plus
the flag throws at boot. Localhost/127.0.0.1 still work in development
via shared isPreviewAuthAllowed helper.
Missing Origin/Referer no longer bypasses CSRF in production. Origins
are compared as scheme+netloc only, so suffix/prefix lookalikes cannot
pass. Development still allows TestClient requests with no Origin.
POST /auth/refresh used to load every live refresh row and hash-compare
in Python. Lookup is now equality on the stored SHA-256, with a unique
partial index so NULLs (logged-out rows) stay allowed.
GET /health now includes llm=connected|unconfigured|unavailable via a
5s GET to LLM_BASE_URL. Provider 401 counts as reachable. LLM outage
does not 503 the process, so Railway/Docker probes stay up.
Unset REQUIRE_EMAIL_VERIFICATION now means true in production and false
in development. Explicit false/0/off still disables it. Missing SMTP in
prod logs an error at boot but does not crash.
Replace path-filtered Backend CI / Frontend CI and the broken
workflow_run deploy-gate with .github/workflows/ci.yml. Gate needs both
jobs and is the status check to require on main.
Stop rewriting transport failures as FastAPI-style {detail}. Upstream
4xx/5xx still pass through. set-cookie stays off the hop-by-hop list.
Record the unmerged hardening branch as current state. Strike
deploy-gate.yml, SMTP_* as required, and NEXT_PUBLIC_PREVIEW_AUTH.
Spell out what Phase 1 closed versus what still blocks taking money
or calling the tutor a finished product.
@KafKafrnZ
KafKafrnZ merged commit 5876bbc into main Aug 25, 2026
0 of 6 checks passed
KafKafrnZ added a commit that referenced this pull request Aug 25, 2026
Backend (mypy):
- config.py: JWT_SECRET/DATABASE_URL os.getenv() results weren't
  narrowed to str for the class-level type annotation.
- database.py: switch declarative_base() to SQLAlchemy 2.0's typed
  DeclarativeBase — the old style made every model class an "invalid
  base class" under mypy.
- main.py: import FastApiIntegration/SqlalchemyIntegration directly
  instead of dotted attribute access on sentry_sdk.integrations.*.
  This wasn't just a type error — sentry_sdk doesn't auto-import
  integration submodules, so sentry_sdk.init() would have raised
  AttributeError at runtime the moment SENTRY_DSN was ever set.
- analytics.py: explicitly annotate weak_areas_serialized as
  str | dict[str, float] so both branches type-check (matches
  StatsResponse's existing schema).

Frontend (eslint): replace `as any` with a narrow structural cast in
api-proxy.test.ts.

Verified locally: mypy backend/app/ --ignore-missing-imports clean,
npm run lint / tsc --noEmit / npm test (46/46) / npm run build all
pass, and app.main imports + FastAPI app builds with SENTRY_DSN set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvQrDCnKHKLdwaaT2Qi2wT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant