first commit

This commit is contained in:
2026-04-29 02:35:00 +00:00
commit 2cb17df4c5
90 changed files with 7321 additions and 0 deletions

203
README.md Normal file
View File

@@ -0,0 +1,203 @@
# nip05api
A single-domain NIP-05 identity service. Lightning-paid registration through
LNbits. Lifecycle DMs over Nostr. Optional outbound webhook on payment events.
Swagger docs. One binary, one SQLite file, one systemd unit.
See [SPEC.md](SPEC.md) for the full design specification.
## Features
- NIP-05 lookup at `/.well-known/nostr.json` for active subscribers
- Yearly or lifetime subscriptions, BOLT11 invoices via LNbits
- Renewal detection by pubkey: same user, same username, extended expiry
- Configurable expiry-reminder DMs and grace-period username reservation
- Profile sync: pulls `kind:0` from configured relays and updates non-pinned usernames
- HMAC-signed webhook outbox with exponential retry
- NIP-04 or NIP-17 (gift-wrapped) DM delivery
- Pure-Go SQLite, no CGO; static binary under 20 MB
- Embedded migrations applied on boot
- Embedded OpenAPI spec served at `/openapi.json`, Swagger UI at `/docs`
## Quick start
```bash
git clone https://github.com/noderunners/nip05api.git
cd nip05api
cp .env.example .env
cp messages.example.yaml messages.yaml
# Edit .env: DOMAIN, ADMIN_API_KEY, LNBITS_*, DM_NSEC, RELAYS
make build
./bin/nip05api
```
The binary creates `.data/nip05.db`, applies migrations, and starts listening
on `PORT` (default `8080`). Hit `http://localhost:8080/healthz` to confirm.
## Configuration
All config is environment-driven. See [.env.example](.env.example) for the full
list. Required keys:
| Key | Notes |
|---|---|
| `DOMAIN` | the apex domain users will be `name@domain` on |
| `ADMIN_API_KEY` | 24+ chars; sent as `X-API-Key` on `/v1/admin/*` |
| `LNBITS_URL`, `LNBITS_INVOICE_KEY` | required when `LIGHTNING_ENABLED=true` |
| `RELAYS` | comma-separated `wss://...`, required when DM or sync enabled |
| `DM_NSEC` | required when `DM_ENABLED=true` |
Subsystems toggle independently: `LIGHTNING_ENABLED`, `DM_ENABLED`,
`USERNAME_SYNC_ENABLED`, plus webhook is enabled by setting `WEBHOOK_URL`.
## API surface
Eleven endpoints, full schema at `/openapi.json` and rendered at `/docs`:
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | `/.well-known/nostr.json` | — | NIP-05 lookup |
| GET | `/healthz` | — | DB ping + version |
| GET | `/v1/pricing` | — | yearly/lifetime sat pricing |
| GET | `/v1/users/{pubkey}` | — | lookup by hex or npub |
| GET | `/v1/usernames/{name}/available` | — | availability check |
| POST | `/v1/invoices` | — | create payment invoice |
| GET | `/v1/invoices/{hash}` | — | invoice status |
| POST | `/v1/admin/users` | api key | manual add |
| GET | `/v1/admin/users` | api key | list / search |
| PUT | `/v1/admin/users/{pubkey}` | api key | pin username |
| DELETE | `/v1/admin/users/{pubkey}` | api key | hard delete |
| POST | `/v1/admin/users/{pubkey}/extend` | api key | extend or upgrade |
## Deployment
### systemd (recommended for VMs)
```bash
make build
sudo make install-systemd
sudo cp .env /opt/nip05api/.env
sudo systemctl enable --now nip05api
journalctl -u nip05api -f
```
The unit is hardened (no-new-privileges, ProtectSystem=strict, capability-bound
to nothing) and runs as a dedicated `nip05` user. See
[deploy/nip05api.service](deploy/nip05api.service).
### Docker
```bash
docker build -t nip05api:latest .
docker compose up -d
```
The image is multi-stage and ships from `gcr.io/distroless/static-debian12:nonroot`.
Compose binds the data directory and `messages.yaml` from the host so updates
to copy don't require a rebuild.
### Reverse proxy
A reverse proxy terminates TLS and forwards `/.well-known/nostr.json`, `/v1/*`,
`/healthz`, `/openapi.json`, and `/docs` to `127.0.0.1:8080`.
Sample configs:
- nginx → [deploy/nginx.conf](deploy/nginx.conf)
- Caddy → [deploy/Caddyfile](deploy/Caddyfile)
The proxy must forward `X-Forwarded-For` or `X-Real-IP`; the API uses these
for rate limiting and access logs.
## Operations
### Database
SQLite WAL-mode at `DATABASE_PATH` (default `.data/nip05.db`). Backups while
the service is running:
```bash
sqlite3 .data/nip05.db ".backup .data/backup-$(date +%F).db"
```
WAL files (`-wal`, `-shm`) are checkpointed on graceful shutdown. To rotate:
stop the service, copy `nip05.db`, `nip05.db-wal`, `nip05.db-shm` together,
restart.
### Migrations
Embedded as `.sql` files under `internal/db/migrations/`. They run inside a
transaction on every boot and record their version in `schema_migrations`.
Applying twice is a no-op. To add a migration: drop a new
`NNNN_description.sql` next to the others; rebuild.
### Audit log
Admin actions, payment confirmations, expiries, and grace-period purges all
write to `audit_log`. Cleanup worker prunes entries older than 180 days
hourly.
### Outbox cleanup
Delivered webhook and DM rows are pruned after 7 days; dead (gave up after
all retries) after 30 days. Tunable in `cmd/nip05api/cleanup.go`.
### Crash safety
- Migrations: transactional, idempotent.
- Payment confirmation: idempotent. The first confirmation captures
`target_expires_at` on the invoice row; subsequent retries apply the same
absolute value, so a crash mid-confirm cannot double-extend a renewal.
- Webhook delivery: outbox + retry schedule (`30s, 2m, 10m, 1h, 6h`),
HMAC-signed if `WEBHOOK_SECRET` is set.
- DM delivery: outbox + retry schedule (`1m, 5m, 30m, 2h, 12h`).
- Expiry pass: each phase (reminders → expirations → grace cleanup) updates
idempotent flags, so partial completion is safe to resume.
- Workers all watch a single root context; SIGINT/SIGTERM closes them in
order with a 30s HTTP grace window.
### Observability
- Logs: structured JSON to stdout via `slog`; ship to journald or your
logging stack of choice. `request_id` is propagated through middleware.
- Secrets that never appear in logs: `ADMIN_API_KEY`, `LNBITS_INVOICE_KEY`,
`WEBHOOK_SECRET`, `DM_NSEC`, BOLT11 strings, DM plaintext.
- Health: `/healthz` returns 200 with version + db status, 503 if DB unreachable.
## Development
```bash
make tidy # go mod tidy
make build # static binary in bin/
make test # short tests
make test-race # all tests with -race
make lint # golangci-lint, see .golangci.yml
make docker # build container image
```
Module layout:
```
cmd/nip05api/ entry point + cleanup worker
internal/audit/ append-only audit log
internal/config/ env loader + validation
internal/db/ SQLite + embedded migrations
internal/dm/ outbox + NIP-04/NIP-17 builder + worker
internal/expiry/ daily reminders / expirations / grace cleanup
internal/http/ chi router, middleware, handlers, OpenAPI docs
internal/invoice/ LNbits client + service + repo
internal/log/ slog setup
internal/messages/ YAML template loader + renderer
internal/nostr/ keys, relay pool, profile fetch, publish
internal/payments/ poll LNbits → confirm → dispatch DM/webhook/audit
internal/sync/ periodic kind:0 username sync
internal/user/ repo + service
internal/webhook/ outbox + HMAC signer + worker
```
Each Go file is kept under ~200 lines. The dependency graph points downward:
handlers depend on services, services on repos, repos on `db`. Nothing in
`internal/` depends on `internal/http/`.
## License
See [LICENSE](LICENSE) (MIT).