Files
Dorian cdd24a5def 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>
2026-02-12 20:14:39 +00:00

90 lines
3.8 KiB
Markdown
Raw Permalink 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.
# 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 callers 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 doesnt 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.