diff --git a/.env.local.example b/.env.local.example index 67b694a6..4c74c896 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,8 +1,20 @@ # Required: The PDS / handle resolver URL NEXT_PUBLIC_PDS_URL=https://certified.one -# Required in production: The public URL of this app (used for OAuth client_id and redirect_uris) -PUBLIC_URL=http://localhost:3000 +# Public URL of this app — used for OAuth client_id, redirect_uris, and the +# CSRF Origin allowlist. +# +# Production: set to the deployed origin, e.g. https://certified.app +# Local dev: use http://127.0.0.1:3000 (NOT http://localhost:3000). +# The atproto OAuth client rejects http:// URLs unless they're +# the spec's `http://localhost` (literal, no port) loopback +# exception — and cookies don't cross localhost ↔ 127.0.0.1, +# so pick the IP form and stick to it for the whole flow. +# When PUBLIC_URL is missing or http://, src/lib/auth/oauth-client.ts +# auto-switches to the loopback dev metadata +# (buildAtprotoLoopbackClientMetadata) so sign-in works without +# a public HTTPS host. +PUBLIC_URL=http://127.0.0.1:3000 # Required in production: Secret for signing session cookies (generate with: openssl rand -hex 32) COOKIE_SECRET=dev-secret-change-in-production @@ -13,6 +25,8 @@ UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= # Optional: Set to enable confidential client (private_key_jwt) authentication +# in production. Ignored in loopback dev mode (the spec mandates +# token_endpoint_auth_method: none for loopback clients). # Generate with: openssl ecparam -name prime256v1 -genkey -noout | openssl pkcs8 -topk8 -nocrypt # ATPROTO_PRIVATE_KEY= diff --git a/.gitignore b/.gitignore index 181a5a40..5635e17a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.pi/ diff --git a/AGENTS.md b/AGENTS.md index cf98125a..16b2046b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,183 +1,894 @@ -# Agent Instructions +# Agent Instructions — Certified -See `README.md` for project overview, architecture, and setup. +This document is the canonical reference for coding agents working in this repository. It supersedes the shorter `AGENTS.md` and complements `README.md`. Read it end-to-end on a fresh clone; treat the file map and security rules as authoritative. -## Quick Reference +> **Before you touch anything: work from a git worktree, not the main checkout.** See [§19 Git & Deployment](#19-git--deployment) for the exact commands. The main `/Users/sharfy/Code/certified-app` checkout is the user's active working tree — every task gets its own `../certified-app-` worktree. + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Tech Stack](#2-tech-stack) +3. [Quick Reference](#3-quick-reference) +4. [Environment Variables](#4-environment-variables) +5. [Architecture & Data Flow](#5-architecture--data-flow) +6. [Provider Tree & Layout System](#6-provider-tree--layout-system) +7. [Routing Map](#7-routing-map) +8. [Authentication Flow](#8-authentication-flow) +9. [API Routes Catalog](#9-api-routes-catalog) +10. [XRPC Proxy](#10-xrpc-proxy) +11. [CSS Conventions](#11-css-conventions) +12. [Component Conventions](#12-component-conventions) +13. [Hooks Catalog](#13-hooks-catalog) +14. [State Management](#14-state-management) +15. [Groups Feature](#15-groups-feature) +16. [Identity-Link / Wallet Attestation](#16-identity-link--wallet-attestation) +17. [Security Rules](#17-security-rules) +18. [SEO / GEO](#18-seo--geo) +19. [Git & Deployment](#19-git--deployment) +20. [File Map](#20-file-map) +21. [Known Limitations](#21-known-limitations) +22. [Common Pitfalls](#22-common-pitfalls) +23. [Adding a New Feature — Checklist](#23-adding-a-new-feature--checklist) +24. [Adding a New API Route — Checklist](#24-adding-a-new-api-route--checklist) +25. [Conventions: Errors, Loading, A11y](#25-conventions-errors-loading-a11y) + +--- + +## 1. Project Overview + +Certified is a passwordless identity platform built on **AT Protocol** (atproto), operated by the **Hypercerts Foundation**. It lets a user create one identity that travels across partner applications with full data portability and no vendor lock-in. + +- **Primary user** — anyone signing in to a partner app via Certified, plus admins managing groups (organizations). +- **Two domains** — `certified.app` (this app, the BFF + UI) and `certified.one` (the ePDS / extended Personal Data Server that hosts user data). When a user signs up they get an atproto identity rooted at `certified.one`; they can also sign in with any external atproto handle. +- **AT Protocol context** — atproto identities are DIDs (`did:plc:...` or `did:web:...`). Each DID resolves to a DID document that points to a PDS service endpoint, where records are stored under collections (NSIDs) like `app.bsky.actor.profile` or the Certified-specific `app.certified.actor.profile`. This app does not run a PDS itself — it is a thin OAuth client + BFF that proxies XRPC calls. +- **Custom collections** the app reads/writes: + - `app.certified.actor.profile` — Certified profile (display name, avatar, banner, etc.). + - `app.certified.actor.organization` — group metadata (org type, urls, founded date). + - `app.certified.actor.membership` — user-side record of group memberships. + - `app.bsky.actor.profile` — fallback profile (for Bluesky discoverability). + - `org.impactindexer.link.attestation` — EIP-712 wallet attestation linking an EVM address to a DID. +- **Group service** — a separate atproto service (currently `atproto-group-gate-staging.up.railway.app`) that manages multi-user organizations. The app proxies all group operations through the user's PDS using a custom `certified_group` proxy pattern with custom NSIDs (`app.certified.group.*`). + +## 2. Tech Stack + +| Concern | Choice | +|---|---| +| Framework | Next.js **16.x** (App Router, React Server Components) | +| React | 19.x | +| Language | TypeScript 5 (strict, `paths: { "@/*": ["./src/*"] }`) | +| Styling | Tailwind CSS 3.4 (utilities only) + custom CSS in `globals.css` (BEM-like) | +| Atproto SDK | `@atproto/api` 0.13, `@atproto/oauth-client-node` 0.3, `@atproto/jwk-jose` 0.1 | +| Session/State store | Upstash Redis (`@upstash/redis`) — REST-based, serverless-safe | +| Server actions | None — all server work is in route handlers (`src/app/api/**`) | +| Wallets | `wagmi` 2.x + `viem` 2.x + `@tanstack/react-query` (mounted only on `/settings/wallet`) | +| Email | `resend` 6.x (feedback only; OTP emails are sent by the PDS) | +| Analytics | `@vercel/analytics` | +| Icons | `lucide-react` | +| Fonts | Inter (sans), Noto Serif (headline), Instrument Serif (alt) — via `next/font/google` | +| Hosting | Vercel | +| Lint | ESLint flat config extending `next/core-web-vitals` and `next/typescript` | +| Test runner | **None.** The "quality gate" is `next build` + `tsc --noEmit`. A behavioral plan lives at `tests/groups.test-plan.md`. | + +> Note: Next.js 16 renamed `middleware.ts` to `proxy.ts`. The proxy handler is at `src/proxy.ts`. + +## 3. Quick Reference ```bash -npm run dev # Start dev server -npm run build # Production build (quality gate) -npx tsc --noEmit # Type check only -npm run lint # ESLint +npm run dev # next dev — http://localhost:3000 +npm run build # next build — production build (quality gate) +npm start # next start — run production build locally +npm run lint # eslint src/ --ext .ts,.tsx +npx tsc --noEmit # type check only ``` -## Coding Conventions +When the user asks for a dev server, run `npm run dev` from the repo root. When verifying changes before reporting done, run `npm run build` — it is the only automated quality signal in the repo. + +## 4. Environment Variables + +Source: `.env.local.example` and `src/lib/utils/config.ts`. + +| Variable | Required | Purpose | +|---|---|---| +| `NEXT_PUBLIC_PDS_URL` | yes | PDS / handle resolver URL. Defaults to `https://certified.one`. | +| `PUBLIC_URL` | production | Public URL of this app. Used to derive OAuth `client_id`, `redirect_uris`, and the CSRF Origin allowlist. Falls back to `http://localhost:3000` in dev. **For local atproto OAuth sign-in to actually complete, set this to `http://127.0.0.1:3000`** — see [§22 Common Pitfalls](#22-common-pitfalls) #3. | +| `COOKIE_SECRET` | production | HMAC secret for the `certified_session` cookie. Generate with `openssl rand -hex 32`. In dev a fallback string is used. | +| `UPSTASH_REDIS_REST_URL` | yes | Upstash REST URL. | +| `UPSTASH_REDIS_REST_TOKEN` | yes | Upstash REST token. | +| `ATPROTO_PRIVATE_KEY` | optional | EC P-256 private key. If set, the OAuth client switches to confidential (`private_key_jwt` with `ES256`) and exposes a JWKS at `/.well-known/jwks.json`. | +| `RESEND_API_KEY` | optional | Resend key for `/api/feedback`. | +| `RESEND_FROM_EMAIL` | optional | Override "from" header. Defaults to `Certified `. | +| `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` | optional | Adds WalletConnect connector to the wagmi config when set. | +| `NEXT_PUBLIC_GROUP_SERVICE_URL` | optional | Group service base URL. Defaults to the staging Railway deployment. | +| `NEXT_PUBLIC_GROUP_SERVICE_DID` | optional | Group service DID (for `getServiceAuth` `aud`). Defaults to `did:web:atproto-group-gate-staging.up.railway.app`. | + +`PUBLIC_URL` is the most consequential variable — it is checked against the `Origin` header on every CSRF-protected route, baked into the OAuth client metadata, and used to build the `redirect_uris` array. If it does not match the deployed domain, sign-in and every POST will fail. + +## 5. Architecture & Data Flow + +``` +┌─────────────────┐ authFetch ┌──────────────────────┐ +│ Client (React) │ ───────────────────▶ │ /api/xrpc/[...method]│ +│ - useProfile │ /api/auth/session │ (BFF / proxy) │ +│ - useOrg │ ◀─────────────────── │ │ +│ - useSession │ │ uses session DID │ +└─────────────────┘ │ restores OAuth │ + │ session via Redis │ + └──────────┬───────────┘ + │ + │ DPoP-bound + │ atproto agent + ▼ + ┌─────────────────────────────┐ + │ User's PDS (e.g. certified.one)│ + └──────────────┬──────────────┘ + │ + ┌──────────────┴──────────────┐ + │ DID document → service ep │ + │ plc.directory or did:web │ + └─────────────────────────────┘ +``` -### CSS -- All custom CSS lives in `src/app/globals.css` — BEM-like classes with component prefixes -- CSS variables defined in `:root` for colors, borders, radius, transitions -- Warning colors use `--color-warning-bg`, `--color-warning-border`, `--color-warning-text` -- Transitions use `--transition-fast`, `--transition-base`, `--transition-slow` -- New components: use BEM classes in globals.css, not Tailwind for layout -- No `100vw` (causes horizontal overflow with scrollbar) — use `100%` instead +Key principles: + +1. **Browser never holds tokens.** The OAuth tokens / DPoP keys live in Upstash Redis under `oauth:session:` (30-day TTL). The browser only has the `certified_session` cookie, which is an HMAC-signed random session id mapping to a DID via `session:did:` in Redis (30-day TTL). +2. **All XRPC calls go through `/api/xrpc/[...method]`.** Never call the PDS from the browser directly with credentials — there are none. Use `authFetch()` from `src/lib/auth/fetch.ts`. It detects 401 and triggers the global `onUnauthorized` handler registered by `AuthProvider`, which clears auth state and asks the user to sign in again. +3. **Group operations** use a parallel set of routes under `/api/groups/**` because they require the AtpAgent's `withProxy("certified_group", groupDid)` pattern + custom NSID lexicons (`app.certified.group.*`). They do not share the `/api/xrpc/[...method]` handler. +4. **DID resolution is direct.** `resolvePdsUrl` and `resolveHandle` (in `src/lib/atproto/did.ts`) hit `plc.directory` or the `did:web` host with a 5s timeout; results are not cached server-side. + +## 6. Provider Tree & Layout System + +`src/app/layout.tsx` mounts the global tree: + +``` + + … JSON-LD: Organization + WebSite … + + // src/lib/providers.tsx (currently a passthrough) + // OAuth state, modal, redirect overlay + // Active group + memberships, persisted to localStorage + // "default" | "transparent" navbar variant + // Skip-to-main link + +
+ {children} // .app-shell wrapper, skipped on /welcome +
+