Files
timmy-tower/replit.md
alexpaynex bc78bfa452 Add Nostr integration to the roadmap for future development
Add a roadmap section to replit.md detailing the planned integration of Nostr for node credential delivery, job status events, and node identity, replacing current HTTP polling mechanisms.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 45e1ce2b-4846-4800-be09-ed16006cca5f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8
Replit-Helium-Checkpoint-Created: true
2026-03-18 19:15:24 +00:00

225 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Workspace
## Overview
pnpm workspace monorepo using TypeScript. Each package manages its own dependencies.
## Stack
- **Monorepo tool**: pnpm workspaces
- **Node.js version**: 24
- **Package manager**: pnpm
- **TypeScript version**: 5.9
- **API framework**: Express 5
- **Database**: PostgreSQL + Drizzle ORM
- **Validation**: Zod (`zod/v4`), `drizzle-zod`
- **API codegen**: Orval (from OpenAPI spec)
- **Build**: esbuild (CJS bundle)
## Structure
```text
artifacts-monorepo/
├── artifacts/ # Deployable applications
│ └── api-server/ # Express API server
├── lib/ # Shared libraries
│ ├── api-spec/ # OpenAPI spec + Orval codegen config
│ ├── api-client-react/ # Generated React Query hooks
│ ├── api-zod/ # Generated Zod schemas from OpenAPI
│ └── db/ # Drizzle ORM schema + DB connection
├── scripts/ # Utility scripts (single workspace package)
│ └── src/ # Individual .ts scripts, run via `pnpm --filter @workspace/scripts run <script>`
├── pnpm-workspace.yaml # pnpm workspace (artifacts/*, lib/*, lib/integrations/*, scripts)
├── tsconfig.base.json # Shared TS options (composite, bundler resolution, es2022)
├── tsconfig.json # Root TS project references
└── package.json # Root package with hoisted devDeps
```
## TypeScript & Composite Projects
Every package extends `tsconfig.base.json` which sets `composite: true`. The root `tsconfig.json` lists all packages as project references. This means:
- **Always typecheck from the root** — run `pnpm run typecheck` (which runs `tsc --build --emitDeclarationOnly`). This builds the full dependency graph so that cross-package imports resolve correctly. Running `tsc` inside a single package will fail if its dependencies haven't been built yet.
- **`emitDeclarationOnly`** — we only emit `.d.ts` files during typecheck; actual JS bundling is handled by esbuild/tsx/vite...etc, not `tsc`.
- **Project references** — when package A depends on package B, A's `tsconfig.json` must list B in its `references` array. `tsc --build` uses this to determine build order and skip up-to-date packages.
## Root Scripts
- `pnpm run build` — runs `typecheck` first, then recursively runs `build` in all packages that define it
- `pnpm run typecheck` — runs `tsc --build --emitDeclarationOnly` using project references
## Environment Variables & Secrets
### Automatically provisioned (do not set manually)
| Secret | Purpose |
|---|---|
| `AI_INTEGRATIONS_ANTHROPIC_BASE_URL` | Replit AI Integrations proxy base URL for Anthropic |
| `AI_INTEGRATIONS_ANTHROPIC_API_KEY` | Replit AI Integrations proxy API key (dummy value, auto-managed) |
| `DATABASE_URL` | PostgreSQL connection string (Replit-managed) |
| `SESSION_SECRET` | Express session secret (Replit-managed) |
### Required secrets (set via Replit Secrets tab)
| Secret | Description | Example |
|---|---|---|
| `LNBITS_URL` | Base URL of your LNbits instance | `https://legend.lnbits.com` |
| `LNBITS_API_KEY` | Invoice/Admin API key from your LNbits wallet | `a3f...` |
> **Note:** If `LNBITS_URL` and `LNBITS_API_KEY` are absent, `LNbitsService` automatically runs in **stub mode** — invoices are simulated in-memory and can be marked paid via `svc.stubMarkPaid(hash)`. This is intentional for development without a Lightning node.
### Node bootstrap secrets (for `POST /api/bootstrap`)
| Secret | Description | Default |
|---|---|---|
| `BOOTSTRAP_FEE_SATS` | Startup fee in sats the user pays | `10000` (dev) |
| `DO_API_TOKEN` | Digital Ocean personal access token | absent = stub mode |
| `DO_REGION` | DO datacenter region | `nyc3` |
| `DO_SIZE` | DO droplet size slug | `s-4vcpu-8gb` |
| `DO_VOLUME_SIZE_GB` | Block volume to attach in GB (`0` = none) | `0` |
| `TAILSCALE_API_KEY` | Tailscale API key for generating auth keys | optional |
| `TAILSCALE_TAILNET` | Tailscale tailnet name (e.g. `example.com`) | required with above |
> **Note:** If `DO_API_TOKEN` is absent, `ProvisionerService` automatically runs in **stub mode** — provisioning is simulated with fake credentials and a real SSH keypair. Set `DO_API_TOKEN` for real node creation.
## Packages
### `artifacts/api-server` (`@workspace/api-server`)
Express 5 API server. Routes live in `src/routes/` and use `@workspace/api-zod` for request and response validation and `@workspace/db` for persistence.
- Entry: `src/index.ts` — reads `PORT`, starts Express
- App setup: `src/app.ts` — mounts CORS, JSON/urlencoded parsing, routes at `/api`
- Routes: `src/routes/index.ts` mounts sub-routers; `src/routes/health.ts` exposes `GET /health` (full path: `/api/health`)
- Depends on: `@workspace/db`, `@workspace/api-zod`
- `pnpm --filter @workspace/api-server run dev` — run the dev server
- `pnpm --filter @workspace/api-server run build` — production esbuild bundle (`dist/index.cjs`)
- Build bundles an allowlist of deps (express, cors, pg, drizzle-orm, zod, etc.) and externalizes the rest
### `lib/db` (`@workspace/db`)
Database layer using Drizzle ORM with PostgreSQL. Exports a Drizzle client instance and schema models.
- `src/index.ts` — creates a `Pool` + Drizzle instance, exports schema
- `src/schema/index.ts` — barrel re-export of all models
- `src/schema/<modelname>.ts` — table definitions with `drizzle-zod` insert schemas (no models definitions exist right now)
- `drizzle.config.ts` — Drizzle Kit config (requires `DATABASE_URL`, automatically provided by Replit)
- Exports: `.` (pool, db, schema), `./schema` (schema only)
Production migrations are handled by Replit when publishing. In development, we just use `pnpm --filter @workspace/db run push`, and we fallback to `pnpm --filter @workspace/db run push-force`.
### `lib/api-spec` (`@workspace/api-spec`)
Owns the OpenAPI 3.1 spec (`openapi.yaml`) and the Orval config (`orval.config.ts`). Running codegen produces output into two sibling packages:
1. `lib/api-client-react/src/generated/` — React Query hooks + fetch client
2. `lib/api-zod/src/generated/` — Zod schemas
Run codegen: `pnpm --filter @workspace/api-spec run codegen`
### `lib/api-zod` (`@workspace/api-zod`)
Generated Zod schemas from the OpenAPI spec (e.g. `HealthCheckResponse`). Used by `api-server` for response validation.
### `lib/api-client-react` (`@workspace/api-client-react`)
Generated React Query hooks and fetch client from the OpenAPI spec (e.g. `useHealthCheck`, `healthCheck`).
### `artifacts/api-server` — Timmy API endpoints
#### Payment-gated job flow
```bash
BASE="https://${REPLIT_DEV_DOMAIN}" # or http://localhost:8080 in dev
# 1. Create a job (returns eval invoice)
curl -s -X POST "$BASE/api/jobs" \
-H "Content-Type: application/json" \
-d '{"request": "Write a haiku about lightning payments"}'
# → {"jobId":"…","evalInvoice":{"paymentRequest":"lnbcrt10u1…","amountSats":10}}
# 2. Poll status (returns eval invoice while unpaid)
curl -s "$BASE/api/jobs/<jobId>"
# 3. (Stub mode only) Mark eval invoice paid
curl -s -X POST "$BASE/api/dev/stub/pay/<paymentHash>"
# 4. Poll again — auto-advances to awaiting_work_payment, returns work invoice
curl -s "$BASE/api/jobs/<jobId>"
# 5. (Stub mode only) Mark work invoice paid
curl -s -X POST "$BASE/api/dev/stub/pay/<workPaymentHash>"
# 6. Poll again — auto-advances to complete, returns result
curl -s "$BASE/api/jobs/<jobId>"
# → {"jobId":"…","state":"complete","result":"…"}
```
Job states: `awaiting_eval_payment``evaluating``awaiting_work_payment``executing``complete` | `rejected` | `failed`
#### Lightning-gated node bootstrap
Pay a one-time startup fee → Timmy auto-provisions a Bitcoin full node on Digital Ocean.
```bash
# 1. Create bootstrap job (returns invoice)
curl -s -X POST "$BASE/api/bootstrap"
# → {"bootstrapJobId":"…","invoice":{"paymentRequest":"…","amountSats":10000},
# "message":"Stub mode: simulate payment with POST /api/dev/stub/pay/<hash>…"}
# 2. (Stub mode) Simulate payment
curl -s -X POST "$BASE/api/dev/stub/pay/<paymentHash>"
# 3. Poll status — transitions: awaiting_payment → provisioning → ready
curl -s "$BASE/api/bootstrap/<bootstrapJobId>"
# 4. When ready, response includes credentials (SSH key delivered once):
# {
# "state": "ready",
# "credentials": {
# "nodeIp": "…",
# "tailscaleHostname": "timmy-node-xxxx.tailnet.ts.net",
# "lnbitsUrl": "https://timmy-node-xxxx.tailnet.ts.net",
# "sshPrivateKey": "-----BEGIN OPENSSH PRIVATE KEY-----…" ← null on subsequent polls
# },
# "nextSteps": [ "SSH in", "Monitor sync", "Run lnd-init.sh", "Configure sweep" ]
# }
```
Bootstrap states: `awaiting_payment``provisioning``ready` | `failed`
Key properties:
- **Stub mode** auto-activates when `DO_API_TOKEN` is absent — returns fake credentials with a real SSH keypair
- **SSH key** delivered once then cleared from DB; subsequent polls show `sshKeyNote`
- **Tailscale** auth key auto-generated if `TAILSCALE_API_KEY` + `TAILSCALE_TAILNET` are set
- Bitcoin sync takes 12 weeks; `ready` means provisioned + bootstrapped, not fully synced
#### Free demo endpoint (rate-limited: 5 req/hour per IP)
```bash
curl -s "$BASE/api/demo?request=Explain+proof+of+work+in+one+sentence"
# → {"result":"…"}
```
#### Dev-only stub payment trigger
`POST /api/dev/stub/pay/:paymentHash` — marks a stub invoice paid in-memory.
Only available in development (`NODE_ENV !== 'production'`).
### `scripts` (`@workspace/scripts`)
Utility scripts package. Each script is a `.ts` file in `src/` with a corresponding npm script in `package.json`. Run scripts via `pnpm --filter @workspace/scripts run <script>`. Scripts can import any workspace package (e.g., `@workspace/db`) by adding it as a dependency in `scripts/package.json`.
## Roadmap
### Mode 2 — Pre-funded session (v2)
Token-based cost debits against a pre-funded balance. Spec in `TIMMY_TEST_PLAN.md` (Tests 1116 as SKIP stubs).
### Nostr integration
Nostr (NIP-04/NIP-44 encrypted DMs) is planned as the delivery layer for the bootstrap flow and beyond:
- **Node credential delivery** — provisioned node generates a Nostr keypair during cloud-init and publishes credentials as an encrypted DM to the user's pubkey; eliminates the current HTTP one-time SSH key polling mechanism
- **Job status events** — job state transitions (`awaiting_payment → complete`) published as Nostr events; clients subscribe instead of polling
- **Node identity** — each Timmy node gets a persistent Nostr identity (npub) for discovery and communication
Until Nostr is wired in, the current HTTP polling + one-time SSH key delivery serves as the POC placeholder.