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
..

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)

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.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:

@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.