diff --git a/.env.example b/.env.example
index dffd47e..76e5a1e 100644
--- a/.env.example
+++ b/.env.example
@@ -1,8 +1,35 @@
# Optional environment overrides for docker compose
# Copy to .env only if you need to change the defaults
-# API port (default: 3000)
-AUTH_PORT=3000
+# Bind addresses (which network interface to listen on)
+# Signet Daemon port (default: 3174)
+SIGNET_BIND_PORT=3174
+# Daemon bind address (default: all interfaces)
+# Examples: 0.0.0.0 (all interfaces), 127.0.0.1 (localhost only), 100.x.x.x (Tailscale)
+SIGNET_BIND_ADDRESS=0.0.0.0
# UI port (default: 4174)
-UI_PORT=4174
+UI_BIND_PORT=4174
+# UI bind address (default: all interfaces)
+# Examples: 0.0.0.0 (all interfaces), 127.0.0.1 (localhost only), 100.x.x.x (Tailscale)
+UI_BIND_ADDRESS=0.0.0.0
+
+# External addresses (if you want to expose parts of signet)
+# Signet Url (default: BIND_PORT:BIND_ADDRESS)
+# Where daemon is accessible (default: http://signet:3000 in Docker)
+# SIGNET_URL=
+
+# UI Host (default: localhost)
+# Where UI is accessible (default: http://UI_BIND_PORT:UI_BIND_ADDRESS)
+#UI_URL=localhost
+
+
+# UI Basic Authentication (disabled by default)
+# Set both values to enable authentication
+# UI_AUTH_USERNAME=admin
+# UI_AUTH_PASSWORD=your_secure_password
+
+# API Token for UI-to-daemon communication
+# Generate a secure token with: openssl rand -hex 32
+# Both the daemon and UI need this token for secure proxying
+SIGNET_API_TOKEN=
diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile
index 582e88e..5ba12d0 100644
--- a/apps/signet-ui/Dockerfile
+++ b/apps/signet-ui/Dockerfile
@@ -15,12 +15,12 @@ ENV NODE_ENV=production
COPY --from=build /app/apps/signet-ui/dist ./dist
COPY --from=build /app/apps/signet-ui/server.mjs ./server.mjs
# Install server dependencies (pinned to match package.json)
-RUN npm install --no-save express@4 http-proxy-middleware@3
+RUN npm install --no-save express@4 express-rate-limit@7 http-proxy-middleware@3 basic-auth@2
# Environment variables (can be overridden at runtime)
-ENV UI_PORT=4174
-ENV UI_HOST=0.0.0.0
-ENV DAEMON_URL=http://signet:3000
+ENV UI_BIND_PORT=4174
+ENV UI_BIND_ADDRESS=0.0.0.0
+ENV SIGNET_URL=http://signet:3000
EXPOSE 4174
CMD ["node", "server.mjs"]
diff --git a/apps/signet-ui/package.json b/apps/signet-ui/package.json
index 3e0d5de..171826d 100644
--- a/apps/signet-ui/package.json
+++ b/apps/signet-ui/package.json
@@ -16,7 +16,9 @@
},
"dependencies": {
"@signet/types": "workspace:*",
+ "basic-auth": "^2.0.1",
"debug": "^4.3.4",
+ "dotenv": "^16.6.1",
"express": "^4.22.1",
"focus-trap-react": "^11.0.6",
"html5-qrcode": "^2.3.8",
diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs
index 75a6bf3..7a6736c 100644
--- a/apps/signet-ui/server.mjs
+++ b/apps/signet-ui/server.mjs
@@ -1,17 +1,58 @@
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
+import rateLimit from 'express-rate-limit';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
+import auth from 'basic-auth';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
+// Load .env from repository root (two levels up) in development
+// In production (NODE_ENV=production), dotenv may not be installed
+if (process.env.NODE_ENV !== 'production') {
+ try {
+ const { config } = await import('dotenv');
+ config({ path: path.resolve(__dirname, '../../.env') });
+ } catch {
+ // dotenv not available, skip .env loading (production mode)
+ }
+}
+
const app = express();
// Support both new (UI_*) and legacy (PORT/HOST) env var names
-const port = Number.parseInt(process.env.UI_PORT ?? process.env.PORT ?? '4174', 10);
-const host = process.env.UI_HOST ?? process.env.HOST ?? '0.0.0.0';
-const daemonUrl = process.env.DAEMON_URL ?? 'http://localhost:3000';
+const port = Number.parseInt(process.env.UI_BIND_PORT ?? process.env.PORT ?? '4174', 10);
+const host = process.env.UI_BIND_ADDRESS ?? '0.0.0.0';
+const signetHost = process.env.SIGNET_HOST ?? 'localhost';
+const signetPort = process.env.SIGNET_PORT ?? '3000';
+const signetUrl = process.env.SIGNET_URL ?? `http://${signetHost}:${signetPort}`;
+
+// Basic auth configuration (disabled by default)
+const authUsername = process.env.UI_AUTH_USERNAME;
+const authPassword = process.env.UI_AUTH_PASSWORD;
+const isAuthEnabled = authUsername && authPassword;
+
+// Rate limiting for basic auth failures (prevent brute force)
+// Allows 10 failed attempts per 15 minutes per IP
+const authRateLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 10, // 10 attempts
+ skipSuccessfulRequests: true, // Only count failed auth attempts
+ standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
+ legacyHeaders: false, // Disable `X-RateLimit-*` headers
+ handler: (req, res) => {
+ res.status(429).send('Too many failed authentication attempts. Please try again later.');
+ },
+});
+
+// Load API token from environment variable
+const apiToken = process.env.SIGNET_API_TOKEN;
+if (apiToken) {
+ console.log('✓ Using API token from SIGNET_API_TOKEN environment variable');
+} else {
+ console.warn('⚠️ No SIGNET_API_TOKEN set - requests to daemon may fail if authentication is required');
+}
// Shared error handler for proxies
const onProxyError = (err, req, res) => {
@@ -43,7 +84,7 @@ const apiPaths = [
// SSE proxy for /events endpoint (no timeout, streaming)
const sseProxy = createProxyMiddleware({
- target: daemonUrl,
+ target: signetUrl,
changeOrigin: true,
proxyTimeout: 0,
timeout: 0,
@@ -53,6 +94,9 @@ const sseProxy = createProxyMiddleware({
proxyReq.setHeader('Accept', 'text/event-stream');
proxyReq.setHeader('Cache-Control', 'no-cache');
proxyReq.setHeader('Connection', 'keep-alive');
+ if (apiToken) {
+ proxyReq.setHeader('X-API-Token', apiToken);
+ }
},
proxyRes(proxyRes) {
proxyRes.headers['x-accel-buffering'] = 'no';
@@ -64,15 +108,38 @@ const sseProxy = createProxyMiddleware({
// API proxy for standard endpoints
const apiProxy = createProxyMiddleware({
- target: daemonUrl,
+ target: signetUrl,
changeOrigin: true,
proxyTimeout: 10_000,
pathFilter: apiPaths,
on: {
+ proxyReq(proxyReq) {
+ if (apiToken) {
+ proxyReq.setHeader('X-API-Token', apiToken);
+ }
+ },
error: onProxyError
}
});
+// Basic authentication middleware with rate limiting
+if (isAuthEnabled) {
+ // Apply rate limiter first
+ //app.use(authRateLimiter);
+
+ // Then check credentials
+ app.use((req, res, next) => {
+ const credentials = auth(req);
+
+ if (!credentials || credentials.name !== authUsername || credentials.pass !== authPassword) {
+ res.set('WWW-Authenticate', 'Basic realm="Signet UI"');
+ return res.status(401).send('Authentication required');
+ }
+
+ next();
+ });
+}
+
// Mount proxies at root - pathFilter handles routing
app.use(sseProxy);
app.use(apiProxy);
@@ -87,5 +154,6 @@ app.get('*', (_req, res) => {
});
app.listen(port, host, () => {
- console.log(`Signet UI listening on http://${host}:${port} (proxying ${daemonUrl})`);
+ const authStatus = isAuthEnabled ? ' [Basic Auth Enabled]' : '';
+ console.log(`Signet UI listening on http://${host}:${port} (proxying ${signetUrl})${authStatus}`);
});
diff --git a/apps/signet-ui/src/design-system.css b/apps/signet-ui/src/design-system.css
index c0b122d..dca08ef 100644
--- a/apps/signet-ui/src/design-system.css
+++ b/apps/signet-ui/src/design-system.css
@@ -30,6 +30,7 @@
/* Semantic colors */
--success: #22c55e;
+ --success-hover: #4cd67d;
--success-muted: rgba(34, 197, 94, 0.15);
--success-border: rgba(34, 197, 94, 0.3);
diff --git a/apps/signet-ui/vite.config.ts b/apps/signet-ui/vite.config.ts
index 3017963..e161aa4 100644
--- a/apps/signet-ui/vite.config.ts
+++ b/apps/signet-ui/vite.config.ts
@@ -1,12 +1,46 @@
///
-import { defineConfig } from 'vite';
+import { defineConfig, type Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { config as dotenvConfig } from 'dotenv';
+
+// Load .env from repository root (two levels up)
+dotenvConfig({ path: resolve(__dirname, '../../.env') });
const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'));
+function basicAuthPlugin(): Plugin {
+ return {
+ name: 'basic-auth',
+ configureServer(server) {
+ const username = process.env.UI_AUTH_USERNAME;
+ const password = process.env.UI_AUTH_PASSWORD;
+ if (!username || !password) return;
+
+ console.log('Basic Auth enabled for dev server');
+
+ server.middlewares.use((req, res, next) => {
+ const header = req.headers.authorization;
+ if (header) {
+ const match = header.match(/^Basic\s+(.+)$/);
+ if (match) {
+ const [user, pass] = Buffer.from(match[1], 'base64').toString().split(':');
+ if (user === username && pass === password) {
+ return next();
+ }
+ }
+ }
+ res.setHeader('WWW-Authenticate', 'Basic realm="Signet UI"');
+ res.statusCode = 401;
+ res.end('Authentication required');
+ });
+ },
+ };
+}
+
export default defineConfig({
- plugins: [react()],
+ plugins: [basicAuthPlugin(), react()],
define: {
__APP_VERSION__: JSON.stringify(packageJson.version),
},
diff --git a/apps/signet/Dockerfile b/apps/signet/Dockerfile
index db96e47..2fc3f3c 100644
--- a/apps/signet/Dockerfile
+++ b/apps/signet/Dockerfile
@@ -17,7 +17,7 @@ ENV CI=true
RUN pnpm install --frozen-lockfile
# Ensure better-sqlite3 native bindings are installed (prebuild-install downloads prebuilt binaries)
-RUN cd /app/node_modules/.pnpm/better-sqlite3@12.5.0/node_modules/better-sqlite3 && \
+RUN cd /app/node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3 && \
npx --yes prebuild-install -d
# Copy source files
@@ -60,8 +60,8 @@ WORKDIR /app/apps/signet
# Environment variables (can be overridden at runtime)
ENV DATABASE_URL="file:/app/config/signet.db"
-ENV SIGNET_PORT=3000
-ENV SIGNET_HOST=0.0.0.0
+ENV SIGNET_BIND_PORT=3000
+ENV SIGNET_BIND_ADDRESS=0.0.0.0
EXPOSE 3000
diff --git a/apps/signet/package.json b/apps/signet/package.json
index 0aa5d67..654f5dc 100644
--- a/apps/signet/package.json
+++ b/apps/signet/package.json
@@ -57,7 +57,6 @@
"handlebars": "^4.7.8",
"isomorphic-ws": "^5.0.0",
"nostr-tools": "^2.22.1",
- "prisma": "^7.3.0",
"qrcode": "^1.5.4",
"websocket-polyfill": "^0.0.3",
"ws": "^8.19.0",
@@ -68,6 +67,7 @@
"@types/node": "^20.19.0",
"@types/qrcode": "^1.5.6",
"@vitest/coverage-v8": "^4.0.18",
+ "prisma": "^7.4.0",
"ts-node": "^10.9.2",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts
index be53712..b6dde35 100644
--- a/apps/signet/src/config/config.ts
+++ b/apps/signet/src/config/config.ts
@@ -44,7 +44,7 @@ export async function loadConfig(configPath: string): Promise {
authPort: 3000,
authHost: '127.0.0.1',
baseUrl: 'http://localhost:4174',
- requireAuth: false,
+ requireAuth: true,
};
needsSave = true;
} else {
diff --git a/apps/signet/src/daemon/authorize.ts b/apps/signet/src/daemon/authorize.ts
index 231656e..060866f 100644
--- a/apps/signet/src/daemon/authorize.ts
+++ b/apps/signet/src/daemon/authorize.ts
@@ -120,8 +120,8 @@ async function resolveBaseUrl(connectionManager: ConnectionManager): Promise {
- const authMiddleware = createAuthMiddleware(this.fastify, this.config.requireAuth);
+ // Proxy authentication - verifies the UI proxy server identity
+ const proxyAuthMiddleware = createProxyAuthMiddleware(this.config.apiToken);
+
+ // User authentication - verifies the end user has a valid JWT session
+ const authMiddleware = createAuthMiddleware(
+ this.fastify,
+ this.config.requireAuth
+ );
+
const csrfMiddleware = createCsrfMiddleware();
const rateLimitAuth = createRateLimitMiddleware('auth');
const rateLimitKeys = createRateLimitMiddleware('keys');
@@ -147,7 +157,8 @@ export class HttpServer {
});
// CSRF token endpoint - provides a fresh token to the client
- this.fastify.get('/csrf-token', { preHandler: [authMiddleware] }, async (_request, reply) => {
+ // Requires both proxy auth (if configured) and user JWT session
+ this.fastify.get('/csrf-token', { preHandler: [proxyAuthMiddleware, authMiddleware] }, async (_request, reply) => {
const token = generateCsrfToken();
setCsrfCookie(reply, token, useSecureCookies);
return reply.send({ token });
@@ -160,14 +171,14 @@ export class HttpServer {
relayService: this.config.relayService,
getTrustScore: this.config.getTrustScore,
getTrustScoresForRelays: this.config.getTrustScoresForRelays,
- }, { auth: [authMiddleware], csrf: [csrfMiddleware] });
+ }, { auth: [proxyAuthMiddleware, authMiddleware], csrf: [csrfMiddleware] });
// Request routes (state-changing, needs CSRF)
registerRequestRoutes(this.fastify, {
requestService: this.config.requestService,
appService: this.config.appService,
}, {
- auth: [authMiddleware],
+ auth: [proxyAuthMiddleware, authMiddleware],
csrf: [csrfMiddleware],
rateLimit: [rateLimitAuth],
});
@@ -176,7 +187,7 @@ export class HttpServer {
registerKeysRoutes(this.fastify, {
keyService: this.config.keyService,
}, {
- auth: [authMiddleware],
+ auth: [proxyAuthMiddleware, authMiddleware],
csrf: [csrfMiddleware],
rateLimit: [rateLimitKeys],
});
@@ -185,25 +196,25 @@ export class HttpServer {
registerAppsRoutes(this.fastify, {
appService: this.config.appService,
}, {
- auth: [authMiddleware],
+ auth: [proxyAuthMiddleware, authMiddleware],
csrf: [csrfMiddleware],
});
// Dashboard routes (GET only, no CSRF needed)
registerDashboardRoutes(this.fastify, {
dashboardService: this.config.dashboardService,
- }, [authMiddleware]);
+ }, [proxyAuthMiddleware, authMiddleware]);
// Token routes (state-changing, needs CSRF)
registerTokensRoutes(this.fastify, {
- auth: [authMiddleware],
+ auth: [proxyAuthMiddleware, authMiddleware],
csrf: [csrfMiddleware],
rateLimit: [rateLimitAuth],
});
// Policy routes (state-changing, needs CSRF)
registerPoliciesRoutes(this.fastify, {
- auth: [authMiddleware],
+ auth: [proxyAuthMiddleware, authMiddleware],
csrf: [csrfMiddleware],
rateLimit: [rateLimitAuth],
});
@@ -211,19 +222,19 @@ export class HttpServer {
// Events routes (SSE, GET only, no CSRF needed)
registerEventsRoutes(this.fastify, {
eventService: this.config.eventService,
- }, [authMiddleware]);
+ }, [proxyAuthMiddleware, authMiddleware]);
// Nostrconnect routes (state-changing, needs CSRF)
registerNostrconnectRoutes(this.fastify, {
appService: this.config.appService,
}, {
- auth: [authMiddleware],
+ auth: [proxyAuthMiddleware, authMiddleware],
csrf: [csrfMiddleware],
});
// Dead man's switch routes (state-changing, needs CSRF)
registerDeadManSwitchRoutes(this.fastify, {
- auth: [authMiddleware],
+ auth: [proxyAuthMiddleware, authMiddleware],
csrf: [csrfMiddleware],
});
diff --git a/apps/signet/src/daemon/index.ts b/apps/signet/src/daemon/index.ts
index da70901..a33d7c7 100644
--- a/apps/signet/src/daemon/index.ts
+++ b/apps/signet/src/daemon/index.ts
@@ -1,3 +1,17 @@
+import { resolve } from 'path';
+
+// Load .env from repository root (three levels up from this file's location) in development
+// In production (NODE_ENV=production), dotenv may not be installed
+if (process.env.NODE_ENV !== 'production') {
+ try {
+ // Use require for synchronous loading to avoid top-level await
+ const dotenv = require('dotenv');
+ dotenv.config({ path: resolve(__dirname, '../../../../.env') });
+ } catch {
+ // dotenv not available, skip .env loading (production mode)
+ }
+}
+
import 'websocket-polyfill';
import { runDaemon } from './run.js';
import type { DaemonBootstrapConfig } from './types.js';
diff --git a/apps/signet/src/daemon/lib/auth.ts b/apps/signet/src/daemon/lib/auth.ts
index 75e0f99..9c673f5 100644
--- a/apps/signet/src/daemon/lib/auth.ts
+++ b/apps/signet/src/daemon/lib/auth.ts
@@ -240,12 +240,41 @@ export function sanitizeCallbackUrl(url: string | null | undefined): string | nu
return url;
}
+/**
+ * Create proxy authentication middleware to verify the UI proxy identity
+ * @param apiToken - API token for server-to-server authentication
+ */
+export function createProxyAuthMiddleware(apiToken?: string) {
+ return async function proxyAuthMiddleware(
+ request: FastifyRequest,
+ reply: FastifyReply
+ ): Promise {
+ // If no API token configured, skip proxy auth (local development)
+ if (!apiToken) {
+ return;
+ }
+
+ // Verify the proxy is authenticated
+ const requestApiToken = request.headers['x-api-token'] as string | undefined;
+ if (!requestApiToken || !timingSafeEqual(requestApiToken, apiToken)) {
+ reply.code(401).send({ error: 'Proxy authentication required' });
+ return;
+ }
+
+ // Mark request as coming from authenticated proxy
+ (request as any).isProxyAuthenticated = true;
+ };
+}
+
/**
* Create authentication middleware for protected routes
* @param fastify - Fastify instance
* @param requireAuth - If false, skip authentication (for local-only deployments)
*/
-export function createAuthMiddleware(fastify: FastifyInstance, requireAuth: boolean = true) {
+export function createAuthMiddleware(
+ fastify: FastifyInstance,
+ requireAuth: boolean = true
+) {
return async function authMiddleware(
request: FastifyRequest,
reply: FastifyReply
@@ -255,6 +284,7 @@ export function createAuthMiddleware(fastify: FastifyInstance, requireAuth: bool
return;
}
+ // Require JWT token validation for user authentication
const payload = await verifyToken(fastify, request);
if (!payload) {
@@ -382,6 +412,7 @@ export function checkRateLimit(
/**
* Create rate limiting middleware for sensitive endpoints
+ * Rate limiting is applied regardless of API token authentication
*/
export function createRateLimitMiddleware(endpoint: string = 'default') {
return async function rateLimitMiddleware(
diff --git a/apps/signet/src/daemon/lib/network.ts b/apps/signet/src/daemon/lib/network.ts
index bbdfb20..eaef2f0 100644
--- a/apps/signet/src/daemon/lib/network.ts
+++ b/apps/signet/src/daemon/lib/network.ts
@@ -105,8 +105,8 @@ export function getLocalAddresses(): LocalAddress[] {
/**
* Print server startup information including local URLs and optionally a QR code.
*/
-export async function printServerInfo(port: number): Promise {
- logger.info('HTTP server listening', { port });
+export async function printServerInfo(host: string, port: number): Promise {
+ logger.info('HTTP server listening', { port, host });
// In containers, the container's IP isn't useful for external connections
if (isRunningInContainer()) {
diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts
index 0e50f42..2095603 100644
--- a/apps/signet/src/daemon/run.ts
+++ b/apps/signet/src/daemon/run.ts
@@ -664,20 +664,25 @@ class Daemon {
private async startWebAuth(): Promise {
// Support both new (SIGNET_*) and legacy (AUTH_*) env var names
- const portEnv = process.env.SIGNET_PORT ?? process.env.AUTH_PORT;
- const authPort = this.config.authPort ?? (portEnv ? parseInt(portEnv, 10) : undefined);
- if (!authPort) {
- logger.info('No authPort configured, HTTP server disabled');
- return;
+ const port = process.env.SIGNET_BIND_PORT ? parseInt(process.env.SIGNET_BIND_PORT) : 3000;
+ const baseUrl = this.config.baseUrl ?? process.env.UI_URL;
+ const bindHost = process.env.SIGNET_BIND_ADDRESS ?? '0.0.0.0';
+ console.log(`Starting HTTP server on ${bindHost}:${port}...`);
+
+ // Load API token from environment variable
+ const apiToken = process.env.SIGNET_API_TOKEN;
+ if (apiToken) {
+ console.log('✓ Using API token from SIGNET_API_TOKEN environment variable');
+ } else {
+ console.log('⚠️ No SIGNET_API_TOKEN set - UI proxy authentication disabled');
}
- const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL;
- logger.info('Starting HTTP server', { port: authPort });
this.httpServer = new HttpServer({
- port: authPort,
- host: this.config.authHost ?? process.env.SIGNET_HOST ?? process.env.AUTH_HOST ?? '0.0.0.0',
+ host: bindHost,
+ port,
baseUrl,
jwtSecret: this.config.jwtSecret,
+ apiToken,
allowedOrigins: this.config.allowedOrigins ?? [],
requireAuth: this.config.requireAuth ?? false,
connectionManager: this.connectionManager,
@@ -694,7 +699,7 @@ class Daemon {
});
await this.httpServer.start();
- await printServerInfo(authPort);
+ await printServerInfo(bindHost, port);
}
private loadKeyMaterial(keyName: string, nsec: string): void {
@@ -719,4 +724,4 @@ class Daemon {
})),
});
}
-}
+}
\ No newline at end of file
diff --git a/apps/signet/src/index.ts b/apps/signet/src/index.ts
index de8a215..e8588d3 100644
--- a/apps/signet/src/index.ts
+++ b/apps/signet/src/index.ts
@@ -1,7 +1,21 @@
#!/usr/bin/env node
+import { resolve } from 'path';
import 'websocket-polyfill';
import { homedir } from 'os';
import { join } from 'path';
+
+// Load .env from repository root (two levels up from this file's location) in development
+// In production (NODE_ENV=production), dotenv may not be installed
+if (process.env.NODE_ENV !== 'production') {
+ try {
+ // Use require for synchronous loading to avoid top-level await
+ const dotenv = require('dotenv');
+ dotenv.config({ path: resolve(__dirname, '../../../.env') });
+ } catch {
+ // dotenv not available, skip .env loading (production mode)
+ }
+}
+
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { addKey } from './commands/add.js';
diff --git a/docker-compose.yml b/docker-compose.yml
index 8ebab12..b51abac 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -9,16 +9,17 @@ services:
mem_limit: 256mb
memswap_limit: 256mb
volumes:
- - $HOME/.signet-config:/app/config
+ - signet_config:/app/config
environment:
DATABASE_URL: ${DATABASE_URL:-file:/app/config/signet.db}
- SIGNET_PORT: ${SIGNET_PORT:-3000}
- SIGNET_HOST: ${SIGNET_HOST:-0.0.0.0}
- EXTERNAL_URL: ${EXTERNAL_URL:-http://localhost:4174}
+ SIGNET_BIND_PORT: ${SIGNET_BIND_PORT:-3000}
+ SIGNET_BIND_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0}
+ SIGNET_URL: ${SIGNET_URL:-http://${SIGNET_BIND_PORT:-signet}:${SIGNET_BIND_ADDRESS:-3000}}
+
ports:
- - "${SIGNET_PORT:-3000}:${SIGNET_PORT:-3000}"
+ - "${SIGNET_BIND_PORT:-3000}:3000"
healthcheck:
- test: ["CMD-SHELL", "wget -qO- http://localhost:${SIGNET_PORT:-3000}/health >/dev/null 2>&1 || exit 1"]
+ test: ["CMD-SHELL", "wget -qO- http://${SIGNET_BIND_ADDRESS:-localhost}:${SIGNET_BIND_PORT:-3000}/health >/dev/null 2>&1 || exit 1"]
interval: 10s
timeout: 5s
retries: 6
@@ -33,8 +34,15 @@ services:
signet:
condition: service_healthy
environment:
- UI_PORT: ${UI_PORT:-4174}
- UI_HOST: ${UI_HOST:-0.0.0.0}
- DAEMON_URL: ${DAEMON_URL:-http://signet:${SIGNET_PORT:-3000}}
+ UI_BIND_PORT: ${UI_BIND_PORT:-4174}
+ UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0}
+ UI_URL: ${UI_URL:-http://${UI_BIND_PORT:-localhost}:${UI_BIND_ADDRESS:-4174}}
+ SIGNET_URL: ${SIGNET_URL:-http://signet:3000}
+ UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-}
+ UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-}
ports:
- - "${UI_PORT:-4174}:${UI_PORT:-4174}"
+ - "${UI_BIND_PORT:-4174}:${UI_BIND_PORT:-4174}"
+
+volumes:
+ signet_config: {}
+ signet_db: {}
\ No newline at end of file
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index e07eef7..a46c8fe 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -19,8 +19,6 @@ All runtime settings live in `signet.json`, located at `~/.signet-config/signet.
"key": "auto-generated",
"secret": "auto-generated-256-bit"
},
- "authPort": 3000,
- "authHost": "0.0.0.0",
"baseUrl": "http://localhost:4174",
"database": "sqlite://signet.db",
"logs": "./signet.log",
@@ -65,7 +63,7 @@ Encrypted keys require the passphrase at boot or can be unlocked through the adm
All administration is done via the web UI. The following settings are required:
- `baseUrl`: public URL where the daemon is reachable (required for request approval flow).
-- `authPort` / `authHost`: local interface for the Fastify REST API.
+- `authPort`: port for the REST API (binds to `0.0.0.0` to accept connections on all interfaces).
## Logging
@@ -169,6 +167,43 @@ When `false` (default), the API is open for local development. Set to `true` for
}
```
+### `SIGNET_API_TOKEN` (Environment Variable)
+
+API token for server-to-server authentication between the UI proxy and the daemon.
+
+- **Type**: string (hex-encoded)
+- **Required**: Yes (for UI deployment)
+- **Location**: Environment variable (not in config file)
+
+Both the daemon and UI server must be configured with the same token for secure communication. Generate a secure token:
+
+```bash
+# Generate a secure API token
+openssl rand -hex 32
+```
+
+Set this token in your environment or `.env` file:
+
+```bash
+SIGNET_API_TOKEN=your_generated_token_here
+```
+
+**Security Model:**
+
+The `SIGNET_API_TOKEN` provides **proxy authentication** - it verifies that requests are coming from your trusted UI proxy server. This token:
+
+1. ✅ Authenticates the UI proxy server identity
+2. ⚠️ **Does NOT bypass user authentication** - the browser must still have a valid JWT session
+3. ⚠️ **Does NOT bypass CSRF protection** - state-changing requests still require CSRF tokens
+4. ⚠️ **Does NOT bypass rate limiting** - rate limits still apply
+
+When a request arrives at the daemon:
+- First, the daemon validates the `X-API-Token` header matches the configured token (proxy authentication)
+- Then, the daemon validates the user's JWT session from cookies/headers (user authentication)
+- Finally, CSRF tokens and rate limiting are enforced as normal
+
+This layered approach ensures that even if the API token leaks, attackers cannot bypass user authentication or perform unauthorized actions.
+
### `admin.secret`
Secret included in the bunker connection URI. Used to validate connection attempts from NIP-46 clients.
@@ -220,16 +255,23 @@ Rate limits are per-IP address. After exceeding the limit, requests receive HTTP
Docker Compose works out of the box with no `.env` file required. To customize settings, set these environment variables before running `docker compose`:
```bash
-SIGNET_PORT=3001 UI_PORT=8080 EXTERNAL_URL=https://signet.example.com docker compose up --build
+# Customize ports
+SIGNET_PORT=3001 UI_PORT=8080 docker compose up --build
+
+# Or set explicit URLs for complex networking
+SIGNET_URL=http://signet.local:3000 UI_URL=https://ui.example.com docker compose up --build
```
### Daemon Variables (`signet`)
| Variable | Description | Default |
|----------|-------------|---------|
+| `SIGNET_BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` (all interfaces) |
+| `SIGNET_HOST` | Hostname where daemon is accessible | `localhost` (or `signet` in Docker) |
| `SIGNET_PORT` | Port for the REST API | `3000` |
-| `SIGNET_HOST` | Host binding for the REST API | `0.0.0.0` |
-| `EXTERNAL_URL` | Public URL of the UI (for authorization flow) | `http://localhost:4174` |
+| `UI_HOST` | Hostname where UI is accessible (used for `UI_URL` if not set) | `localhost` |
+| `UI_PORT` | Port where UI is accessible (used for `UI_URL` if not set) | `4174` |
+| `UI_URL` | Public URL of the UI (for authorization flow). Defaults to `http://${UI_HOST}:${UI_PORT}` | `http://localhost:4174` |
| `DATABASE_URL` | SQLite database path | `file:~/.signet-config/signet.db` |
| `SIGNET_LOCAL` | Set to `1` for local development (uses relative DB path) | (not set) |
| `NODE_ENV` | Set to `development` for dev mode | `production` |
@@ -240,13 +282,31 @@ SIGNET_PORT=3001 UI_PORT=8080 EXTERNAL_URL=https://signet.example.com docker com
| Variable | Description | Default |
|----------|-------------|---------|
+| `UI_BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` (all interfaces) |
| `UI_PORT` | Port for the React UI | `4174` |
-| `UI_HOST` | Host binding for the UI server | `0.0.0.0` |
-| `DAEMON_URL` | Internal URL to reach the daemon | `http://localhost:3000` |
+| `SIGNET_HOST` | Hostname where daemon is accessible (used for `SIGNET_URL` if not set) | `localhost` (or `signet` in Docker) |
+| `SIGNET_PORT` | Port where daemon is accessible (used for `SIGNET_URL` if not set) | `3000` |
+| `SIGNET_URL` | Internal URL to reach the daemon. Defaults to `http://${SIGNET_HOST}:${SIGNET_PORT}` | `http://localhost:3000` |
+
+**How the services communicate:**
+- **UI → Daemon**: The UI uses `SIGNET_URL` to proxy API requests to the daemon
+- **Daemon → User**: The daemon uses `UI_URL` to send authorization redirect URLs
+
+**Network binding:**
+- Both services bind to `0.0.0.0` (all interfaces) by default
+- Use `SIGNET_BIND_ADDRESS` and `UI_BIND_ADDRESS` to bind to specific interfaces (e.g., `127.0.0.1` for localhost only, or a Tailscale IP like `100.x.x.x`)
+- The `*_HOST` and `*_PORT` variables are used to construct the URLs for service discovery, not for binding
-The `EXTERNAL_URL` environment variable is particularly important for Docker deployments. It tells Signet where to redirect users for request approval. If not set in the config file, the daemon will use this environment variable.
+**Example use cases:**
+```bash
+# Bind to Tailscale interface only
+SIGNET_BIND_ADDRESS=100.101.102.103 UI_BIND_ADDRESS=100.101.102.103 docker compose up
+
+# Localhost only (not accessible from network)
+SIGNET_BIND_ADDRESS=127.0.0.1 UI_BIND_ADDRESS=127.0.0.1 docker compose up
+```
-> **Note:** Legacy variable names (`AUTH_PORT`, `AUTH_HOST`, `BASE_URL`, `PORT`, `HOST`) are still supported for backward compatibility but are deprecated.
+> **Note:** The `authHost` config field is no longer used.
All other settings are configured in `signet.json`.
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
index cbc6bff..a9a74f7 100644
--- a/docs/DEPLOYMENT.md
+++ b/docs/DEPLOYMENT.md
@@ -18,10 +18,10 @@ You only expose the UI. The daemon doesn't need direct external access - it comm
### Configuration
-Set `EXTERNAL_URL` to your Tailscale hostname so that `auth_url` responses are reachable from other devices on your tailnet:
+Set `UI_URL` to your Tailscale hostname so that `auth_url` responses are reachable from other devices on your tailnet:
```bash
-EXTERNAL_URL=http://signet.tailnet-name.ts.net:4174 docker compose up --build
+UI_URL=http://signet.tailnet-name.ts.net:4174 docker compose up --build
```
Or in `signet.json`:
@@ -59,9 +59,9 @@ Then update your config to use HTTPS:
Note: Tailscale Serve on port 443 means you drop the port from URLs.
-### When is EXTERNAL_URL needed?
+### When is UI_URL needed?
-| Setup | EXTERNAL_URL |
+| Setup | UI_URL |
|-------|--------------|
| Single machine (Signet + apps on same device) | Not needed (localhost works) |
| Multi-device (Signet on server, apps on phone/laptop) | Required - use Tailscale hostname |
@@ -99,10 +99,10 @@ Use the server's Wireguard IP (e.g., `10.0.0.1`) - this is reachable from all pe
### Configuration
-Set `EXTERNAL_URL` to your Wireguard IP so that `auth_url` responses are reachable from other devices on your VPN:
+Set `UI_URL` to your Wireguard IP so that `auth_url` responses are reachable from other devices on your VPN:
```bash
-EXTERNAL_URL=http://10.0.0.1:4174 docker compose up --build
+UI_URL=http://10.0.0.1:4174 docker compose up --build
```
Or in `signet.json`:
@@ -128,9 +128,9 @@ Some browser features (like clipboard copy) require HTTPS. Unlike Tailscale, Wir
For most private network setups, HTTP is fine.
-### When is EXTERNAL_URL needed?
+### When is UI_URL needed?
-| Setup | EXTERNAL_URL |
+| Setup | UI_URL |
|-------|--------------|
| Single machine (Signet + apps on same device) | Not needed (localhost works) |
| Multi-device (Signet on server, apps on phone/laptop) | Required - use Wireguard IP |
diff --git a/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts
index 4777d20..b9dc6a7 100644
--- a/packages/signet-types/src/config/types.ts
+++ b/packages/signet-types/src/config/types.ts
@@ -64,10 +64,6 @@ export interface ConfigFile {
nostr: NostrConfig;
/** Admin interface configuration */
admin: AdminConfig;
- /** HTTP server port for REST API */
- authPort?: number;
- /** HTTP server host binding */
- authHost?: string;
/** Public base URL for callbacks */
baseUrl?: string;
/** Database connection string */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c9de313..25c4155 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -30,7 +30,7 @@ importers:
version: 7.3.0
'@prisma/client':
specifier: ^7.3.0
- version: 7.3.0(prisma@7.3.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
+ version: 7.3.0(prisma@7.4.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
'@signet/types':
specifier: workspace:*
version: link:../../packages/signet-types
@@ -67,9 +67,6 @@ importers:
nostr-tools:
specifier: ^2.22.1
version: 2.22.1(typescript@5.9.3)
- prisma:
- specifier: ^7.3.0
- version: 7.3.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
qrcode:
specifier: ^1.5.4
version: 1.5.4
@@ -95,6 +92,9 @@ importers:
'@vitest/coverage-v8':
specifier: ^4.0.18
version: 4.0.18(vitest@4.0.18(@types/node@20.19.23)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(yaml@2.8.1))
+ prisma:
+ specifier: ^7.4.0
+ version: 7.4.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.19.23)(typescript@5.9.3)
@@ -113,9 +113,15 @@ importers:
'@signet/types':
specifier: workspace:*
version: link:../../packages/signet-types
+ basic-auth:
+ specifier: ^2.0.1
+ version: 2.0.1
debug:
specifier: ^4.3.4
version: 4.4.3
+ dotenv:
+ specifier: ^16.6.1
+ version: 16.6.1
express:
specifier: ^4.22.1
version: 4.22.1
@@ -695,8 +701,8 @@ packages:
typescript:
optional: true
- '@prisma/config@7.3.0':
- resolution: {integrity: sha512-QyMV67+eXF7uMtKxTEeQqNu/Be7iH+3iDZOQZW5ttfbSwBamCSdwPszA0dum+Wx27I7anYTPLmRmMORKViSW1A==}
+ '@prisma/config@7.4.0':
+ resolution: {integrity: sha512-EnNrZMwZ9+O6UlG+YO9SP3VhVw4zwMahDRzQm3r0DQn9KeU5NwzmaDAY+BzACrgmaU71Id1/0FtWIDdl7xQp9g==}
'@prisma/debug@7.2.0':
resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==}
@@ -704,26 +710,29 @@ packages:
'@prisma/debug@7.3.0':
resolution: {integrity: sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg==}
+ '@prisma/debug@7.4.0':
+ resolution: {integrity: sha512-fZicwzgFHvvPMrRLCUinrsBTdadJsi/1oirzShjmFvNLwtu2DYlkxwRVy5zEGhp85mrEGnLeS/PdNRCdE027+Q==}
+
'@prisma/dev@0.20.0':
resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==}
'@prisma/driver-adapter-utils@7.3.0':
resolution: {integrity: sha512-Wdlezh1ck0Rq2dDINkfSkwbR53q53//Eo1vVqVLwtiZ0I6fuWDGNPxwq+SNAIHnsU+FD/m3aIJKevH3vF13U3w==}
- '@prisma/engines-version@7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735':
- resolution: {integrity: sha512-IH2va2ouUHihyiTTRW889LjKAl1CusZOvFfZxCDNpjSENt7g2ndFsK0vdIw/72v7+jCN6YgkHmdAP/BI7SDgyg==}
+ '@prisma/engines-version@7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57':
+ resolution: {integrity: sha512-5o3/bubIYdUeg38cyNf+VDq+LVtxvvi2393Fd1Uru52LPfkGJnmVbCaX1wBOAncgKR3BCloMJFD+Koog9LtYqQ==}
- '@prisma/engines@7.3.0':
- resolution: {integrity: sha512-cWRQoPDXPtR6stOWuWFZf9pHdQ/o8/QNWn0m0zByxf5Kd946Q875XdEJ52pEsX88vOiXUmjuPG3euw82mwQNMg==}
+ '@prisma/engines@7.4.0':
+ resolution: {integrity: sha512-H+dgpbbY3VN/j5hOSVP1LXsv/rU0w/4C2zh5PZUwo/Q3NqZjOvBlVvkhtziioRmeEZ3SBAqPCsf1sQ74sI3O/w==}
- '@prisma/fetch-engine@7.3.0':
- resolution: {integrity: sha512-Mm0F84JMqM9Vxk70pzfNpGJ1lE4hYjOeLMu7nOOD1i83nvp8MSAcFYBnHqLvEZiA6onUR+m8iYogtOY4oPO5lQ==}
+ '@prisma/fetch-engine@7.4.0':
+ resolution: {integrity: sha512-IXPOYskT89UTVsntuSnMTiKRWCuTg5JMWflgEDV1OSKFpuhwP5vqbfF01/iwo9y6rCjR0sDIO+jdV5kq38/hgA==}
'@prisma/get-platform@7.2.0':
resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==}
- '@prisma/get-platform@7.3.0':
- resolution: {integrity: sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg==}
+ '@prisma/get-platform@7.4.0':
+ resolution: {integrity: sha512-fOUIoGzAPgtjHVs4DsVSnEDPBEauAmFeZr4Ej3tMwxywam7hHdRtCzgKagQBKcYIJuya8gzYrTqUoukzXtWJaA==}
'@prisma/query-plan-executor@7.2.0':
resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==}
@@ -1196,6 +1205,10 @@ packages:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
+ basic-auth@2.0.1:
+ resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
+ engines: {node: '>= 0.8'}
+
bcrypt@6.0.0:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'}
@@ -1928,6 +1941,7 @@ packages:
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
globals@14.0.0:
@@ -2675,8 +2689,8 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- prisma@7.3.0:
- resolution: {integrity: sha512-ApYSOLHfMN8WftJA+vL6XwAPOh/aZ0BgUyyKPwUFgjARmG6EBI9LzDPf6SWULQMSAxydV9qn5gLj037nPNlg2w==}
+ prisma@7.4.0:
+ resolution: {integrity: sha512-n2xU9vSaH4uxZF/l2aKoGYtKtC7BL936jM9Q94Syk1zOD39t/5hjDUxMgaPkVRDX5wWEMsIqvzQxoebNIesOKw==}
engines: {node: ^20.19 || ^22.12 || >=24.0}
hasBin: true
peerDependencies:
@@ -2837,6 +2851,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -3918,14 +3935,14 @@ snapshots:
'@prisma/client-runtime-utils@7.3.0': {}
- '@prisma/client@7.3.0(prisma@7.3.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)':
+ '@prisma/client@7.3.0(prisma@7.4.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
'@prisma/client-runtime-utils': 7.3.0
optionalDependencies:
- prisma: 7.3.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
+ prisma: 7.4.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
typescript: 5.9.3
- '@prisma/config@7.3.0':
+ '@prisma/config@7.4.0':
dependencies:
c12: 3.1.0
deepmerge-ts: 7.1.5
@@ -3938,6 +3955,8 @@ snapshots:
'@prisma/debug@7.3.0': {}
+ '@prisma/debug@7.4.0': {}
+
'@prisma/dev@0.20.0(typescript@5.9.3)':
dependencies:
'@electric-sql/pglite': 0.3.15
@@ -3964,28 +3983,28 @@ snapshots:
dependencies:
'@prisma/debug': 7.3.0
- '@prisma/engines-version@7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735': {}
+ '@prisma/engines-version@7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57': {}
- '@prisma/engines@7.3.0':
+ '@prisma/engines@7.4.0':
dependencies:
- '@prisma/debug': 7.3.0
- '@prisma/engines-version': 7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735
- '@prisma/fetch-engine': 7.3.0
- '@prisma/get-platform': 7.3.0
+ '@prisma/debug': 7.4.0
+ '@prisma/engines-version': 7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57
+ '@prisma/fetch-engine': 7.4.0
+ '@prisma/get-platform': 7.4.0
- '@prisma/fetch-engine@7.3.0':
+ '@prisma/fetch-engine@7.4.0':
dependencies:
- '@prisma/debug': 7.3.0
- '@prisma/engines-version': 7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735
- '@prisma/get-platform': 7.3.0
+ '@prisma/debug': 7.4.0
+ '@prisma/engines-version': 7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57
+ '@prisma/get-platform': 7.4.0
'@prisma/get-platform@7.2.0':
dependencies:
'@prisma/debug': 7.2.0
- '@prisma/get-platform@7.3.0':
+ '@prisma/get-platform@7.4.0':
dependencies:
- '@prisma/debug': 7.3.0
+ '@prisma/debug': 7.4.0
'@prisma/query-plan-executor@7.2.0': {}
@@ -4492,6 +4511,10 @@ snapshots:
baseline-browser-mapping@2.9.19: {}
+ basic-auth@2.0.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
bcrypt@6.0.0:
dependencies:
node-addon-api: 8.5.0
@@ -6127,11 +6150,11 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
- prisma@7.3.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
+ prisma@7.4.0(@types/react@19.2.10)(better-sqlite3@12.6.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
dependencies:
- '@prisma/config': 7.3.0
+ '@prisma/config': 7.4.0
'@prisma/dev': 0.20.0(typescript@5.9.3)
- '@prisma/engines': 7.3.0
+ '@prisma/engines': 7.4.0
'@prisma/studio-core': 0.13.1(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
mysql2: 3.15.3
postgres: 3.4.7
@@ -6314,6 +6337,8 @@ snapshots:
has-symbols: 1.1.0
isarray: 2.0.5
+ safe-buffer@5.1.2: {}
+
safe-buffer@5.2.1: {}
safe-push-apply@1.0.0: