- 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>
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:
kind27235,created_atwithin ±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)
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)
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.nostrPubkeyandreq.nostrEventare populated on successful verification. - Session bridge:
POST /auth/nostr/session(guarded byNostrAuthGuard) exchanges a signed HTTP request for a 15-minute JWT (sub = pubkey) plus refresh token.POST /auth/nostr/refreshexchanges a refresh token for a new pair. Use@UseGuards(NostrSessionJwtGuard)orHybridAuthGuardto accept these JWTs. - Link an existing user:
POST /auth/nostr/linkwith bothJwtAuthGuard(current user) andNostrAuthGuard(NIP-98 header) to attach a pubkey to the user record.POST /auth/nostr/unlinkremoves it. When a pubkey is linked, Nostr session JWTs also includeuidso downstream code can attachreq.user. If you need both JWT + Nostr on the same call, send the Nostr signature innostr-authorization(orx-nostr-authorization) header so it doesn’t conflict withAuthorization: Bearer ....
Example:
@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
Hostand (if behind proxies)x-forwarded-prototo the values used when signing the URL. - Stale signatures: rotate signatures per request; reuse will fail once outside the replay window.