Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,26 @@ EVENTS_SUBSCRIBE_LISTS=0135a251-8a46-4f88-b5bc-315d982eb7fa
# project pages, dynamic OG images, and the Nostr sitemap pick up new
# community submissions. Send the value as `x-revalidate-secret` header.
REVALIDATE_SECRET=change-me

# ─── Local dev environment (optional, DEV ONLY) ─────────────────────────────
#
# Isolated testing: a local relay + throwaway dev key + in-app impersonation.
# See AGENTS.md ("Local dev environment"). Run `pnpm gen:dev-keys` to generate
# the keypair, then `pnpm relay:up` to start the local relay.
#
# ⚠ NEVER set any NEXT_PUBLIC_DEV_* / NEXT_PUBLIC_NOSTR_RELAYS in production —
# they expose impersonation UI and a signing secret to the browser. Leave all
# of these unset in prod; the app falls back to real relays and hides the bar.

# Turn on the DEV MODE bar (account impersonation, one-click admin login).
# NEXT_PUBLIC_DEV_MODE=true

# Route ALL Nostr publish/read traffic to a relay set (comma-separated).
# Point at the local relay so nothing reaches public relays.
# NEXT_PUBLIC_NOSTR_RELAYS=ws://localhost:7777

# Browser-side admin secret for the bar's "Entrar como La Crypta" button.
# Must be the nsec whose npub equals NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB above.
# In dev, set LACRYPTA_NSEC + NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB to this same
# throwaway keypair (all three come from `pnpm gen:dev-keys`).
# NEXT_PUBLIC_DEV_ADMIN_NSEC=nsec1...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ yarn-error.log*
.env*
!.env.example

# local dev nostr relay data (sqlite db + wal/shm)
/dev/relay/data/

# vercel
.vercel

Expand Down
53 changes: 53 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,56 @@

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

## Local dev environment (isolated Nostr)

**Why this exists.** Nostr events can never be deleted once a relay accepts them. Testing flows that publish — community voting, project submissions, badges — against public relays with La Crypta's real key would permanently pollute production. So local dev runs against an **isolated local relay** signed by a **throwaway dev key**, and surfaces a **DEV MODE bar** for impersonating accounts. Three independent pillars, all off by default:

1. **Local relay** — `dev/relay/` runs `nostr-rs-relay` (Docker) at `ws://localhost:7777`. It implements parameterized-replaceable events (kind 30078 / NIP-33), so ballot re-votes replace correctly. Nothing published here leaves the machine.
2. **Dev keypair** — a throwaway key, **never** the production nsec. Generate with `pnpm gen:dev-keys`; it prints the `.env.local` lines below.
3. **`NEXT_PUBLIC_DEV_MODE=true`** — gates the DEV MODE bar (`components/DevModeBar.tsx`) and impersonation. `isDevMode()` in `lib/devMode.ts` is the single check (build-time inlined, hydration-safe).

### Setup

```bash
pnpm install
pnpm gen:dev-keys # prints dev keys — paste the block into .env.local
pnpm relay:up # start the local relay (ws://localhost:7777)
pnpm dev # NEXT_PUBLIC_DEV_MODE=true → DEV MODE bar appears
# pnpm relay:logs / pnpm relay:down to tail / stop the relay
```

### The DEV MODE bar

A fixed 32px strip above the header (header shifts to `top-8`, `<main>` gets `pt-8` — both gated by `isDevMode()`, no per-page edits). Its switcher (backed by `lib/useDevIdentities.ts`, shared with `/dev/voting`) lets you:

- Generate throwaway identities; log in as any of them.
- **"Entrar como La Crypta (admin)"** via `NEXT_PUBLIC_DEV_ADMIN_NSEC` to open/close voting.
- **Impersonate any soldier with a linked Nostr pubkey** (listed under "Soldados con Nostr", also available as an "Impersonar" button on each `/soldados/[slug]` profile).
- **"Generar datos dummy"** — seed a complete fake dataset (see below).

### Impersonation model (stand-in keys)

We never hold real users' secret keys, so impersonating a user logs in with a **deterministic dev stand-in key** derived from their pubkey (`lib/devImpersonation.ts`: `sha256("lacrypta-dev-impersonation:v1:" + pubkey)`). To make this consistent across the app:

- **Voting** (`app/api/hackathons/[id]/voting/route.ts`): in dev mode the eligibility snapshot is remapped real-pubkey → stand-in, so impersonated users are eligible and self-vote blocks/budgets are preserved.
- **Reads** (`/dashboard/projects`, `/dashboard/hackathones`): `auth.impersonating` holds the real pubkey; in dev these read the impersonated user's data by it.

