first commit
This commit is contained in:
203
README.md
Normal file
203
README.md
Normal 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).
|
||||
Reference in New Issue
Block a user