Backend in @gashboard/api (Express 5 + TS, ~1.2k LoC):
Auth (NIP-98 over HTTP, lifted from indeehub pattern):
- Client signs a kind-27235 event with method+URL, base64s as
Authorization: Nostr <event>. Server verifies sig, freshness
(±120s), method/URL tags via constant-time string compare.
- npub allowlist decoded to hex once at boot, fail-closed if any
entry is malformed or list is empty.
- HS256 JWT sessions returning {token, npub, expiresAt}.
- express-rate-limit on POST /api/auth/login (10/min/IP).
Datum integration (the trickier half):
- HTTP Digest *SHA-256* client (community-fork Datum uses sha-256,
not md5; node has no first-class support — hand-rolled in
digest.ts: parse challenge → ha1=sha256(user:realm:pw),
ha2=sha256(method:URI), response=sha256(...) → retry).
- HTML parsers for /clients (per-worker) and /threads (auth-less
fallback) using node-html-parser.
- Profile matcher: UserAgent contains "NerdQAxe" → NerdQAxe;
else worker-name suffix on auth username → workerNameMatchers.
Live UA strings observed: NerdQAxe self-IDs; Bitaxe / Avalon
Nano 3 / Avalon Mini 3 all report cgminer/4.11.1, must match
via workername.
- 5s poll interval, 10s AbortController timeout per upstream call,
in-memory snapshot, /api/datum/stats + SSE /api/datum/stream.
Hardened-by-default Express setup:
helmet CSP (frame-ancestors 'none', script-src 'self'),
pino with redaction (auth header, *.password, *.token, *.jwt,
*.sig), AppError class + central errorHandler, zod env validation,
graceful shutdown on SIGTERM/SIGINT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
32 lines
1.2 KiB
TypeScript
32 lines
1.2 KiB
TypeScript
import type { Request, Response, NextFunction } from "express";
|
|
import { logger } from "./logger.js";
|
|
|
|
export class AppError extends Error {
|
|
readonly status: number;
|
|
readonly code: string;
|
|
constructor(status: number, code: string, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
export const unauthorized = (code = "unauthorized") =>
|
|
new AppError(401, code, "Authentication required.");
|
|
export const forbidden = (code = "forbidden") =>
|
|
new AppError(403, code, "Access denied.");
|
|
export const badRequest = (code = "bad_request", message = "Invalid request.") =>
|
|
new AppError(400, code, message);
|
|
export const upstreamUnavailable = () =>
|
|
new AppError(503, "upstream_unavailable", "Upstream temporarily unavailable.");
|
|
|
|
export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) {
|
|
if (res.headersSent) return;
|
|
if (err instanceof AppError) {
|
|
res.status(err.status).json({ error: { code: err.code, message: err.message } });
|
|
return;
|
|
}
|
|
logger.error({ err }, "unhandled_error");
|
|
res.status(500).json({ error: { code: "internal_error", message: "Something went wrong." } });
|
|
}
|