TrustBridge Dashboard is the open-source web interface for the TrustBridge project. It connects GitHub contributor identities to Stellar G-addresses and validates payout readiness before Wave disbursements.
The problem: Stellar payments fail with
PAYMENT_NO_TRUSTwhen a recipient account lacks a trustline for the asset being sent (e.g. USDC). Maintainers need a single source of truth mappinggithub_username → stellar_addresswith live trustline and reserve checks — before batch payouts.
- Features
- Quick start
- Documentation
- Tech stack
- Routes & API
- Environment variables
- Testing
- Deployment
- Contributing
- License
| Audience | Capability |
|---|---|
| Contributors | Sign in with GitHub OAuth, register a Stellar G-address, get live Horizon validation (funding, USDC trustline, XLM reserve), view outreach template examples |
| Maintainers | View all registrations, filter by readiness, batch re-check via Horizon, review per-row Horizon diagnostics, use an accessible desktop table or mobile card layout, export enriched CSV/JSON data, generate outreach templates for contributors, review recent Soroban contract events |
| Everyone | Public landing page with Wave readiness stats |
- Freighter ownership proof — the
/registerflow now generates a deterministic message-signing challenge tied to the contributor's GitHub handle and Stellar payout address. Contributors can copy that challenge and sign it in Freighter when maintainers need wallet ownership proof. - Per-row Horizon debug panel — each dashboard row exposes the current readiness summary, the next recommended action, and the underlying funded/trustline/reserve checkpoints used for payout decisions.
- Accessible responsive table — the maintainer table includes captioned headers, sortable column labels with
aria-sort, and a mobile card layout so contributor readiness stays reviewable on smaller screens. - Export parity — CSV and JSON exports now include the Horizon debug summary, recommended next action, and Freighter proof challenge so maintainer reviews stay consistent outside the UI.
The dashboard includes a template generator (on the register page) that creates contributor outreach materials in three formats:
- Email — subject line, body, next steps, wallet proof guidelines
- Markdown — checklist, troubleshooting table, with emoji and formatting
- Plain text — simple, universal format for copy-paste or SMS
Templates are customizable by Wave number, contributor name, minimum XLM requirement, deadline, and support email. Download or copy directly to clipboard.
| Status | Badge | Meaning |
|---|---|---|
| Ready | ✅ | Funded, USDC trustline active and authorized, spendable XLM ≥ minimum reserve |
| Low reserve | Funded + authorized trustline, but spendable XLM below threshold | |
| Not ready | ❌ | Unfunded, missing trustline, or a present-but-unauthorized trustline |
Authorization matters: a trustline can exist but remain unauthorized by the asset issuer (Stellar
AUTH_REQUIREDassets). Payments to an unauthorized trustline still fail, so the dashboard tracksis_authorizedseparately and an account is only verified (✅ on-chain badge) when funded and holding an authorized trustline.
Spendable vs. raw XLM: every Stellar account must keep a minimum reserve locked up —
baseReserve * (2 + subentries + sponsoring − sponsored)— plus any XLM tied up inselling_liabilities. That reserve is not available for payments, so the reserve check compares against spendable XLM (spendableXlmBalance), not the rawxlm_balance. A contributor with 5 XLM raw balance and 3 trustlines can show ~3.5 XLM locked in reserve, leaving well under 1 XLM spendable — this islow_reserve, notready, even though the raw balance alone looks healthy.
- Node.js 18+ and npm
- PostgreSQL database (local, Neon, Supabase, or Vercel Postgres)
- GitHub OAuth App (create one here)
git clone https://github.com/your-org/trustbridge-dashboard.git
cd trustbridge-dashboard
npm installFor local development with PostgreSQL in Docker:
docker-compose up -dSee docs/DOCKER_COMPOSE.md for details.
cp .env.example .env.localFill in all required values — see docs/ENVIRONMENT.md for full reference.
If using Docker Compose, set:
DATABASE_URL="postgresql://trustbridge:trustbridge-dev-password@localhost:5432/trustbridge_dashboard?schema=public"npm run db:pushnpm run devOpen http://localhost:3000.
GitHub OAuth callback URL (local):
http://localhost:3000/api/auth/callback/github
All docs are cross-linked from this README:
| Document | Description |
|---|---|
| Setup guide | Step-by-step local development setup |
| Docker Compose stack | Containerized dev environment with PostgreSQL |
| Environment variables | Every env var explained |
| Architecture | System design, data flow, auth model |
| Project structure | Directory layout and key files |
| Deployment | Vercel deployment checklist |
| Grafana dashboards & metrics | Ready-to-import Grafana JSON dashboards, Prometheus metric freeze specification, on-call alert rules |
| Contributing | How to contribute to this repo |
| CSRF protection | Threat model, protected routes, non-browser client policy, testing guide |
| Sentry error tracking | Setup, environment variables, instrumented routes, testing guide |
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS, shadcn/ui patterns |
| Data fetching | TanStack React Query |
| Auth | NextAuth.js + GitHub OAuth |
| Database | PostgreSQL + Prisma ORM |
| Blockchain | stellar-sdk + Horizon API |
| Deployment | Vercel (recommended) |
Brand colors: Stellar purple #3E1BDB, cyan accent #00B4D8. Dark mode supported — all UI colour pairs meet WCAG 2.1 AA contrast requirements (≥ 4.5:1 for normal text, ≥ 3.0:1 for large text / UI components).
| Route | Auth | Description |
|---|---|---|
/ |
Public | Landing page, TrustBridge explainer, Wave stats |
/register |
GitHub OAuth | Contributor Stellar address registration |
/dashboard |
GitHub OAuth + org member | Maintainer payout readiness table |
/dashboard/metrics |
GitHub OAuth + org member | Admin metrics: readiness counts, audit activity, ops config |
| Endpoint | Method | Description |
|---|---|---|
/api/auth/[...nextauth] |
GET/POST | NextAuth.js handlers |
/api/check |
POST | Horizon validation { address, asset_code?, asset_issuer? } |
/api/register |
GET/POST | Read/save contributor registration (authenticated) |
/api/address-history |
GET | Contributor's own AddressHistoryRecord timeline, newest first (self only; maintainers may pass ?userId= for another user) |
/api/contributors |
GET/POST | List contributors / batch re-check (maintainer only). Response includes registryMode (REGISTRY_MODE env var) |
/api/contributors/paginated |
GET | Cursor-paginated contributor list for infinite scroll (maintainer only). Also includes registryMode |
/api/contributors/[id] |
POST | Re-check a single contributor via Horizon (maintainer only) |
/api/audit |
GET | Recent maintainer actions — audit log (maintainer only) |
/api/stats |
GET | Aggregate readiness statistics — publicly cached (Cache-Control: public, max-age=<ttl>, stale-while-revalidate) |
/api/actions/lookup |
GET | Cached Horizon readiness lookup + wizard nextAction guidance, ?address=G... — publicly cached (30 s TTL) |
/api/soroban/events |
GET | Recent events for SOROBAN_CONTRACT_ID (maintainer only) |
/api/settings/network |
GET | Resolved Horizon/Soroban network + mismatch warnings (maintainer only) |
/api/contract-sync |
GET/POST | GET: last sync result (public). POST: trigger a contract-to-Postgres sync (maintainer session or CRON_SECRET) |
/api/cron/export |
GET/POST | GET: last export result (public). POST: trigger an automated nightly treasury CSV dump (maintainer session or CRON_SECRET) |
src/lib/contract-sync.tsre-syncs Postgres registration state against Horizon-verified funded/trustline/balance state, intended to be driven by a scheduler (e.g. Vercel Cron) rather than a maintainer clicking "recheck all". Trigger withPOST /api/contract-sync(maintainer session, or a scheduler presentingAuthorization: Bearer $CRON_SECRET); read the last run viaGET /api/contract-syncor thecontractSyncblock of/api/health.- Rate-limited via
CONTRACT_SYNC_MIN_INTERVAL_MS(default 60s) so a mis-configured scheduler can't fan out into repeated full-table Horizon sweeps, and never throws — Horizon/RPC outages and DB errors are captured in the result instead of raising a 500.
src/lib/cron-export.tsautomates nightly contributor CSV dumps for treasury payout operations during Waves. Trigger withPOST /api/cron/export(maintainer session orAuthorization: Bearer $CRON_SECRET).- Checks contributor staleness against
STALE_CSV_MAX_AGE_MS(default 24h), attaches the full CSV dataset, emails the destination configured viaTREASURY_EXPORT_EMAIL, and records an audit log entry (export.cron). - Rate-limited via
CRON_EXPORT_MIN_INTERVAL_MS(default 60s) to prevent spamming destinations, and never throws unhandled errors. Check the last run status viaGET /api/cron/export.
- Stats API cache headers —
GET /api/statsandGET /api/actions/lookupemitCache-Control: public, max-age=<ttl>, stale-while-revalidate=<swr>so CDN edges (Vercel Edge Network, Cloudflare, etc.) and browsers serve cached responses without hitting the origin. Two layers cooperate: an in-processstatsCache(default 60 s TTL, controlled bySTATS_CACHE_TTL_MS) eliminates redundant DB queries within a server instance, and the HTTP headers let the CDN cache aggregate stats globally. The cache is automatically evicted whenever contributor readiness changes (batch recheck, single recheck). The lookup endpoint uses a shorter fixed 30 s TTL to keep wizard validation fresh. Neither endpoint exposes contributor PII — only aggregate counts and per-address Horizon results are returned. - Background recheck queue —
src/lib/background-queue.tsimplements an in-memory job queue for Horizon rechecks. All recheck requests (batch and single) are queued and processed with a default concurrency limit of 2. This prevents Horizon rate-limit exhaustion and allows maintainers to request rechecks without blocking. Check queue status and job results via/api/contributors/queue/statusand/api/contributors/queue/jobs/[jobId]. Configurable concurrency via code (currently hardcoded at 2 jobs max). Job history is retained in memory (last 100 completed jobs). - Horizon circuit breaker —
src/lib/circuit-breaker.tswraps Horizon API calls. After 5 consecutive failures, the breaker opens and fast-fails for 30s, returning a friendly "Horizon is temporarily unavailable" message. Configurable viaHORIZON_CB_FAILURE_THRESHOLD,HORIZON_CB_RECOVERY_MS, andHORIZON_CB_SUCCESS_THRESHOLD. - Stale CSV export guard —
src/lib/stale-export.tscheckslastCheckedAttimestamps before CSV export. If any contributor hasn't been verified within the configured window (default 24h), the dashboard shows an amber warning banner and requires confirmation before exporting. Configurable viaSTALE_CSV_MAX_AGE_MS.
- Request/response logging —
src/lib/logger.tsprovides structured JSON logging for debugging and monitoring. All API requests, Horizon calls, and database operations can be logged with context and metadata. Enable debug logging viaDEBUG=trueenvironment variable. - Observability — Log format includes timestamp, log level (info/warn/error/debug), context identifier, message, and optional details. Perfect for ingestion into centralized logging platforms.
- Cursor-based pagination —
/api/contributors/paginatedsupports efficient cursor-based pagination via theuseInfiniteContributors()React Query hook. Useful for tables with 100+ contributors. - React Query integration —
src/lib/use-infinite-contributors.tsprovides a drop-in hook for infinite scroll UIs. Automatically fetches next pages as users scroll.
src/middleware.ts protects /register (requires sign-in) and /dashboard (requires sign-in + GITHUB_MAINTAINER_ORG membership).
The dashboard listens for GitHub organization membership changes via webhook at /api/webhooks/github-org-membership. When a member is added to or removed from your maintainer organization, the webhook records an audit entry, allowing the system to track membership changes.
Setup:
-
Generate a webhook secret:
openssl rand -base64 32
-
Set
GITHUB_WEBHOOK_SECRETin your.env.local:GITHUB_WEBHOOK_SECRET=<generated-secret>
-
Configure the webhook in your GitHub organization:
- Go to Organization Settings → Webhooks → Add webhook
- Payload URL:
https://your-domain.com/api/webhooks/github-org-membership - Content type:
application/json - Secret: The same secret from step 1
- Events: Select "Organization" and check "Member"
The webhook endpoint returns HTTP 202 (Accepted) for all webhook deliveries to prevent GitHub retry storms. Processing is logged and failures are captured in the audit log.
- CSRF protection — All mutating API routes validate the
Origin/Refererheader against the application's host. See docs/CSRF.md. - Rate limiting —
POST /api/checkis rate-limited per IP (default 10 requests per minute) to prevent Horizon API abuse. Configure viaRATE_LIMIT_WINDOW_MSandRATE_LIMIT_MAX_REQUESTS. - CSV / JSON exports — Maintainer dashboard exports contributor data as CSV or JSON. Export helpers live in
src/lib/csv.tsand are covered by snapshot tests. - Freighter proof workflow —
GET/POST /api/registerreturnswalletProofandhorizonDebugmetadata alongside the registration so the register page, dashboard table, and exports can render the same ownership-proof and troubleshooting guidance.
Copy .env.example to .env.local and configure:
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=
TOKEN_ENCRYPTION_KEY= # required, openssl rand -base64 32 — encrypts stored access tokens
GITHUB_MAINTAINER_ORG=
GITHUB_MAINTAINER_TEAM= # optional, team slug within GITHUB_MAINTAINER_ORG — org-only check if unset
DATABASE_URL=
NEXT_PUBLIC_HORIZON_URL=https://horizon.stellar.org
NEXT_PUBLIC_DEFAULT_ASSET_CODE=USDC
NEXT_PUBLIC_DEFAULT_ASSET_ISSUER=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN
NEXT_PUBLIC_MIN_XLM_BALANCE=1
NEXT_PUBLIC_BASE_RESERVE_XLM=0.5 # optional, Stellar base reserve used for spendable-balance checks
SOROBAN_CONTRACT_ID= # optional, future on-chain registry + event timeline panel
SOROBAN_RPC_URL= # optional, defaults to soroban-testnet.stellar.org
STATS_CACHE_TTL_MS= # optional, in-process + HTTP cache TTL for /api/stats (default 60000 ms = 60 s)
CHECK_CACHE_TTL_MS= # optional, in-process cache TTL for /api/check responses (default 120000 ms = 2 min)Note:
NEXT_PUBLIC_HORIZON_URLandSOROBAN_RPC_URLshould point at the same Stellar network. Their defaults don't (mainnet vs. testnet) — the dashboard detects and warns on this mismatch via/api/settings/networkand the maintainer dashboard's network status panel, and records it to the audit log.
Caching:
STATS_CACHE_TTL_MScontrols both the in-processstatsCacheTTL and themax-agevalue in theCache-Controlheader emitted byGET /api/stats. Setting it to0or a non-numeric value falls back to the 60 s default. To disable CDN caching entirely during local development, setSTATS_CACHE_TTL_MS=1(1 ms expires immediately in the in-process cache and producesmax-age=0in the header).
See docs/ENVIRONMENT.md for details.
The dashboard validates all environment variables using Zod at startup (src/lib/env-validation.ts). This schema:
- Ensures required fields are present — missing
GITHUB_CLIENT_ID,DATABASE_URL, etc. will fail fast - Casts numeric values —
RATE_LIMIT_MAX_REQUESTS→ number,NEXT_PUBLIC_MIN_XLM_BALANCE→ float - Validates URLs —
DATABASE_URL,NEXTAUTH_URL,SOROBAN_RPC_URLmust be valid URLs - Provides defaults — optional fields like
NEXT_PUBLIC_HORIZON_URLdefault to Stellar mainnet - Fails on startup — invalid configuration is caught before any route handlers run
Use validateEnv() or getValidatedEnv() to get the fully typed, validated configuration:
import { getValidatedEnv } from "@/lib/env-validation";
const env = getValidatedEnv();
// env is strongly typed and guaranteed validGenerate NEXTAUTH_SECRET:
openssl rand -base64 32Generate TOKEN_ENCRYPTION_KEY:
openssl rand -base64 32Generate GITHUB_WEBHOOK_SECRET (if using org membership sync):
openssl rand -base64 32Unit tests run on Vitest and cover the pure business logic
(readiness/authorization rules, the Horizon retry helper, audit-log formatting,
batch verification, contributor search/filter/sort/column helpers, the
GET /api/metrics admin endpoint, and the Soroban event-timeline read path):
npm test # run all Vitest unit + API tests once
npm run test:watch # watch mode
npm run test:unit # unit tests only
npm run test:api # API route tests onlyEnd-to-end tests run on Playwright and cover the maintainer dashboard flow end-to-end (access control, table search & column toggles, re-check, and the admin metrics page):
npm run test:e2e # headless Playwright run (requires running app)
npm run test:e2e:ui # interactive Playwright UIAll tests run in CI on every push and pull request, before the build.
Audits and enforces WCAG 2.1 AA contrast ratios across all dark-mode colour
pairs in the dashboard. Run automatically as part of npm test.
What was audited and fixed:
| File | Issue | Fix |
|---|---|---|
src/app/globals.css |
--destructive at L=30.6% gave ~2.3:1 on dark bg (hard WCAG AA fail) |
Raised to L=65% → ~5.9:1 |
src/app/globals.css |
--destructive-foreground was near-white (unreadable on new lighter red) |
Changed to dark (0 0% 10%) |
src/app/globals.css |
--muted-foreground at L=65.1% was marginal on card bg |
Raised to L=70% |
src/app/globals.css |
--primary / --ring at L=58% |
Raised to L=65% |
src/app/globals.css |
--accent at L=42% |
Raised to L=48% |
src/app/globals.css |
--border / --input at L=17.5% (invisible dividers in dark) |
Raised to L=22% |
src/components/ui/badge.tsx |
dark:text-*-400 (L≈60%) on dark card bg — below 4.5:1 |
Raised to dark:text-*-300 (L≈73%) |
src/components/ui/badge.tsx |
Light mode text-*-600 on white — marginal |
Raised to text-*-700 |
src/app/dashboard/metrics/page.tsx |
Status-box sub-labels dark:text-*-400 — below AA |
Raised to dark:text-*-200 |
src/app/dashboard/metrics/page.tsx |
Status-box borders dark:border-*-900 — near-invisible |
Raised to dark:border-*-800 |
src/app/register/RegisterClient.tsx |
Amber banner dark:text-amber-300 dark:bg-amber-500/10 |
Fixed to dark:text-amber-200 dark:bg-amber-950/40 |
src/components/ContributorTable.tsx |
Stale-data warning dark:border-amber-900 — near-invisible |
Raised to dark:border-amber-700/60 |
Automated tests live in src/lib/dark-mode-contrast-audit.test.ts. The file
encodes the WCAG 2.1 relative-luminance algorithm in pure TypeScript (no DOM
required) and runs 25 assertions across five describe blocks:
- CSS design tokens (foreground, muted-foreground, destructive, primary, accent)
- Badge dark/light text on card backgrounds
- Metrics-page status-box blended backgrounds (alpha-composited)
- RegisterClient maintainer error banner
- ContributorTable stale-data warning
- Regression guard: four pre-fix colours that must fail WCAG AA — if these ever pass it means the test palette data needs updating
Validates that POST /api/register is correct under concurrent load. Run with
npm run test:api or npm test.
Test file: tests/api/register-concurrency.test.ts — 22 assertions across 7
describe blocks.
| Scenario | Coverage |
|---|---|
| Idempotency | 5 simultaneous requests from the same user all return 200; Horizon called exactly N times |
| Address conflict race | Two users racing to claim one address: exactly one 200 + one 409 |
| Re-assignment (no spurious 409) | User updating their own registered address never gets a 409 |
| 100+ contributor scale | 120 distinct users register concurrently — all 200, no dropped requests, upsert called exactly 120× |
| Horizon outage | checkStellarAddress rejects for all callers → all 500, DB never written |
| Partial Horizon outage | Alternating pass/fail — correct mix of 200/500 responses |
| Mixed address pool | 50 user pairs each racing for the same address — exactly 50 wins (200) + 50 losses (409) |
| Auth edge cases | Unauthenticated → 401, cross-origin → 403, invalid format → 400, empty address → 400 with validationErrors |
All mocks target prisma, @/lib/horizon, next-auth, and @/lib/soroban-register
so no real database or network is needed. The conflict-detection mock simulates the
findUnique → upsert race window that exists in the route handler.
Schema hardening: The
Registrationtable enforces uniquestellarAddressper user (one-to-one viauserId), with comprehensive indexes ontrustlineReady,trustlineAuthorized,funded, andlastCheckedAtfor efficient filtering. All models include detailed field documentation inprisma/schema.prisma. The schema supports optimistic registration updates with proper cascading deletes and constraints to ensure data integrity during high-concurrency Wave operations.
This project is optimized for Vercel:
- Push to GitHub
- Import repo in Vercel
- Add environment variables from docs/ENVIRONMENT.md
- Attach a Postgres database (Vercel Postgres, Neon, etc.)
- Run
npm run db:pushagainst productionDATABASE_URL - Set GitHub OAuth callback to
https://your-domain.vercel.app/api/auth/callback/github
Full checklist: docs/DEPLOYMENT.md
We welcome contributions! Please read docs/CONTRIBUTING.md before opening a PR.
Quick flow:
- Fork the repo
- Create a feature branch (
git checkout -b feat/my-feature) - Commit with clear messages
- Run tests (
npm run test) - Open a pull request against
main
PostgreSQL is the source of truth for registrations. Setting SOROBAN_CONTRACT_ID
(+ optionally SOROBAN_RPC_URL) today enables the read-only maintainer event
timeline (GET /api/soroban/events), which fetches recent contract events and
degrades gracefully — never failing the dashboard — on RPC outages, rate limits,
or a missing contract ID.
Mirroring registrations to a Soroban contract (write-through) is designed but not yet implemented — see docs/ARCHITECTURE.md § Soroban register write-through for the read-vs-write-through breakdown, the intended write design, and how each edge case (outage, missing config, rate limits) is or would be handled.
This project is licensed under the MIT License.
- Architecture overview
- Project structure
- Setup guide
- Stellar Horizon API
- Stellar USDC trustlines Here’s a ~150-line implementation TODO you can drop directly into the issue/PR task:
TODO — GitHub Membership Session Revocation
Complexity: High — 200 points
Objective
Implement immediate access revocation when a GitHub organization member is removed. Existing dashboard JWTs must no longer remain valid until their normal expiry.
- Understand Current Auth Flow
[ ] Inspect src/lib/auth.ts.
[ ] Identify JWT creation logic.
[ ] Identify JWT verification logic.
[ ] Identify token expiry configuration.
[ ] Identify whether sessions are stateless or database-backed.
[ ] Identify where the authenticated user/org is resolved.
[ ] Inspect src/app/api/webhooks/github-org-membership/route.ts.
[ ] Document the current membership verification flow.
[ ] Find existing auth and webhook tests.
[ ] Find existing Prisma user/session models.
[ ] Check whether a session/version field already exists.
[ ] Confirm how GitHub organization membership is represented internally.
- Define Revocation Strategy
[ ] Prefer a per-user/session authorization version.
[ ] Add a sessionVersion/authVersion field if required.
[ ] Include the version in newly issued JWTs.
[ ] Compare JWT version against the current user version.
[ ] Increment the version when membership is revoked.
[ ] Ensure old JWTs immediately become unauthorized.
[ ] Avoid globally invalidating unrelated users.
[ ] Keep normal JWT expiration as a secondary safety mechanism.
[ ] Document why version-based revocation was selected.
[ ] Ensure missing version values fail safely.
[ ] Ensure malformed tokens remain rejected.
- Update auth.ts
[ ] Locate JWT signing implementation.
[ ] Add authorization/session version to JWT claims.
[ ] Keep claims minimal.
[ ] Do not place sensitive information in the JWT.
[ ] Update token verification to retrieve current auth state.
[ ] Compare token version with current version.
[ ] Reject revoked versions.
[ ] Return the existing unauthorized behavior.
[ ] Avoid leaking revocation details to clients.
[ ] Preserve existing expiry validation.
[ ] Preserve existing issuer/audience checks if present.
[ ] Ensure deleted users cannot authenticate.
[ ] Ensure users from another org cannot authenticate.
[ ] Ensure old tokens remain invalid after removal.
- Update GitHub Webhook
[ ] Inspect the member_removed event handler.
[ ] Verify the GitHub webhook signature.
[ ] Reject invalid webhook signatures.
[ ] Validate the webhook secret configuration.
[ ] Validate the GitHub organization.
[ ] Do not trust organization data supplied by the client.
[ ] Confirm the event action is actually member_removed.
[ ] Resolve the affected GitHub account.
[ ] Map GitHub account to the internal user.
[ ] Increment that user's authorization/session version.
[ ] Do not revoke sessions for unrelated users.
[ ] Make repeated removal events safe.
[ ] Make the operation idempotent.
[ ] Avoid throwing on unknown users.
[ ] Avoid locking out everyone on malformed payloads.
[ ] Return appropriate webhook HTTP responses.
[ ] Avoid logging webhook secrets.
[ ] Avoid logging complete JWTs.
[ ] Avoid logging unnecessary user information.
- Replay Protection
[ ] Inspect existing webhook replay protection.
[ ] Validate GitHub delivery identifiers where appropriate.
[ ] Prevent the same delivery from causing unintended state changes.
[ ] Ensure duplicate member_removed events are harmless.
[ ] Consider storing processed delivery IDs if necessary.
[ ] Avoid introducing a full session store unless required.
[ ] Ensure replayed valid events cannot revoke unrelated accounts.
[ ] Document replay assumptions.
- Database / Prisma
[ ] Determine whether Prisma changes are necessary.
[ ] Add authVersion if no suitable field exists.
[ ] Give existing users a safe default version.
[ ] Create the migration.
[ ] Ensure the field is indexed if required by lookup patterns.
[ ] Keep the migration backwards compatible.
[ ] Verify existing users can still authenticate.
[ ] Verify version increments are atomic.
[ ] Avoid race conditions during concurrent webhook processing.
- CSRF / Webhook Security
[ ] Confirm webhook endpoint is not relying on browser CSRF protection.
[ ] Require GitHub signature verification.
[ ] Validate the signature before processing the event.
[ ] Use constant-time signature comparison where applicable.
[ ] Reject missing signatures.
[ ] Reject malformed signatures.
[ ] Do not process unsigned membership events.
[ ] Ensure webhook authentication cannot be bypassed through headers/body fields.
- Tests — Webhook
[ ] Test valid member_removed.
[ ] Test invalid webhook signature.
[ ] Test missing webhook signature.
[ ] Test wrong organization.
[ ] Test malformed payload.
[ ] Test unknown GitHub user.
[ ] Test duplicate removal event.
[ ] Test unrelated organization.
[ ] Test unrelated user remains unaffected.
[ ] Test successful revocation.
[ ] Test webhook secret is never exposed.
[ ] Test replay behavior.
- Tests — Auth
[ ] Test newly issued JWT contains auth version.
[ ] Test valid JWT is accepted.
[ ] Test expired JWT is rejected.
[ ] Test revoked JWT is rejected.
[ ] Test new JWT after revocation works.
[ ] Test another user's JWT remains valid.
[ ] Test malformed JWT is rejected.
[ ] Test missing auth version fails safely.
[ ] Test deleted user is rejected.
[ ] Test wrong organization is rejected.
[ ] Test version mismatch returns unauthorized.
[ ] Test authorization works after a non-membership event.
- Documentation
[ ] Update docs/ENVIRONMENT.md if webhook/auth variables change.
[ ] Document required GitHub webhook secret.
[ ] Document supported GitHub organization.
[ ] Document JWT revocation behavior.
[ ] Document auth/session version semantics.
[ ] Add a short threat model.
[ ] Document replay protection.
[ ] Document malformed webhook handling.
[ ] Document operational recovery procedure.
[ ] Document required test commands.
- Threat Note
[ ] Threat: already-issued JWT remains valid after membership removal.
[ ] Mitigation: per-user authorization version revocation.
[ ] Threat: forged webhook removes legitimate users.
[ ] Mitigation: GitHub signature verification.
[ ] Threat: webhook from wrong organization.
[ ] Mitigation: explicit organization validation.
[ ] Threat: replayed webhook.
[ ] Mitigation: idempotent processing/replay protection.
[ ] Threat: malformed webhook locks out all users.
[ ] Mitigation: isolate updates to the resolved user.
[ ] Threat: JWT/token leakage through logs.
[ ] Mitigation: never log complete tokens.
- Validation
[ ] Run npm test -- webhook.
[ ] Run npm test -- auth.
[ ] Run the full test suite if practical.
[ ] Run Prisma migration tests if applicable.
[ ] Verify existing authentication flows.
[ ] Verify member removal manually in development.
[ ] Confirm the old JWT fails immediately.
[ ] Confirm a newly issued JWT works.
[ ] Confirm unrelated users remain logged in.
[ ] Confirm malformed events do not cause global revocation.
[ ] Confirm webhook secrets are absent from logs.
- PR Checklist
[ ] Revocation implemented.
[ ] JWT/session version validation implemented.
[ ] GitHub webhook signature verification implemented.
[ ] Organization validation implemented.
[ ] Replay handling implemented.
[ ] Tests added.
[ ] Existing auth tests still pass.
[ ] Webhook tests pass.
[ ] Database migration included if required.
[ ] Documentation updated.
[ ] Threat note included.
[ ] No unnecessary session-store complexity introduced.
[ ] No global user lockout on malformed events.
[ ] No secrets or JWTs logged.
[ ] npm test -- webhook && npm test -- auth passes.
[ ] PR description explains the revocation strategy.
[ ] PR is ready for maintainer review.