Implement backend API and database services in Docker setup
- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO. - Introduced PostgreSQL and Redis services with health checks and configurations for data persistence. - Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets. - Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage. - Enhanced the Dockerfile to support the new API environment variables and configurations. - Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
89
backend/docs/nostr-auth/README.md
Normal file
89
backend/docs/nostr-auth/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Nostr HTTP Auth (NIP-98)
|
||||
|
||||
IndeeHub exposes an HTTP authentication path that accepts requests signed by Nostr keys (NIP-98). Clients sign each request body + method + URL and send it via the `Authorization: Nostr <base64Event>` header. The server verifies the event, enforces a tight replay window, and makes the caller’s pubkey available on the Express request.
|
||||
|
||||
## Request format
|
||||
- Header: `Authorization: Nostr <base64Event>`.
|
||||
- Event requirements: `kind` 27235, `created_at` within ±120s of server time, and the following tags:
|
||||
- `["method", "<HTTP_METHOD>"]`
|
||||
- `["u", "<FULL_URL>"]` (include scheme, host, path, and query; exclude fragment)
|
||||
- `["payload", "<sha256-hex of raw body>"]` (omit or leave empty only when the request has no body)
|
||||
|
||||
## Client examples
|
||||
|
||||
### Browser (NIP-07)
|
||||
```ts
|
||||
async function fetchWithNostr(url: string, method: string, body?: unknown) {
|
||||
const payload = body ? JSON.stringify(body) : '';
|
||||
const tags = [
|
||||
['method', method],
|
||||
['u', url],
|
||||
['payload', payload ? sha256(payload) : ''],
|
||||
];
|
||||
const unsigned = {
|
||||
kind: 27235,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: '',
|
||||
};
|
||||
|
||||
const event = await window.nostr.signEvent(unsigned);
|
||||
const authorization = `Nostr ${btoa(JSON.stringify(event))}`;
|
||||
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
authorization,
|
||||
},
|
||||
body: payload || undefined,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Node (nostr-tools)
|
||||
```ts
|
||||
import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const secret = generateSecretKey();
|
||||
const pubkey = getPublicKey(secret);
|
||||
|
||||
const payload = JSON.stringify({ ping: 'pong' });
|
||||
const unsigned = {
|
||||
kind: 27235,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['u', 'https://api.indeehub.studio/nostr-auth/echo'],
|
||||
['method', 'POST'],
|
||||
['payload', crypto.createHash('sha256').update(payload).digest('hex')],
|
||||
],
|
||||
content: '',
|
||||
};
|
||||
|
||||
const event = finalizeEvent(unsigned, secret);
|
||||
const authorization = `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`;
|
||||
```
|
||||
|
||||
## Server usage
|
||||
- Apply the guard: `@UseGuards(NostrAuthGuard)` to require Nostr signatures.
|
||||
- Hybrid mode: `@UseGuards(HybridAuthGuard)` accepts either Nostr (NIP-98) or the existing JWT guard.
|
||||
- Access the caller: `req.nostrPubkey` and `req.nostrEvent` are populated on successful verification.
|
||||
- Session bridge: `POST /auth/nostr/session` (guarded by `NostrAuthGuard`) exchanges a signed HTTP request for a 15-minute JWT (`sub = pubkey`) plus refresh token. `POST /auth/nostr/refresh` exchanges a refresh token for a new pair. Use `@UseGuards(NostrSessionJwtGuard)` or `HybridAuthGuard` to accept these JWTs.
|
||||
- Link an existing user: `POST /auth/nostr/link` with both `JwtAuthGuard` (current user) and `NostrAuthGuard` (NIP-98 header) to attach a pubkey to the user record. `POST /auth/nostr/unlink` removes it. When a pubkey is linked, Nostr session JWTs also include `uid` so downstream code can attach `req.user`. If you need both JWT + Nostr on the same call, send the Nostr signature in `nostr-authorization` (or `x-nostr-authorization`) header so it doesn’t conflict with `Authorization: Bearer ...`.
|
||||
|
||||
Example:
|
||||
```ts
|
||||
@Post('protected')
|
||||
@UseGuards(NostrAuthGuard)
|
||||
handle(@Req() req: Request) {
|
||||
return { pubkey: req.nostrPubkey };
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
- Clock skew: ensure client and server clocks are within ±2 minutes.
|
||||
- URL canonicalization: sign the exact scheme/host/path/query received by the server (no fragments).
|
||||
- Payload hash: hash the raw request body bytes; any whitespace or re-serialization changes will fail verification.
|
||||
- Headers: set the `Host` and (if behind proxies) `x-forwarded-proto` to the values used when signing the URL.
|
||||
- Stale signatures: rotate signatures per request; reuse will fail once outside the replay window.
|
||||
Reference in New Issue
Block a user