`lib/voting.ts` (the shared contract) is untouched — all dev behaviour is gated by `isDevMode()`.

### Dummy data generator ("complete dev")

`lib/devSeed.ts` (`generateDummyData`, triggered by the bar's "Generar datos dummy") creates 8 dummy users + ~14 projects across hackathons and publishes **real kind-0 profiles + kind-30078 project events, each signed by that user's own key**, to the local relay. Because they go through the same event shapes the app reads, the dummy users automatically become soldiers (impersonatable + voting-eligible) with vote budgets matching their hackathon count, and their projects appear on hackathon pages, `/projects`, and the dashboard. The generated **nsecs are stored** in `localStorage["labs:dev:dummy-users:v1"]` and logged to the console. After seeding, the client flushes the roster cache via the dev-only `POST /api/dev/revalidate` (gated by `isDevMode()`, no secret needed).

`/dashboard/hackathones` ("Mis hackatones") shows the logged-in (or impersonated) user's hackathon participations grouped by hackathon, derived from their Nostr projects.

### Env vars (all dev-only; see `.env.example`)

| Var | Effect |
| --- | --- |
| `NEXT_PUBLIC_DEV_MODE` | `true` → DEV MODE bar + impersonation. |
| `NEXT_PUBLIC_NOSTR_RELAYS` | Comma-separated relay override. `ws://localhost:7777` routes **all** traffic local. Single chokepoint in `lib/nostrRelayConfig.ts`. |
| `NEXT_PUBLIC_DEV_ADMIN_NSEC` | Browser-side dev admin secret for the one-click admin login. Must match `NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB`. |
| `LACRYPTA_NSEC` / `NEXT_PUBLIC_LACRYPTA_ADMIN_NPUB` | In dev, set both to the throwaway keypair from `gen:dev-keys`. |

> ⚠️ **Never set `NEXT_PUBLIC_DEV_MODE`, `NEXT_PUBLIC_DEV_ADMIN_NSEC`, or `NEXT_PUBLIC_NOSTR_RELAYS` in a production deploy.** They expose impersonation UI and a signing secret to the browser. Production leaves all three unset — the app falls back to the real relays and the bar never renders.
24 changes: 24 additions & 0 deletions app/api/dev/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
import { isDevMode } from "@/lib/devMode";
import {
NOSTR_PROJECTS_TAG,
NOSTR_LEGACY_SUBMISSIONS_TAG,
NOSTR_SOLDIERS_RANKING_TAG,
} from "@/lib/nostrCacheTags";

/**
* Dev-only cache flush. After seeding dummy users/projects to the local relay,
* the client calls this so the soldiers roster + voting eligibility (which read
* cached relay snapshots) pick up the new data immediately — no production
* REVALIDATE_SECRET needed. 404s unless NEXT_PUBLIC_DEV_MODE.
*/
export async function POST() {
if (!isDevMode()) {
return NextResponse.json({ error: "not found" }, { status: 404 });
}
revalidateTag(NOSTR_PROJECTS_TAG, { expire: 0 });
revalidateTag(NOSTR_LEGACY_SUBMISSIONS_TAG, { expire: 0 });
revalidateTag(NOSTR_SOLDIERS_RANKING_TAG, { expire: 0 });
return NextResponse.json({ ok: true });
}
24 changes: 24 additions & 0 deletions app/api/dev/soldiers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { isDevMode } from "@/lib/devMode";
import { getSoldiers } from "@/lib/soldiers";
import { buildEligibleVoters } from "@/lib/voting";
import { HACKATHONS } from "@/lib/hackathons";

/**
* Dev-only roster of impersonatable users (soldiers with a linked Nostr pubkey)
* for the DEV MODE bar's account switcher. Returns the same set + vote budget
* the voting eligibility builder produces. 404s unless NEXT_PUBLIC_DEV_MODE.
*/
export async function GET() {
if (!isDevMode()) {
return NextResponse.json({ error: "not found" }, { status: 404 });
}
const soldiers = await getSoldiers().catch(() => []);
// maxVotes (distinct hackathons participated) is hackathon-independent, so
// any id reuses the canonical budget logic. Only voters with a pubkey appear.
const eligible = buildEligibleVoters(soldiers, HACKATHONS[0]?.id ?? "");
const list = eligible
.map((v) => ({ pubkey: v.pubkey, name: v.name, maxVotes: v.maxVotes }))
.sort((a, b) => b.maxVotes - a.maxVotes || a.name.localeCompare(b.name));
return NextResponse.json({ soldiers: list });
}
Loading