From 36fcb0787837e56768bb424fc4e8a897d6db5749 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Tue, 6 Jan 2026 06:01:47 +0100 Subject: [PATCH 01/72] Update docker compose with external ports --- docker-compose.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c4c0eda..2df58a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,14 +9,14 @@ 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} ports: - - "${SIGNET_PORT:-3000}:3000" + - "${SIGNET_PORT:-100.113.147.36:3174}:3000" healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health >/dev/null 2>&1 || exit 1"] interval: 10s @@ -37,4 +37,8 @@ services: UI_HOST: ${UI_HOST:-0.0.0.0} DAEMON_URL: ${DAEMON_URL:-http://signet:3000} ports: - - "${UI_PORT:-4174}:${UI_PORT:-4174}" + - "${UI_PORT:-100.113.147.36:4174}:${UI_PORT:-4174}" + +volumes: + signet_config: {} + signet_db: {} \ No newline at end of file From fce4987e06a157c8818257f39c7b5349550a6ad7 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Tue, 6 Jan 2026 06:01:47 +0100 Subject: [PATCH 02/72] Update docker compose with external ports --- docker-compose.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8ebab12..2df58a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,16 +9,16 @@ 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} ports: - - "${SIGNET_PORT:-3000}:${SIGNET_PORT:-3000}" + - "${SIGNET_PORT:-100.113.147.36:3174}: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://localhost:3000/health >/dev/null 2>&1 || exit 1"] interval: 10s timeout: 5s retries: 6 @@ -35,6 +35,10 @@ services: environment: UI_PORT: ${UI_PORT:-4174} UI_HOST: ${UI_HOST:-0.0.0.0} - DAEMON_URL: ${DAEMON_URL:-http://signet:${SIGNET_PORT:-3000}} + DAEMON_URL: ${DAEMON_URL:-http://signet:3000} ports: - - "${UI_PORT:-4174}:${UI_PORT:-4174}" + - "${UI_PORT:-100.113.147.36:4174}:${UI_PORT:-4174}" + +volumes: + signet_config: {} + signet_db: {} \ No newline at end of file From 2e5efc6bfc749a11ac314ea593d1be7e770a0f6d Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:38:47 +0100 Subject: [PATCH 03/72] Add basic-auth package --- apps/signet-ui/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/signet-ui/package.json b/apps/signet-ui/package.json index 22922db..1cef580 100644 --- a/apps/signet-ui/package.json +++ b/apps/signet-ui/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@signet/types": "workspace:*", + "basic-auth": "^2.0.1", "debug": "^4.3.4", "express": "^4.19.2", "focus-trap-react": "^11.0.4", From 764d25ac585484d7d5ed77d05bff5ee4d4c0b09c Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:39:25 +0100 Subject: [PATCH 04/72] Pnpm lockfile with basic auth --- pnpm-lock.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f762615..1deaf4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,9 @@ 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 @@ -1609,6 +1612,10 @@ packages: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} 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'} @@ -3379,6 +3386,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==} @@ -5399,6 +5409,10 @@ snapshots: baseline-browser-mapping@2.9.11: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + bcrypt@6.0.0: dependencies: node-addon-api: 8.5.0 @@ -7446,6 +7460,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: From 36a6476b8d0c75c1d96af0835ca25165506e5e85 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:39:47 +0100 Subject: [PATCH 05/72] Run express server secured + add message to log when running in auth mode --- apps/signet-ui/server.mjs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 2ba651f..bdb54a5 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -2,6 +2,7 @@ import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; 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); @@ -13,6 +14,11 @@ const port = Number.parseInt(process.env.UI_PORT ?? process.env.PORT ?? '4174', const host = process.env.UI_HOST ?? process.env.HOST ?? '0.0.0.0'; const daemonUrl = process.env.DAEMON_URL ?? 'http://localhost:3000'; +// Basic auth configuration (disabled by default) +const authUsername = process.env.UI_AUTH_USERNAME; +const authPassword = process.env.UI_AUTH_PASSWORD; +const isAuthEnabled = authUsername && authPassword; + // Shared error handler for proxies const onProxyError = (err, req, res) => { if (res.headersSent) return; @@ -72,6 +78,20 @@ const apiProxy = createProxyMiddleware({ } }); +// Basic authentication middleware +if (isAuthEnabled) { + 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); @@ -86,5 +106,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 ${daemonUrl})${authStatus}`); }); From f8283ab6dc77653c4dd847a25004a409f3725454 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:40:06 +0100 Subject: [PATCH 06/72] Update .env example --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index dffd47e..90860c9 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,8 @@ AUTH_PORT=3000 # UI port (default: 4174) UI_PORT=4174 + +# UI Basic Authentication (disabled by default) +# Set both values to enable authentication +# UI_AUTH_USERNAME=admin +# UI_AUTH_PASSWORD=your_secure_password From f841fccf18c4fdfe4ab4639136771f6364fac121 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:41:08 +0100 Subject: [PATCH 07/72] Update docker compose --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2df58a1..fbf8d4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,8 @@ services: UI_PORT: ${UI_PORT:-4174} UI_HOST: ${UI_HOST:-0.0.0.0} DAEMON_URL: ${DAEMON_URL:-http://signet:3000} + UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} + UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: - "${UI_PORT:-100.113.147.36:4174}:${UI_PORT:-4174}" From 3018e9be9d9ac25de9cf66233df7f8350817a720 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:46:04 +0100 Subject: [PATCH 08/72] Update docker build command --- apps/signet-ui/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 582e88e..7bc878d 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -15,7 +15,7 @@ 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 http-proxy-middleware@3 basic-auth@2 # Environment variables (can be overridden at runtime) ENV UI_PORT=4174 From 626fef6444dadbdcf87c82e49189c02a57bc1086 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:48:53 +0100 Subject: [PATCH 09/72] Bring ports to standard --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fbf8d4a..2981142 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: SIGNET_HOST: ${SIGNET_HOST:-0.0.0.0} EXTERNAL_URL: ${EXTERNAL_URL:-http://localhost:4174} ports: - - "${SIGNET_PORT:-100.113.147.36:3174}:3000" + - "${SIGNET_PORT:-3000}:3000" healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health >/dev/null 2>&1 || exit 1"] interval: 10s @@ -39,7 +39,7 @@ services: UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: - - "${UI_PORT:-100.113.147.36:4174}:${UI_PORT:-4174}" + - "${UI_PORT:-4174}:${UI_PORT:-4174}" volumes: signet_config: {} From ee08753ded32f37dbefd08ea1ea1feaedde5a5c0 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:19:44 +0100 Subject: [PATCH 10/72] Add api token to config and types --- apps/signet/src/config/config.ts | 9 ++++++++- packages/signet-types/src/config/types.ts | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index ab59afb..6dbb9e1 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -35,6 +35,7 @@ export async function loadConfig(configPath: string): Promise { keys: {}, verbose: false, jwtSecret: generateSecret(32), + apiToken: generateSecret(32), allowedOrigins: [ 'http://localhost:4174', 'http://localhost:3000', @@ -44,7 +45,7 @@ export async function loadConfig(configPath: string): Promise { authPort: 3000, authHost: '0.0.0.0', baseUrl: 'http://localhost:4174', - requireAuth: false, + requireAuth: true, }; needsSave = true; } else { @@ -75,6 +76,12 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } + // Generate API token if not present + if (!config.apiToken) { + config.apiToken = generateSecret(32); + needsSave = true; + } + // Set default allowed origins if not present if (!config.allowedOrigins) { config.allowedOrigins = [ diff --git a/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index eb81af3..0a9b8c1 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -72,6 +72,8 @@ export interface ConfigFile { allowedOrigins?: string[]; /** Require authentication for API access (default: false for local use) */ requireAuth?: boolean; + /** API token for server-to-server authentication (e.g., UI proxy to daemon) */ + apiToken?: string; /** Kill switch configuration for remote admin commands */ killSwitch?: KillSwitchConfig; } From 55c4ddc32ab480ef922ac2d2251cb03f21b9398a Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:22:12 +0100 Subject: [PATCH 11/72] Pass api token to daemon --- apps/signet/src/daemon/run.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index 141226c..33cd467 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -603,6 +603,7 @@ class Daemon { host: this.config.authHost ?? process.env.SIGNET_HOST ?? process.env.AUTH_HOST ?? '0.0.0.0', baseUrl, jwtSecret: this.config.jwtSecret, + apiToken: this.config.apiToken, allowedOrigins: this.config.allowedOrigins ?? [], requireAuth: this.config.requireAuth ?? false, connectionManager: this.connectionManager, From 6c5165084329a6ddcd8dfaff9123d24d29f0d0ba Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:23:34 +0100 Subject: [PATCH 12/72] Extract API token from config to pass it from UI --- apps/signet-ui/server.mjs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index bdb54a5..2924a9d 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -2,6 +2,8 @@ import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { readFileSync, existsSync } from 'node:fs'; +import { homedir } from 'node:os'; import auth from 'basic-auth'; const __filename = fileURLToPath(import.meta.url); @@ -19,6 +21,23 @@ const authUsername = process.env.UI_AUTH_USERNAME; const authPassword = process.env.UI_AUTH_PASSWORD; const isAuthEnabled = authUsername && authPassword; +// Load API token from daemon config file +let apiToken = null; +const configPath = process.env.SIGNET_CONFIG ?? path.join(homedir(), '.signet', 'config.json'); +if (existsSync(configPath)) { + try { + const config = JSON.parse(readFileSync(configPath, 'utf8')); + apiToken = config.apiToken; + if (apiToken) { + console.log('✓ Loaded API token from daemon config'); + } + } catch (err) { + console.warn('⚠️ Failed to load daemon config:', err.message); + } +} else { + console.warn('⚠️ Daemon config not found at', configPath); +} + // Shared error handler for proxies const onProxyError = (err, req, res) => { if (res.headersSent) return; @@ -58,6 +77,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'; @@ -74,6 +96,11 @@ const apiProxy = createProxyMiddleware({ proxyTimeout: 10_000, pathFilter: apiPaths, on: { + proxyReq(proxyReq) { + if (apiToken) { + proxyReq.setHeader('X-API-Token', apiToken); + } + }, error: onProxyError } }); From bd7ea4d22002ff8e34d9375cf4d82707d1cdec7e Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:24:21 +0100 Subject: [PATCH 13/72] Extend daemon middleware to also accept apiToken --- apps/signet/src/daemon/lib/auth.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/signet/src/daemon/lib/auth.ts b/apps/signet/src/daemon/lib/auth.ts index 6329bcc..cfe3d76 100644 --- a/apps/signet/src/daemon/lib/auth.ts +++ b/apps/signet/src/daemon/lib/auth.ts @@ -246,8 +246,13 @@ export function sanitizeCallbackUrl(url: string | null | undefined): string | nu * Create authentication middleware for protected routes * @param fastify - Fastify instance * @param requireAuth - If false, skip authentication (for local-only deployments) + * @param apiToken - Optional API token for server-to-server authentication */ -export function createAuthMiddleware(fastify: FastifyInstance, requireAuth: boolean = true) { +export function createAuthMiddleware( + fastify: FastifyInstance, + requireAuth: boolean = true, + apiToken?: string +) { return async function authMiddleware( request: FastifyRequest, reply: FastifyReply @@ -257,6 +262,16 @@ export function createAuthMiddleware(fastify: FastifyInstance, requireAuth: bool return; } + // Check for API token in X-API-Token header (for UI proxy) + if (apiToken) { + const requestApiToken = request.headers['x-api-token'] as string | undefined; + if (requestApiToken && timingSafeEqual(requestApiToken, apiToken)) { + // Valid API token - allow access + return; + } + } + + // Fall back to JWT token validation const payload = await verifyToken(fastify, request); if (!payload) { From 2cebac2f221109f37a4835ac8bfd9e88230e52f5 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:28:31 +0100 Subject: [PATCH 14/72] Update HttpServerConfig type --- apps/signet/src/daemon/http/server.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/signet/src/daemon/http/server.ts b/apps/signet/src/daemon/http/server.ts index 88de718..793c7cf 100644 --- a/apps/signet/src/daemon/http/server.ts +++ b/apps/signet/src/daemon/http/server.ts @@ -41,6 +41,7 @@ export interface HttpServerConfig { host: string; baseUrl?: string; jwtSecret?: string; + apiToken?: string; allowedOrigins: string[]; requireAuth: boolean; connectionManager: ConnectionManager; @@ -123,7 +124,11 @@ export class HttpServer { } private async setupRoutes(): Promise { - const authMiddleware = createAuthMiddleware(this.fastify, this.config.requireAuth); + const authMiddleware = createAuthMiddleware( + this.fastify, + this.config.requireAuth, + this.config.apiToken + ); const csrfMiddleware = createCsrfMiddleware(); const rateLimitAuth = createRateLimitMiddleware('auth'); const rateLimitKeys = createRateLimitMiddleware('keys'); From 28e660aa140342e6c4d02f2c1976d0ccd52fed57 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 17:44:40 +0100 Subject: [PATCH 15/72] Fix the hover background color of 'generate key' --- apps/signet-ui/src/design-system.css | 1 + 1 file changed, 1 insertion(+) 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); From 09a6a0eaaa95838cab9037326e99a458a4c4d28d Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:15:53 +0100 Subject: [PATCH 16/72] Add dotenv dependency to add support for .env file --- apps/signet-ui/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/apps/signet-ui/package.json b/apps/signet-ui/package.json index 1cef580..8266a74 100644 --- a/apps/signet-ui/package.json +++ b/apps/signet-ui/package.json @@ -18,6 +18,7 @@ "@signet/types": "workspace:*", "basic-auth": "^2.0.1", "debug": "^4.3.4", + "dotenv": "^16.6.1", "express": "^4.19.2", "focus-trap-react": "^11.0.4", "html5-qrcode": "^2.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1deaf4a..44e3990 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: debug: specifier: ^4.3.4 version: 4.4.3 + dotenv: + specifier: ^16.6.1 + version: 16.6.1 express: specifier: ^4.19.2 version: 4.21.2 From 04211eadc748af8b1be5899e77fe96503b9fbb93 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:16:24 +0100 Subject: [PATCH 17/72] Load .env in daemon and UI --- apps/signet-ui/server.mjs | 15 +++++++++++---- apps/signet/src/daemon/index.ts | 6 ++++++ apps/signet/src/index.ts | 6 ++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 2924a9d..7644bc1 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -1,20 +1,27 @@ +import { config as dotenvConfig } from 'dotenv'; import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + import { readFileSync, existsSync } from 'node:fs'; import { homedir } from 'node:os'; import auth from 'basic-auth'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +// Load .env from repository root (two levels up) +dotenvConfig({ path: path.resolve(__dirname, '../../.env') }); 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 host = process.env.UI_BIND_HOST ?? '0.0.0.0'; +const signetHost = process.env.SIGNET_HOST ?? 'localhost'; +const signetPort = process.env.SIGNET_PORT ?? '3000'; +const daemonUrl = process.env.DAEMON_URL ?? `http://${signetHost}:${signetPort}`; // Basic auth configuration (disabled by default) const authUsername = process.env.UI_AUTH_USERNAME; diff --git a/apps/signet/src/daemon/index.ts b/apps/signet/src/daemon/index.ts index da70901..4336224 100644 --- a/apps/signet/src/daemon/index.ts +++ b/apps/signet/src/daemon/index.ts @@ -1,3 +1,9 @@ +import { config as dotenvConfig } from 'dotenv'; +import { resolve } from 'path'; + +// Load .env from repository root (three levels up from this file's location) +dotenvConfig({ path: resolve(__dirname, '../../../../.env') }); + import 'websocket-polyfill'; import { runDaemon } from './run.js'; import type { DaemonBootstrapConfig } from './types.js'; diff --git a/apps/signet/src/index.ts b/apps/signet/src/index.ts index e6e5e30..ea38856 100644 --- a/apps/signet/src/index.ts +++ b/apps/signet/src/index.ts @@ -1,7 +1,13 @@ #!/usr/bin/env node +import { config as dotenvConfig } from 'dotenv'; +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) +dotenvConfig({ path: resolve(__dirname, '../../../.env') }); + import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { addKey } from './commands/add.js'; From 44ddfc977e9ea3f7fbf6d8e31fb3bb0b43101c95 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:22:39 +0100 Subject: [PATCH 18/72] Drop authHost from project --- apps/signet/src/config/config.ts | 7 ---- docs/CONFIGURATION.md | 42 ++++++++++++++++++----- packages/signet-types/src/config/types.ts | 2 -- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index 6dbb9e1..4cf15a8 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -43,7 +43,6 @@ export async function loadConfig(configPath: string): Promise { 'http://127.0.0.1:3000', ], authPort: 3000, - authHost: '0.0.0.0', baseUrl: 'http://localhost:4174', requireAuth: true, }; @@ -99,12 +98,6 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } - // Set default authHost if not present - if (config.authHost === undefined) { - config.authHost = '0.0.0.0'; - needsSave = true; - } - // Set default baseUrl if not present (for authorization redirects) if (config.baseUrl === undefined) { config.baseUrl = 'http://localhost:4174'; diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 70fa932..9e377fc 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -20,7 +20,6 @@ All runtime settings live in `signet.json`, located at `~/.signet-config/signet. "secret": "auto-generated-256-bit" }, "authPort": 3000, - "authHost": "0.0.0.0", "baseUrl": "http://localhost:4174", "database": "sqlite://signet.db", "logs": "./signet.log", @@ -59,7 +58,7 @@ Keys are encrypted using AES-256-GCM with PBKDF2 key derivation (600,000 iterati 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 @@ -183,16 +182,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 +DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build ``` ### Daemon Variables (`signet`) | Variable | Description | Default | |----------|-------------|---------| +| `SIGNET_BIND_HOST` | 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 `EXTERNAL_URL` if not set) | `localhost` | +| `UI_PORT` | Port where UI is accessible (used for `EXTERNAL_URL` if not set) | `4174` | +| `EXTERNAL_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` | @@ -201,13 +207,31 @@ SIGNET_PORT=3001 UI_PORT=8080 EXTERNAL_URL=https://signet.example.com docker com | Variable | Description | Default | |----------|-------------|---------| +| `UI_BIND_HOST` | 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 `DAEMON_URL` if not set) | `localhost` (or `signet` in Docker) | +| `SIGNET_PORT` | Port where daemon is accessible (used for `DAEMON_URL` if not set) | `3000` | +| `DAEMON_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 `DAEMON_URL` to proxy API requests to the daemon +- **Daemon → User**: The daemon uses `EXTERNAL_URL` to send authorization redirect URLs + +**Network binding:** +- Both services bind to `0.0.0.0` (all interfaces) by default +- Use `SIGNET_BIND_HOST` and `UI_BIND_HOST` 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_HOST=100.101.102.103 UI_BIND_HOST=100.101.102.103 docker compose up + +# Localhost only (not accessible from network) +SIGNET_BIND_HOST=127.0.0.1 UI_BIND_HOST=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/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index 0a9b8c1..82015af 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -54,8 +54,6 @@ export interface ConfigFile { 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 */ From 7461eb610822ca1f51ef3ad52383d603022c6469 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:26:19 +0100 Subject: [PATCH 19/72] Drop authPort from project --- apps/signet/src/config/config.ts | 7 ------- apps/signet/src/daemon/run.ts | 17 ++++++----------- docs/CONFIGURATION.md | 1 - packages/signet-types/src/config/types.ts | 2 -- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index 4cf15a8..b808210 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -42,7 +42,6 @@ export async function loadConfig(configPath: string): Promise { 'http://127.0.0.1:4174', 'http://127.0.0.1:3000', ], - authPort: 3000, baseUrl: 'http://localhost:4174', requireAuth: true, }; @@ -92,12 +91,6 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } - // Set default authPort if not present (enables HTTP server) - if (config.authPort === undefined) { - config.authPort = 3000; - needsSave = true; - } - // Set default baseUrl if not present (for authorization redirects) if (config.baseUrl === undefined) { config.baseUrl = 'http://localhost:4174'; diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index 33cd467..b4e3fa6 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -589,18 +589,13 @@ 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) { - console.log('No authPort configured, HTTP server disabled'); - return; - } - + const port = process.env.SIGNET_PORT ? parseInt(process.env.SIGNET_PORT) : 3000; const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; - console.log(`Starting HTTP server on port ${authPort}...`); + const bindHost = process.env.SIGNET_BIND_HOST ?? '0.0.0.0'; + console.log(`Starting HTTP server on ${bindHost}:${port}...`); 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: this.config.apiToken, @@ -618,7 +613,7 @@ class Daemon { }); await this.httpServer.start(); - await printServerInfo(authPort); + await printServerInfo(port); } private loadKeyMaterial(keyName: string, nsec: string): void { diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9e377fc..dd641bf 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -19,7 +19,6 @@ All runtime settings live in `signet.json`, located at `~/.signet-config/signet. "key": "auto-generated", "secret": "auto-generated-256-bit" }, - "authPort": 3000, "baseUrl": "http://localhost:4174", "database": "sqlite://signet.db", "logs": "./signet.log", diff --git a/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index 82015af..dc6e347 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -52,8 +52,6 @@ export interface ConfigFile { nostr: NostrConfig; /** Admin interface configuration */ admin: AdminConfig; - /** HTTP server port for REST API */ - authPort?: number; /** Public base URL for callbacks */ baseUrl?: string; /** Database connection string */ From 35f06c43e0ec5cd62c283294dba00f7a716b63f7 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:29:55 +0100 Subject: [PATCH 20/72] Update .env.example --- .env.example | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 90860c9..4b2db2f 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,18 @@ # 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: 3000) +SIGNET_PORT=3000 +# 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_HOST=0.0.0.0 # UI port (default: 4174) UI_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_HOST=0.0.0.0 # UI Basic Authentication (disabled by default) # Set both values to enable authentication From 8e4b2eeedd5778bf5c89a57631169f79cd339392 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:40:19 +0100 Subject: [PATCH 21/72] Improve naming from _HOST to _ADDRESS --- apps/signet-ui/server.mjs | 2 +- apps/signet/src/daemon/run.ts | 2 +- docker-compose.yml | 8 ++++---- docs/CONFIGURATION.md | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 7644bc1..ee2e988 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -18,7 +18,7 @@ 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_BIND_HOST ?? '0.0.0.0'; +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 daemonUrl = process.env.DAEMON_URL ?? `http://${signetHost}:${signetPort}`; diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index b4e3fa6..5f91d2a 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -591,7 +591,7 @@ class Daemon { // Support both new (SIGNET_*) and legacy (AUTH_*) env var names const port = process.env.SIGNET_PORT ? parseInt(process.env.SIGNET_PORT) : 3000; const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; - const bindHost = process.env.SIGNET_BIND_HOST ?? '0.0.0.0'; + const bindHost = process.env.SIGNET_BIND_ADDRESS ?? '0.0.0.0'; console.log(`Starting HTTP server on ${bindHost}:${port}...`); this.httpServer = new HttpServer({ host: bindHost, diff --git a/docker-compose.yml b/docker-compose.yml index 2981142..86b8b21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,8 +13,8 @@ services: 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_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} + EXTERNAL_URL: ${EXTERNAL_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} ports: - "${SIGNET_PORT:-3000}:3000" healthcheck: @@ -34,8 +34,8 @@ services: condition: service_healthy environment: UI_PORT: ${UI_PORT:-4174} - UI_HOST: ${UI_HOST:-0.0.0.0} - DAEMON_URL: ${DAEMON_URL:-http://signet:3000} + UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} + DAEMON_URL: ${DAEMON_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index dd641bf..b01ee6b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -192,7 +192,7 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c | Variable | Description | Default | |----------|-------------|---------| -| `SIGNET_BIND_HOST` | Network interface to bind to | `0.0.0.0` (all interfaces) | +| `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` | | `UI_HOST` | Hostname where UI is accessible (used for `EXTERNAL_URL` if not set) | `localhost` | @@ -206,7 +206,7 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c | Variable | Description | Default | |----------|-------------|---------| -| `UI_BIND_HOST` | Network interface to bind to | `0.0.0.0` (all interfaces) | +| `UI_BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` (all interfaces) | | `UI_PORT` | Port for the React UI | `4174` | | `SIGNET_HOST` | Hostname where daemon is accessible (used for `DAEMON_URL` if not set) | `localhost` (or `signet` in Docker) | | `SIGNET_PORT` | Port where daemon is accessible (used for `DAEMON_URL` if not set) | `3000` | @@ -218,16 +218,16 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c **Network binding:** - Both services bind to `0.0.0.0` (all interfaces) by default -- Use `SIGNET_BIND_HOST` and `UI_BIND_HOST` to bind to specific interfaces (e.g., `127.0.0.1` for localhost only, or a Tailscale IP like `100.x.x.x`) +- 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 **Example use cases:** ```bash # Bind to Tailscale interface only -SIGNET_BIND_HOST=100.101.102.103 UI_BIND_HOST=100.101.102.103 docker compose up +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_HOST=127.0.0.1 UI_BIND_HOST=127.0.0.1 docker compose up +SIGNET_BIND_ADDRESS=127.0.0.1 UI_BIND_ADDRESS=127.0.0.1 docker compose up ``` > **Note:** The `authHost` config field is no longer used. From c33ab537ed9d7b455b45e9c05d320dfddde72228 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:43:57 +0100 Subject: [PATCH 22/72] Rename port to BIND_PORT too as we need an external port too --- apps/signet-ui/Dockerfile | 4 ++-- apps/signet-ui/server.mjs | 2 +- apps/signet/Dockerfile | 4 ++-- apps/signet/src/daemon/run.ts | 2 +- docker-compose.yml | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 7bc878d..1db69fc 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -18,8 +18,8 @@ COPY --from=build /app/apps/signet-ui/server.mjs ./server.mjs RUN npm install --no-save express@4 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 UI_BIND_PORT=4174 +ENV UI_BIND_ADDRESS=0.0.0.0 ENV DAEMON_URL=http://signet:3000 EXPOSE 4174 diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index ee2e988..6b19166 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -17,7 +17,7 @@ dotenvConfig({ path: path.resolve(__dirname, '../../.env') }); 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 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'; diff --git a/apps/signet/Dockerfile b/apps/signet/Dockerfile index db96e47..3d043f3 100644 --- a/apps/signet/Dockerfile +++ b/apps/signet/Dockerfile @@ -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/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index 5f91d2a..b794814 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -589,7 +589,7 @@ class Daemon { private async startWebAuth(): Promise { // Support both new (SIGNET_*) and legacy (AUTH_*) env var names - const port = process.env.SIGNET_PORT ? parseInt(process.env.SIGNET_PORT) : 3000; + const port = process.env.SIGNET_BIND_PORT ? parseInt(process.env.SIGNET_BIND_PORT) : 3000; const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; const bindHost = process.env.SIGNET_BIND_ADDRESS ?? '0.0.0.0'; console.log(`Starting HTTP server on ${bindHost}:${port}...`); diff --git a/docker-compose.yml b/docker-compose.yml index 86b8b21..5432049 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,11 +12,11 @@ services: - signet_config:/app/config environment: DATABASE_URL: ${DATABASE_URL:-file:/app/config/signet.db} - SIGNET_PORT: ${SIGNET_PORT:-3000} + SIGNET_BIND_PORT: ${SIGNET_BIND_PORT:-3000} SIGNET_BIND_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} EXTERNAL_URL: ${EXTERNAL_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} ports: - - "${SIGNET_PORT:-3000}:3000" + - "${SIGNET_BIND_PORT:-3000}:3000" healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health >/dev/null 2>&1 || exit 1"] interval: 10s @@ -33,13 +33,13 @@ services: signet: condition: service_healthy environment: - UI_PORT: ${UI_PORT:-4174} + UI_BIND_PORT: ${UI_BIND_PORT:-4174} UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} DAEMON_URL: ${DAEMON_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-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: {} From e7245ec39fff2eb7649c63f69b64655be8884a6b Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:54:22 +0100 Subject: [PATCH 23/72] Rename DAEMON_URL to SIGNET_URL --- apps/signet-ui/Dockerfile | 2 +- apps/signet-ui/server.mjs | 8 ++++---- docker-compose.yml | 2 +- docs/CONFIGURATION.md | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 1db69fc..1003e78 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -20,7 +20,7 @@ RUN npm install --no-save express@4 http-proxy-middleware@3 basic-auth@2 # Environment variables (can be overridden at runtime) ENV UI_BIND_PORT=4174 ENV UI_BIND_ADDRESS=0.0.0.0 -ENV DAEMON_URL=http://signet:3000 +ENV SIGNET_URL=http://signet:3000 EXPOSE 4174 CMD ["node", "server.mjs"] diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 6b19166..516d3b8 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -21,7 +21,7 @@ const port = Number.parseInt(process.env.UI_BIND_PORT ?? process.env.PORT ?? '41 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 daemonUrl = process.env.DAEMON_URL ?? `http://${signetHost}:${signetPort}`; +const signetUrl = process.env.SIGNET_URL ?? `http://${signetHost}:${signetPort}`; // Basic auth configuration (disabled by default) const authUsername = process.env.UI_AUTH_USERNAME; @@ -74,7 +74,7 @@ const apiPaths = [ // SSE proxy for /events endpoint (no timeout, streaming) const sseProxy = createProxyMiddleware({ - target: daemonUrl, + target: signetUrl, changeOrigin: true, proxyTimeout: 0, timeout: 0, @@ -98,7 +98,7 @@ const sseProxy = createProxyMiddleware({ // API proxy for standard endpoints const apiProxy = createProxyMiddleware({ - target: daemonUrl, + target: signetUrl, changeOrigin: true, proxyTimeout: 10_000, pathFilter: apiPaths, @@ -141,5 +141,5 @@ app.get('*', (_req, res) => { app.listen(port, host, () => { const authStatus = isAuthEnabled ? ' [Basic Auth Enabled]' : ''; - console.log(`Signet UI listening on http://${host}:${port} (proxying ${daemonUrl})${authStatus}`); + console.log(`Signet UI listening on http://${host}:${port} (proxying ${signetUrl})${authStatus}`); }); diff --git a/docker-compose.yml b/docker-compose.yml index 5432049..5350d0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: environment: UI_BIND_PORT: ${UI_BIND_PORT:-4174} UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} - DAEMON_URL: ${DAEMON_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} + SIGNET_URL: ${SIGNET_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index b01ee6b..57d5dff 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -185,7 +185,7 @@ Docker Compose works out of the box with no `.env` file required. To customize s SIGNET_PORT=3001 UI_PORT=8080 docker compose up --build # Or set explicit URLs for complex networking -DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build +SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build ``` ### Daemon Variables (`signet`) @@ -208,12 +208,12 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c |----------|-------------|---------| | `UI_BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` (all interfaces) | | `UI_PORT` | Port for the React UI | `4174` | -| `SIGNET_HOST` | Hostname where daemon is accessible (used for `DAEMON_URL` if not set) | `localhost` (or `signet` in Docker) | -| `SIGNET_PORT` | Port where daemon is accessible (used for `DAEMON_URL` if not set) | `3000` | -| `DAEMON_URL` | Internal URL to reach the daemon. Defaults to `http://${SIGNET_HOST}:${SIGNET_PORT}` | `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 `DAEMON_URL` to proxy API requests to the daemon +- **UI → Daemon**: The UI uses `SIGNET_URL` to proxy API requests to the daemon - **Daemon → User**: The daemon uses `EXTERNAL_URL` to send authorization redirect URLs **Network binding:** From 191e633263b509bec255cba796547685cefa3e11 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:00:16 +0100 Subject: [PATCH 24/72] Drop support for env.BASE_URL --- apps/signet/src/daemon/authorize.ts | 4 ++-- apps/signet/src/daemon/run.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/signet/src/daemon/authorize.ts b/apps/signet/src/daemon/authorize.ts index 154514f..b6673ed 100644 --- a/apps/signet/src/daemon/authorize.ts +++ b/apps/signet/src/daemon/authorize.ts @@ -134,8 +134,8 @@ async function resolveBaseUrl(connectionManager: ConnectionManager): Promise { // Support both new (SIGNET_*) and legacy (AUTH_*) env var names const port = process.env.SIGNET_BIND_PORT ? parseInt(process.env.SIGNET_BIND_PORT) : 3000; - const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; + 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}...`); this.httpServer = new HttpServer({ From f7975ef18631be9081f495b098ad430c04c00247 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:00:36 +0100 Subject: [PATCH 25/72] Replace EXTERNAL_URL with UI_URL --- docker-compose.yml | 2 +- docs/CONFIGURATION.md | 10 +++++----- docs/DEPLOYMENT.md | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5350d0f..175393d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: DATABASE_URL: ${DATABASE_URL:-file:/app/config/signet.db} SIGNET_BIND_PORT: ${SIGNET_BIND_PORT:-3000} SIGNET_BIND_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} - EXTERNAL_URL: ${EXTERNAL_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} + UI_URL: ${UI_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} ports: - "${SIGNET_BIND_PORT:-3000}:3000" healthcheck: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 57d5dff..f754d10 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -185,7 +185,7 @@ Docker Compose works out of the box with no `.env` file required. To customize s SIGNET_PORT=3001 UI_PORT=8080 docker compose up --build # Or set explicit URLs for complex networking -SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build +SIGNET_URL=http://signet.local:3000 UI_URL=https://ui.example.com docker compose up --build ``` ### Daemon Variables (`signet`) @@ -195,9 +195,9 @@ SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c | `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` | -| `UI_HOST` | Hostname where UI is accessible (used for `EXTERNAL_URL` if not set) | `localhost` | -| `UI_PORT` | Port where UI is accessible (used for `EXTERNAL_URL` if not set) | `4174` | -| `EXTERNAL_URL` | Public URL of the UI (for authorization flow). Defaults to `http://${UI_HOST}:${UI_PORT}` | `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` | @@ -214,7 +214,7 @@ SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c **How the services communicate:** - **UI → Daemon**: The UI uses `SIGNET_URL` to proxy API requests to the daemon -- **Daemon → User**: The daemon uses `EXTERNAL_URL` to send authorization redirect URLs +- **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 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 | From a5c1ee588ab70d0a7be875b86f8bed9c0a6c1148 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:13:40 +0100 Subject: [PATCH 26/72] Align docker-compose vars + update healthcheck to use bind variables --- docker-compose.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 175393d..ff6ea32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,11 +14,12 @@ services: DATABASE_URL: ${DATABASE_URL:-file:/app/config/signet.db} SIGNET_BIND_PORT: ${SIGNET_BIND_PORT:-3000} SIGNET_BIND_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} - UI_URL: ${UI_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} + SIGNET_URL: ${SIGNET_URL:-http://${SIGNET_BIND_PORT:-signet}:${SIGNET_BIND_ADDRESS:-3000}} + ports: - "${SIGNET_BIND_PORT:-3000}:3000" healthcheck: - test: ["CMD-SHELL", "wget -qO- http://localhost: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 @@ -35,7 +36,7 @@ services: environment: UI_BIND_PORT: ${UI_BIND_PORT:-4174} UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} - SIGNET_URL: ${SIGNET_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} + UI_URL: ${UI_URL:-http://${UI_BIND_PORT:-localhost}:${UI_BIND_ADDRESS:-4174}} UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: From bdee3ddb681eb3a38c098e45dbc1df5dbfc2f2f3 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:22:17 +0100 Subject: [PATCH 27/72] Load dotenv dynamically. And do not fail in production --- apps/signet-ui/server.mjs | 13 ++++++++++--- apps/signet/src/daemon/index.ts | 13 ++++++++++--- apps/signet/src/index.ts | 13 ++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 516d3b8..56fdd29 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -1,4 +1,3 @@ -import { config as dotenvConfig } from 'dotenv'; import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import path from 'node:path'; @@ -11,8 +10,16 @@ import { readFileSync, existsSync } from 'node:fs'; import { homedir } from 'node:os'; import auth from 'basic-auth'; -// Load .env from repository root (two levels up) -dotenvConfig({ path: path.resolve(__dirname, '../../.env') }); +// 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(); diff --git a/apps/signet/src/daemon/index.ts b/apps/signet/src/daemon/index.ts index 4336224..b7ad294 100644 --- a/apps/signet/src/daemon/index.ts +++ b/apps/signet/src/daemon/index.ts @@ -1,8 +1,15 @@ -import { config as dotenvConfig } from 'dotenv'; import { resolve } from 'path'; -// Load .env from repository root (three levels up from this file's location) -dotenvConfig({ path: resolve(__dirname, '../../../../.env') }); +// 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 { + const dotenv = await import('dotenv'); + dotenv.config({ path: resolve(__dirname, '../../../../.env') }); + } catch { + // dotenv not available, skip .env loading (production mode) + } +} import 'websocket-polyfill'; import { runDaemon } from './run.js'; diff --git a/apps/signet/src/index.ts b/apps/signet/src/index.ts index ea38856..978f2ae 100644 --- a/apps/signet/src/index.ts +++ b/apps/signet/src/index.ts @@ -1,12 +1,19 @@ #!/usr/bin/env node -import { config as dotenvConfig } from 'dotenv'; 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) -dotenvConfig({ path: resolve(__dirname, '../../../.env') }); +// 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 { + const dotenv = await import('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'; From 820d9b3f8234ec5148a339ac2f367a90700e14c5 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:25:38 +0100 Subject: [PATCH 28/72] Use require as top-level await requires ES2022 or newer --- apps/signet/src/daemon/index.ts | 3 ++- apps/signet/src/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/signet/src/daemon/index.ts b/apps/signet/src/daemon/index.ts index b7ad294..a33d7c7 100644 --- a/apps/signet/src/daemon/index.ts +++ b/apps/signet/src/daemon/index.ts @@ -4,7 +4,8 @@ import { resolve } from 'path'; // In production (NODE_ENV=production), dotenv may not be installed if (process.env.NODE_ENV !== 'production') { try { - const dotenv = await import('dotenv'); + // 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) diff --git a/apps/signet/src/index.ts b/apps/signet/src/index.ts index 978f2ae..0a7a14f 100644 --- a/apps/signet/src/index.ts +++ b/apps/signet/src/index.ts @@ -8,7 +8,8 @@ import { join } from 'path'; // In production (NODE_ENV=production), dotenv may not be installed if (process.env.NODE_ENV !== 'production') { try { - const dotenv = await import('dotenv'); + // 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) From 231f3befd49ed976cda52d50db0c09c914f01eef Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:28:50 +0100 Subject: [PATCH 29/72] Add signet_URL to UI (for proxying) --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index ff6ea32..b51abac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: 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: From e5dcfca53497f7c50a7f439f37853ce7cc4dc5dc Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:33:06 +0100 Subject: [PATCH 30/72] Replace config api token by env variable + update docs --- apps/signet-ui/server.mjs | 24 ++++++----------------- apps/signet/src/config/config.ts | 7 ------- apps/signet/src/daemon/run.ts | 11 ++++++++++- docs/CONFIGURATION.md | 23 ++++++++++++++++++++++ packages/signet-types/src/config/types.ts | 2 -- 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 56fdd29..9db10fb 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -2,14 +2,11 @@ import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; 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); -import { readFileSync, existsSync } from 'node:fs'; -import { homedir } from 'node:os'; -import auth from 'basic-auth'; - // 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') { @@ -35,21 +32,12 @@ const authUsername = process.env.UI_AUTH_USERNAME; const authPassword = process.env.UI_AUTH_PASSWORD; const isAuthEnabled = authUsername && authPassword; -// Load API token from daemon config file -let apiToken = null; -const configPath = process.env.SIGNET_CONFIG ?? path.join(homedir(), '.signet', 'config.json'); -if (existsSync(configPath)) { - try { - const config = JSON.parse(readFileSync(configPath, 'utf8')); - apiToken = config.apiToken; - if (apiToken) { - console.log('✓ Loaded API token from daemon config'); - } - } catch (err) { - console.warn('⚠️ Failed to load daemon config:', err.message); - } +// 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('⚠️ Daemon config not found at', configPath); + console.warn('⚠️ No SIGNET_API_TOKEN set - requests to daemon may fail if authentication is required'); } // Shared error handler for proxies diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index b808210..4483249 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -35,7 +35,6 @@ export async function loadConfig(configPath: string): Promise { keys: {}, verbose: false, jwtSecret: generateSecret(32), - apiToken: generateSecret(32), allowedOrigins: [ 'http://localhost:4174', 'http://localhost:3000', @@ -74,12 +73,6 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } - // Generate API token if not present - if (!config.apiToken) { - config.apiToken = generateSecret(32); - needsSave = true; - } - // Set default allowed origins if not present if (!config.allowedOrigins) { config.allowedOrigins = [ diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index c95bd15..6cb8db7 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -593,12 +593,21 @@ class Daemon { 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'); + } + this.httpServer = new HttpServer({ host: bindHost, port, baseUrl, jwtSecret: this.config.jwtSecret, - apiToken: this.config.apiToken, + apiToken, allowedOrigins: this.config.allowedOrigins ?? [], requireAuth: this.config.requireAuth ?? false, connectionManager: this.connectionManager, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f754d10..ac61928 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -130,6 +130,29 @@ 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 +``` + +The daemon validates this token in the `X-API-Token` header sent by the UI proxy. Without a matching token, requests from the UI proxy will be rejected when `requireAuth` is enabled. + ### `admin.secret` Secret included in the bunker connection URI. Used to validate connection attempts from NIP-46 clients. diff --git a/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index dc6e347..ff46d3f 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -68,8 +68,6 @@ export interface ConfigFile { allowedOrigins?: string[]; /** Require authentication for API access (default: false for local use) */ requireAuth?: boolean; - /** API token for server-to-server authentication (e.g., UI proxy to daemon) */ - apiToken?: string; /** Kill switch configuration for remote admin commands */ killSwitch?: KillSwitchConfig; } From 33a1bf6c02fa4a377d4a672019d17d03a53cd1bb Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:34:49 +0100 Subject: [PATCH 31/72] Update env example --- .env.example | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 4b2db2f..76e5a1e 100644 --- a/.env.example +++ b/.env.example @@ -2,19 +2,34 @@ # Copy to .env only if you need to change the defaults # Bind addresses (which network interface to listen on) -# Signet Daemon port (default: 3000) -SIGNET_PORT=3000 +# 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_HOST=0.0.0.0 +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_HOST=0.0.0.0 +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= From aa9c6ec2d057cbeba437e63c671de73c1a9a6270 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:44:29 +0100 Subject: [PATCH 32/72] Introduce proxy authentication middleware to not bypass existing JWT/CSRF/rate limiting --- apps/signet/src/daemon/http/server.ts | 32 +++++++++++--------- apps/signet/src/daemon/lib/auth.ts | 42 ++++++++++++++++++--------- docs/CONFIGURATION.md | 16 +++++++++- 3 files changed, 63 insertions(+), 27 deletions(-) diff --git a/apps/signet/src/daemon/http/server.ts b/apps/signet/src/daemon/http/server.ts index 793c7cf..1539dae 100644 --- a/apps/signet/src/daemon/http/server.ts +++ b/apps/signet/src/daemon/http/server.ts @@ -5,6 +5,7 @@ import Handlebars from 'handlebars'; import { registerAuthPlugins, createAuthMiddleware, + createProxyAuthMiddleware, createRateLimitMiddleware, createCsrfMiddleware, generateCsrfToken, @@ -124,11 +125,15 @@ export class HttpServer { } private async setupRoutes(): Promise { + // 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, - this.config.apiToken + this.config.requireAuth ); + const csrfMiddleware = createCsrfMiddleware(); const rateLimitAuth = createRateLimitMiddleware('auth'); const rateLimitKeys = createRateLimitMiddleware('keys'); @@ -146,7 +151,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 }); @@ -157,14 +163,14 @@ export class HttpServer { connectionManager: this.config.connectionManager, nostrConfig: this.config.nostrConfig, relayService: this.config.relayService, - }, { 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], }); @@ -173,7 +179,7 @@ export class HttpServer { registerKeysRoutes(this.fastify, { keyService: this.config.keyService, }, { - auth: [authMiddleware], + auth: [proxyAuthMiddleware, authMiddleware], csrf: [csrfMiddleware], rateLimit: [rateLimitKeys], }); @@ -182,25 +188,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], }); @@ -208,19 +214,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/lib/auth.ts b/apps/signet/src/daemon/lib/auth.ts index cfe3d76..2424abe 100644 --- a/apps/signet/src/daemon/lib/auth.ts +++ b/apps/signet/src/daemon/lib/auth.ts @@ -242,16 +242,40 @@ 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) - * @param apiToken - Optional API token for server-to-server authentication */ export function createAuthMiddleware( fastify: FastifyInstance, - requireAuth: boolean = true, - apiToken?: string + requireAuth: boolean = true ) { return async function authMiddleware( request: FastifyRequest, @@ -262,16 +286,7 @@ export function createAuthMiddleware( return; } - // Check for API token in X-API-Token header (for UI proxy) - if (apiToken) { - const requestApiToken = request.headers['x-api-token'] as string | undefined; - if (requestApiToken && timingSafeEqual(requestApiToken, apiToken)) { - // Valid API token - allow access - return; - } - } - - // Fall back to JWT token validation + // Require JWT token validation for user authentication const payload = await verifyToken(fastify, request); if (!payload) { @@ -398,6 +413,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/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index ac61928..4268f08 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -151,7 +151,21 @@ Set this token in your environment or `.env` file: SIGNET_API_TOKEN=your_generated_token_here ``` -The daemon validates this token in the `X-API-Token` header sent by the UI proxy. Without a matching token, requests from the UI proxy will be rejected when `requireAuth` is enabled. +**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` From 27e9fe55e22b24f98641938544b5c654940448cf Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:47:57 +0100 Subject: [PATCH 33/72] Add express rate limiter to protect Basic-Auth from bruce-force attempts --- apps/signet-ui/Dockerfile | 2 +- apps/signet-ui/package.json | 1 + apps/signet-ui/server.mjs | 20 +++++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 1003e78..5ba12d0 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -15,7 +15,7 @@ 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 basic-auth@2 +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_BIND_PORT=4174 diff --git a/apps/signet-ui/package.json b/apps/signet-ui/package.json index 8266a74..27d2125 100644 --- a/apps/signet-ui/package.json +++ b/apps/signet-ui/package.json @@ -20,6 +20,7 @@ "debug": "^4.3.4", "dotenv": "^16.6.1", "express": "^4.19.2", + "express-rate-limit": "^7.5.0", "focus-trap-react": "^11.0.4", "html5-qrcode": "^2.3.8", "http-proxy-middleware": "^3.0.5", diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 9db10fb..46ae071 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -1,5 +1,6 @@ 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'; @@ -32,6 +33,19 @@ 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) { @@ -107,8 +121,12 @@ const apiProxy = createProxyMiddleware({ } }); -// Basic authentication middleware +// 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); From 44c72e33a8b999d525bd2908dcaf5a2449aabd67 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:48:27 +0100 Subject: [PATCH 34/72] Update lockfile --- pnpm-lock.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44e3990..cd1bf95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,9 @@ importers: express: specifier: ^4.19.2 version: 4.21.2 + express-rate-limit: + specifier: ^7.5.0 + version: 7.5.1(express@4.21.2) focus-trap-react: specifier: ^11.0.4 version: 11.0.4(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2194,6 +2197,12 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -6168,6 +6177,10 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@7.5.1(express@4.21.2): + dependencies: + express: 4.21.2 + express@4.21.2: dependencies: accepts: 1.3.8 From ed4eff58e9aa1348dd4c47ef3cb1c8c232a3721b Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:49:43 +0100 Subject: [PATCH 35/72] Update author. You should also update LICENSE --- apps/signet/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/signet/package.json b/apps/signet/package.json index 73de649..c3d89a7 100644 --- a/apps/signet/package.json +++ b/apps/signet/package.json @@ -35,7 +35,7 @@ "keywords": [ "nostr" ], - "author": "pablof7z", + "author": "Letdown2491", "license": "MIT", "dependencies": { "@fastify/cookie": "^11.0.2", From e7e604802828c81988d662b2a7fb9b002c4dd5fd Mon Sep 17 00:00:00 2001 From: digitalbase Date: Tue, 6 Jan 2026 06:01:47 +0100 Subject: [PATCH 36/72] Update docker compose with external ports --- docker-compose.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8ebab12..2df58a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,16 +9,16 @@ 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} ports: - - "${SIGNET_PORT:-3000}:${SIGNET_PORT:-3000}" + - "${SIGNET_PORT:-100.113.147.36:3174}: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://localhost:3000/health >/dev/null 2>&1 || exit 1"] interval: 10s timeout: 5s retries: 6 @@ -35,6 +35,10 @@ services: environment: UI_PORT: ${UI_PORT:-4174} UI_HOST: ${UI_HOST:-0.0.0.0} - DAEMON_URL: ${DAEMON_URL:-http://signet:${SIGNET_PORT:-3000}} + DAEMON_URL: ${DAEMON_URL:-http://signet:3000} ports: - - "${UI_PORT:-4174}:${UI_PORT:-4174}" + - "${UI_PORT:-100.113.147.36:4174}:${UI_PORT:-4174}" + +volumes: + signet_config: {} + signet_db: {} \ No newline at end of file From 9413d6dd21328021f69e07893b6cba5c0bbe3471 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:38:47 +0100 Subject: [PATCH 37/72] Add basic-auth package --- apps/signet-ui/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/signet-ui/package.json b/apps/signet-ui/package.json index 80a00dd..9df6556 100644 --- a/apps/signet-ui/package.json +++ b/apps/signet-ui/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@signet/types": "workspace:*", + "basic-auth": "^2.0.1", "debug": "^4.3.4", "express": "^4.19.2", "focus-trap-react": "^11.0.4", From bce703fdd8801225a08934e0bc55fc8dd2e36f78 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:39:25 +0100 Subject: [PATCH 38/72] Pnpm lockfile with basic auth --- pnpm-lock.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f762615..1deaf4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,9 @@ 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 @@ -1609,6 +1612,10 @@ packages: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} 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'} @@ -3379,6 +3386,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==} @@ -5399,6 +5409,10 @@ snapshots: baseline-browser-mapping@2.9.11: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + bcrypt@6.0.0: dependencies: node-addon-api: 8.5.0 @@ -7446,6 +7460,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: From bb3f0945d8a5960e4dafc4e2858a8cc31a7b2045 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:39:47 +0100 Subject: [PATCH 39/72] Run express server secured + add message to log when running in auth mode --- apps/signet-ui/server.mjs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 75a6bf3..b2a62e2 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -2,6 +2,7 @@ import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; 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); @@ -13,6 +14,11 @@ const port = Number.parseInt(process.env.UI_PORT ?? process.env.PORT ?? '4174', const host = process.env.UI_HOST ?? process.env.HOST ?? '0.0.0.0'; const daemonUrl = process.env.DAEMON_URL ?? 'http://localhost:3000'; +// Basic auth configuration (disabled by default) +const authUsername = process.env.UI_AUTH_USERNAME; +const authPassword = process.env.UI_AUTH_PASSWORD; +const isAuthEnabled = authUsername && authPassword; + // Shared error handler for proxies const onProxyError = (err, req, res) => { if (res.headersSent) return; @@ -73,6 +79,20 @@ const apiProxy = createProxyMiddleware({ } }); +// Basic authentication middleware +if (isAuthEnabled) { + 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 +107,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 ${daemonUrl})${authStatus}`); }); From 884d596cd65f749ca21aee66e2dca6815fe66f7d Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:40:06 +0100 Subject: [PATCH 40/72] Update .env example --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index dffd47e..90860c9 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,8 @@ AUTH_PORT=3000 # UI port (default: 4174) UI_PORT=4174 + +# UI Basic Authentication (disabled by default) +# Set both values to enable authentication +# UI_AUTH_USERNAME=admin +# UI_AUTH_PASSWORD=your_secure_password From 97d4d24bb2f3031aa701a275b8fc524cfa45e727 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:41:08 +0100 Subject: [PATCH 41/72] Update docker compose --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2df58a1..fbf8d4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,8 @@ services: UI_PORT: ${UI_PORT:-4174} UI_HOST: ${UI_HOST:-0.0.0.0} DAEMON_URL: ${DAEMON_URL:-http://signet:3000} + UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} + UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: - "${UI_PORT:-100.113.147.36:4174}:${UI_PORT:-4174}" From 19555bfb29260b8c54c9eda5ce2c5d4154628e23 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:46:04 +0100 Subject: [PATCH 42/72] Update docker build command --- apps/signet-ui/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 582e88e..7bc878d 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -15,7 +15,7 @@ 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 http-proxy-middleware@3 basic-auth@2 # Environment variables (can be overridden at runtime) ENV UI_PORT=4174 From 9194263776238f3f89bbdc62efd90f303eed285d Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 15:48:53 +0100 Subject: [PATCH 43/72] Bring ports to standard --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fbf8d4a..2981142 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: SIGNET_HOST: ${SIGNET_HOST:-0.0.0.0} EXTERNAL_URL: ${EXTERNAL_URL:-http://localhost:4174} ports: - - "${SIGNET_PORT:-100.113.147.36:3174}:3000" + - "${SIGNET_PORT:-3000}:3000" healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health >/dev/null 2>&1 || exit 1"] interval: 10s @@ -39,7 +39,7 @@ services: UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: - - "${UI_PORT:-100.113.147.36:4174}:${UI_PORT:-4174}" + - "${UI_PORT:-4174}:${UI_PORT:-4174}" volumes: signet_config: {} From 1c0637de0246502f2f1a2cee6d9e4fb5182d738e Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:19:44 +0100 Subject: [PATCH 44/72] Add api token to config and types --- apps/signet/src/config/config.ts | 9 ++++++++- packages/signet-types/src/config/types.ts | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index ab59afb..6dbb9e1 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -35,6 +35,7 @@ export async function loadConfig(configPath: string): Promise { keys: {}, verbose: false, jwtSecret: generateSecret(32), + apiToken: generateSecret(32), allowedOrigins: [ 'http://localhost:4174', 'http://localhost:3000', @@ -44,7 +45,7 @@ export async function loadConfig(configPath: string): Promise { authPort: 3000, authHost: '0.0.0.0', baseUrl: 'http://localhost:4174', - requireAuth: false, + requireAuth: true, }; needsSave = true; } else { @@ -75,6 +76,12 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } + // Generate API token if not present + if (!config.apiToken) { + config.apiToken = generateSecret(32); + needsSave = true; + } + // Set default allowed origins if not present if (!config.allowedOrigins) { config.allowedOrigins = [ diff --git a/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index eb81af3..0a9b8c1 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -72,6 +72,8 @@ export interface ConfigFile { allowedOrigins?: string[]; /** Require authentication for API access (default: false for local use) */ requireAuth?: boolean; + /** API token for server-to-server authentication (e.g., UI proxy to daemon) */ + apiToken?: string; /** Kill switch configuration for remote admin commands */ killSwitch?: KillSwitchConfig; } From ac5eea52e4118b40b1c0a84adaab5973473aad9a Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:22:12 +0100 Subject: [PATCH 45/72] Pass api token to daemon --- apps/signet/src/daemon/run.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index a2beb24..67ca9d5 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -601,6 +601,7 @@ class Daemon { host: this.config.authHost ?? process.env.SIGNET_HOST ?? process.env.AUTH_HOST ?? '0.0.0.0', baseUrl, jwtSecret: this.config.jwtSecret, + apiToken: this.config.apiToken, allowedOrigins: this.config.allowedOrigins ?? [], requireAuth: this.config.requireAuth ?? false, connectionManager: this.connectionManager, From 3a24b472c429ff3d94596a5232ef40e101cd2468 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:23:34 +0100 Subject: [PATCH 46/72] Extract API token from config to pass it from UI --- apps/signet-ui/server.mjs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index b2a62e2..68d886e 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -2,6 +2,8 @@ import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { readFileSync, existsSync } from 'node:fs'; +import { homedir } from 'node:os'; import auth from 'basic-auth'; const __filename = fileURLToPath(import.meta.url); @@ -19,6 +21,23 @@ const authUsername = process.env.UI_AUTH_USERNAME; const authPassword = process.env.UI_AUTH_PASSWORD; const isAuthEnabled = authUsername && authPassword; +// Load API token from daemon config file +let apiToken = null; +const configPath = process.env.SIGNET_CONFIG ?? path.join(homedir(), '.signet', 'config.json'); +if (existsSync(configPath)) { + try { + const config = JSON.parse(readFileSync(configPath, 'utf8')); + apiToken = config.apiToken; + if (apiToken) { + console.log('✓ Loaded API token from daemon config'); + } + } catch (err) { + console.warn('⚠️ Failed to load daemon config:', err.message); + } +} else { + console.warn('⚠️ Daemon config not found at', configPath); +} + // Shared error handler for proxies const onProxyError = (err, req, res) => { if (res.headersSent) return; @@ -59,6 +78,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'; @@ -75,6 +97,11 @@ const apiProxy = createProxyMiddleware({ proxyTimeout: 10_000, pathFilter: apiPaths, on: { + proxyReq(proxyReq) { + if (apiToken) { + proxyReq.setHeader('X-API-Token', apiToken); + } + }, error: onProxyError } }); From abb2b0579a1772d07b41f085f5984cb316eadc7c Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:24:21 +0100 Subject: [PATCH 47/72] Extend daemon middleware to also accept apiToken --- apps/signet/src/daemon/lib/auth.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/signet/src/daemon/lib/auth.ts b/apps/signet/src/daemon/lib/auth.ts index cf880d4..db3a656 100644 --- a/apps/signet/src/daemon/lib/auth.ts +++ b/apps/signet/src/daemon/lib/auth.ts @@ -246,8 +246,13 @@ export function sanitizeCallbackUrl(url: string | null | undefined): string | nu * Create authentication middleware for protected routes * @param fastify - Fastify instance * @param requireAuth - If false, skip authentication (for local-only deployments) + * @param apiToken - Optional API token for server-to-server authentication */ -export function createAuthMiddleware(fastify: FastifyInstance, requireAuth: boolean = true) { +export function createAuthMiddleware( + fastify: FastifyInstance, + requireAuth: boolean = true, + apiToken?: string +) { return async function authMiddleware( request: FastifyRequest, reply: FastifyReply @@ -257,6 +262,16 @@ export function createAuthMiddleware(fastify: FastifyInstance, requireAuth: bool return; } + // Check for API token in X-API-Token header (for UI proxy) + if (apiToken) { + const requestApiToken = request.headers['x-api-token'] as string | undefined; + if (requestApiToken && timingSafeEqual(requestApiToken, apiToken)) { + // Valid API token - allow access + return; + } + } + + // Fall back to JWT token validation const payload = await verifyToken(fastify, request); if (!payload) { From 1b005efe0bbbdd086d13cfc4513944b972e455c3 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 16:28:31 +0100 Subject: [PATCH 48/72] Update HttpServerConfig type --- apps/signet/src/daemon/http/server.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/signet/src/daemon/http/server.ts b/apps/signet/src/daemon/http/server.ts index 74cec33..7dab119 100644 --- a/apps/signet/src/daemon/http/server.ts +++ b/apps/signet/src/daemon/http/server.ts @@ -44,6 +44,7 @@ export interface HttpServerConfig { host: string; baseUrl?: string; jwtSecret?: string; + apiToken?: string; allowedOrigins: string[]; requireAuth: boolean; connectionManager: ConnectionManager; @@ -126,7 +127,11 @@ export class HttpServer { } private async setupRoutes(): Promise { - const authMiddleware = createAuthMiddleware(this.fastify, this.config.requireAuth); + const authMiddleware = createAuthMiddleware( + this.fastify, + this.config.requireAuth, + this.config.apiToken + ); const csrfMiddleware = createCsrfMiddleware(); const rateLimitAuth = createRateLimitMiddleware('auth'); const rateLimitKeys = createRateLimitMiddleware('keys'); From 2a2c1a0adb7bb662f392b10146abc44529972faf Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 17:44:40 +0100 Subject: [PATCH 49/72] Fix the hover background color of 'generate key' --- apps/signet-ui/src/design-system.css | 1 + 1 file changed, 1 insertion(+) 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); From 8391b38235142d9a4d30d9b5aab8fd823eee15c1 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:15:53 +0100 Subject: [PATCH 50/72] Add dotenv dependency to add support for .env file --- apps/signet-ui/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/apps/signet-ui/package.json b/apps/signet-ui/package.json index 9df6556..28c05e6 100644 --- a/apps/signet-ui/package.json +++ b/apps/signet-ui/package.json @@ -18,6 +18,7 @@ "@signet/types": "workspace:*", "basic-auth": "^2.0.1", "debug": "^4.3.4", + "dotenv": "^16.6.1", "express": "^4.19.2", "focus-trap-react": "^11.0.4", "html5-qrcode": "^2.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1deaf4a..44e3990 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: debug: specifier: ^4.3.4 version: 4.4.3 + dotenv: + specifier: ^16.6.1 + version: 16.6.1 express: specifier: ^4.19.2 version: 4.21.2 From bce88ddbc3b5151bfd7ede160adc0fd5978e96c6 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:16:24 +0100 Subject: [PATCH 51/72] Load .env in daemon and UI --- apps/signet-ui/server.mjs | 15 +++++++++++---- apps/signet/src/daemon/index.ts | 6 ++++++ apps/signet/src/index.ts | 6 ++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 68d886e..2cbbb62 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -1,20 +1,27 @@ +import { config as dotenvConfig } from 'dotenv'; import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + import { readFileSync, existsSync } from 'node:fs'; import { homedir } from 'node:os'; import auth from 'basic-auth'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +// Load .env from repository root (two levels up) +dotenvConfig({ path: path.resolve(__dirname, '../../.env') }); 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 host = process.env.UI_BIND_HOST ?? '0.0.0.0'; +const signetHost = process.env.SIGNET_HOST ?? 'localhost'; +const signetPort = process.env.SIGNET_PORT ?? '3000'; +const daemonUrl = process.env.DAEMON_URL ?? `http://${signetHost}:${signetPort}`; // Basic auth configuration (disabled by default) const authUsername = process.env.UI_AUTH_USERNAME; diff --git a/apps/signet/src/daemon/index.ts b/apps/signet/src/daemon/index.ts index da70901..4336224 100644 --- a/apps/signet/src/daemon/index.ts +++ b/apps/signet/src/daemon/index.ts @@ -1,3 +1,9 @@ +import { config as dotenvConfig } from 'dotenv'; +import { resolve } from 'path'; + +// Load .env from repository root (three levels up from this file's location) +dotenvConfig({ path: resolve(__dirname, '../../../../.env') }); + import 'websocket-polyfill'; import { runDaemon } from './run.js'; import type { DaemonBootstrapConfig } from './types.js'; diff --git a/apps/signet/src/index.ts b/apps/signet/src/index.ts index e6e5e30..ea38856 100644 --- a/apps/signet/src/index.ts +++ b/apps/signet/src/index.ts @@ -1,7 +1,13 @@ #!/usr/bin/env node +import { config as dotenvConfig } from 'dotenv'; +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) +dotenvConfig({ path: resolve(__dirname, '../../../.env') }); + import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { addKey } from './commands/add.js'; From d58e8d81d26b867c0f88a06e229de04facba23dc Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:22:39 +0100 Subject: [PATCH 52/72] Drop authHost from project --- apps/signet/src/config/config.ts | 7 ---- docs/CONFIGURATION.md | 42 ++++++++++++++++++----- packages/signet-types/src/config/types.ts | 2 -- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index 6dbb9e1..4cf15a8 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -43,7 +43,6 @@ export async function loadConfig(configPath: string): Promise { 'http://127.0.0.1:3000', ], authPort: 3000, - authHost: '0.0.0.0', baseUrl: 'http://localhost:4174', requireAuth: true, }; @@ -99,12 +98,6 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } - // Set default authHost if not present - if (config.authHost === undefined) { - config.authHost = '0.0.0.0'; - needsSave = true; - } - // Set default baseUrl if not present (for authorization redirects) if (config.baseUrl === undefined) { config.baseUrl = 'http://localhost:4174'; diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 4cce431..df6938f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -20,7 +20,6 @@ All runtime settings live in `signet.json`, located at `~/.signet-config/signet. "secret": "auto-generated-256-bit" }, "authPort": 3000, - "authHost": "0.0.0.0", "baseUrl": "http://localhost:4174", "database": "sqlite://signet.db", "logs": "./signet.log", @@ -59,7 +58,7 @@ Keys are encrypted using AES-256-GCM with PBKDF2 key derivation (600,000 iterati 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 @@ -214,16 +213,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 +DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build ``` ### Daemon Variables (`signet`) | Variable | Description | Default | |----------|-------------|---------| +| `SIGNET_BIND_HOST` | 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 `EXTERNAL_URL` if not set) | `localhost` | +| `UI_PORT` | Port where UI is accessible (used for `EXTERNAL_URL` if not set) | `4174` | +| `EXTERNAL_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` | @@ -234,13 +240,31 @@ SIGNET_PORT=3001 UI_PORT=8080 EXTERNAL_URL=https://signet.example.com docker com | Variable | Description | Default | |----------|-------------|---------| +| `UI_BIND_HOST` | 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 `DAEMON_URL` if not set) | `localhost` (or `signet` in Docker) | +| `SIGNET_PORT` | Port where daemon is accessible (used for `DAEMON_URL` if not set) | `3000` | +| `DAEMON_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 `DAEMON_URL` to proxy API requests to the daemon +- **Daemon → User**: The daemon uses `EXTERNAL_URL` to send authorization redirect URLs + +**Network binding:** +- Both services bind to `0.0.0.0` (all interfaces) by default +- Use `SIGNET_BIND_HOST` and `UI_BIND_HOST` 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_HOST=100.101.102.103 UI_BIND_HOST=100.101.102.103 docker compose up + +# Localhost only (not accessible from network) +SIGNET_BIND_HOST=127.0.0.1 UI_BIND_HOST=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/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index 0a9b8c1..82015af 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -54,8 +54,6 @@ export interface ConfigFile { 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 */ From 5dec61e2f7dcd08a0869454a3a805cdd4827de8c Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:26:19 +0100 Subject: [PATCH 53/72] Drop authPort from project --- apps/signet/src/config/config.ts | 7 ------- apps/signet/src/daemon/run.ts | 17 ++++++----------- docs/CONFIGURATION.md | 1 - packages/signet-types/src/config/types.ts | 2 -- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index 4cf15a8..b808210 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -42,7 +42,6 @@ export async function loadConfig(configPath: string): Promise { 'http://127.0.0.1:4174', 'http://127.0.0.1:3000', ], - authPort: 3000, baseUrl: 'http://localhost:4174', requireAuth: true, }; @@ -92,12 +91,6 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } - // Set default authPort if not present (enables HTTP server) - if (config.authPort === undefined) { - config.authPort = 3000; - needsSave = true; - } - // Set default baseUrl if not present (for authorization redirects) if (config.baseUrl === undefined) { config.baseUrl = 'http://localhost:4174'; diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index 67ca9d5..5d9b623 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -587,18 +587,13 @@ 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_PORT ? parseInt(process.env.SIGNET_PORT) : 3000; const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; - logger.info('Starting HTTP server', { port: authPort }); + const bindHost = process.env.SIGNET_BIND_HOST ?? '0.0.0.0'; + logger.info(`Starting HTTP server on ${bindHost}:${port}...`); 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: this.config.apiToken, @@ -616,7 +611,7 @@ class Daemon { }); await this.httpServer.start(); - await printServerInfo(authPort); + await printServerInfo(port); } private loadKeyMaterial(keyName: string, nsec: string): void { diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index df6938f..9bc2ffd 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -19,7 +19,6 @@ All runtime settings live in `signet.json`, located at `~/.signet-config/signet. "key": "auto-generated", "secret": "auto-generated-256-bit" }, - "authPort": 3000, "baseUrl": "http://localhost:4174", "database": "sqlite://signet.db", "logs": "./signet.log", diff --git a/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index 82015af..dc6e347 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -52,8 +52,6 @@ export interface ConfigFile { nostr: NostrConfig; /** Admin interface configuration */ admin: AdminConfig; - /** HTTP server port for REST API */ - authPort?: number; /** Public base URL for callbacks */ baseUrl?: string; /** Database connection string */ From f21c25686b1fd25d0f3afe3e5ffb9799b917bb69 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:29:55 +0100 Subject: [PATCH 54/72] Update .env.example --- .env.example | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 90860c9..4b2db2f 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,18 @@ # 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: 3000) +SIGNET_PORT=3000 +# 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_HOST=0.0.0.0 # UI port (default: 4174) UI_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_HOST=0.0.0.0 # UI Basic Authentication (disabled by default) # Set both values to enable authentication From dc2bceefb87c8247755e3b1146bff3c5666e5b5b Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:40:19 +0100 Subject: [PATCH 55/72] Improve naming from _HOST to _ADDRESS --- apps/signet-ui/server.mjs | 2 +- apps/signet/src/daemon/run.ts | 2 +- docker-compose.yml | 8 ++++---- docs/CONFIGURATION.md | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 2cbbb62..dee2b6e 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -18,7 +18,7 @@ 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_BIND_HOST ?? '0.0.0.0'; +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 daemonUrl = process.env.DAEMON_URL ?? `http://${signetHost}:${signetPort}`; diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index 5d9b623..85a3097 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -589,7 +589,7 @@ class Daemon { // Support both new (SIGNET_*) and legacy (AUTH_*) env var names const port = process.env.SIGNET_PORT ? parseInt(process.env.SIGNET_PORT) : 3000; const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; - const bindHost = process.env.SIGNET_BIND_HOST ?? '0.0.0.0'; + const bindHost = process.env.SIGNET_BIND_ADDRESS ?? '0.0.0.0'; logger.info(`Starting HTTP server on ${bindHost}:${port}...`); this.httpServer = new HttpServer({ host: bindHost, diff --git a/docker-compose.yml b/docker-compose.yml index 2981142..86b8b21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,8 +13,8 @@ services: 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_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} + EXTERNAL_URL: ${EXTERNAL_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} ports: - "${SIGNET_PORT:-3000}:3000" healthcheck: @@ -34,8 +34,8 @@ services: condition: service_healthy environment: UI_PORT: ${UI_PORT:-4174} - UI_HOST: ${UI_HOST:-0.0.0.0} - DAEMON_URL: ${DAEMON_URL:-http://signet:3000} + UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} + DAEMON_URL: ${DAEMON_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9bc2ffd..368ac14 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -223,7 +223,7 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c | Variable | Description | Default | |----------|-------------|---------| -| `SIGNET_BIND_HOST` | Network interface to bind to | `0.0.0.0` (all interfaces) | +| `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` | | `UI_HOST` | Hostname where UI is accessible (used for `EXTERNAL_URL` if not set) | `localhost` | @@ -239,7 +239,7 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c | Variable | Description | Default | |----------|-------------|---------| -| `UI_BIND_HOST` | Network interface to bind to | `0.0.0.0` (all interfaces) | +| `UI_BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` (all interfaces) | | `UI_PORT` | Port for the React UI | `4174` | | `SIGNET_HOST` | Hostname where daemon is accessible (used for `DAEMON_URL` if not set) | `localhost` (or `signet` in Docker) | | `SIGNET_PORT` | Port where daemon is accessible (used for `DAEMON_URL` if not set) | `3000` | @@ -251,16 +251,16 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c **Network binding:** - Both services bind to `0.0.0.0` (all interfaces) by default -- Use `SIGNET_BIND_HOST` and `UI_BIND_HOST` to bind to specific interfaces (e.g., `127.0.0.1` for localhost only, or a Tailscale IP like `100.x.x.x`) +- 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 **Example use cases:** ```bash # Bind to Tailscale interface only -SIGNET_BIND_HOST=100.101.102.103 UI_BIND_HOST=100.101.102.103 docker compose up +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_HOST=127.0.0.1 UI_BIND_HOST=127.0.0.1 docker compose up +SIGNET_BIND_ADDRESS=127.0.0.1 UI_BIND_ADDRESS=127.0.0.1 docker compose up ``` > **Note:** The `authHost` config field is no longer used. From a170a7ae78123c3841123212d3373be28fde6f89 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:43:57 +0100 Subject: [PATCH 56/72] Rename port to BIND_PORT too as we need an external port too --- apps/signet-ui/Dockerfile | 4 ++-- apps/signet-ui/server.mjs | 2 +- apps/signet/Dockerfile | 4 ++-- apps/signet/src/daemon/run.ts | 2 +- docker-compose.yml | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 7bc878d..1db69fc 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -18,8 +18,8 @@ COPY --from=build /app/apps/signet-ui/server.mjs ./server.mjs RUN npm install --no-save express@4 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 UI_BIND_PORT=4174 +ENV UI_BIND_ADDRESS=0.0.0.0 ENV DAEMON_URL=http://signet:3000 EXPOSE 4174 diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index dee2b6e..d45ff2b 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -17,7 +17,7 @@ dotenvConfig({ path: path.resolve(__dirname, '../../.env') }); 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 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'; diff --git a/apps/signet/Dockerfile b/apps/signet/Dockerfile index db96e47..3d043f3 100644 --- a/apps/signet/Dockerfile +++ b/apps/signet/Dockerfile @@ -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/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index 85a3097..4c24ee9 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -587,7 +587,7 @@ class Daemon { private async startWebAuth(): Promise { // Support both new (SIGNET_*) and legacy (AUTH_*) env var names - const port = process.env.SIGNET_PORT ? parseInt(process.env.SIGNET_PORT) : 3000; + const port = process.env.SIGNET_BIND_PORT ? parseInt(process.env.SIGNET_BIND_PORT) : 3000; const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; const bindHost = process.env.SIGNET_BIND_ADDRESS ?? '0.0.0.0'; logger.info(`Starting HTTP server on ${bindHost}:${port}...`); diff --git a/docker-compose.yml b/docker-compose.yml index 86b8b21..5432049 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,11 +12,11 @@ services: - signet_config:/app/config environment: DATABASE_URL: ${DATABASE_URL:-file:/app/config/signet.db} - SIGNET_PORT: ${SIGNET_PORT:-3000} + SIGNET_BIND_PORT: ${SIGNET_BIND_PORT:-3000} SIGNET_BIND_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} EXTERNAL_URL: ${EXTERNAL_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} ports: - - "${SIGNET_PORT:-3000}:3000" + - "${SIGNET_BIND_PORT:-3000}:3000" healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health >/dev/null 2>&1 || exit 1"] interval: 10s @@ -33,13 +33,13 @@ services: signet: condition: service_healthy environment: - UI_PORT: ${UI_PORT:-4174} + UI_BIND_PORT: ${UI_BIND_PORT:-4174} UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} DAEMON_URL: ${DAEMON_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-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: {} From eb0ebdc6f64bb32604a7dfd31b78cb2de4223289 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 22:54:22 +0100 Subject: [PATCH 57/72] Rename DAEMON_URL to SIGNET_URL --- apps/signet-ui/Dockerfile | 2 +- apps/signet-ui/server.mjs | 8 ++++---- docker-compose.yml | 2 +- docs/CONFIGURATION.md | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 1db69fc..1003e78 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -20,7 +20,7 @@ RUN npm install --no-save express@4 http-proxy-middleware@3 basic-auth@2 # Environment variables (can be overridden at runtime) ENV UI_BIND_PORT=4174 ENV UI_BIND_ADDRESS=0.0.0.0 -ENV DAEMON_URL=http://signet:3000 +ENV SIGNET_URL=http://signet:3000 EXPOSE 4174 CMD ["node", "server.mjs"] diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index d45ff2b..22a2e62 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -21,7 +21,7 @@ const port = Number.parseInt(process.env.UI_BIND_PORT ?? process.env.PORT ?? '41 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 daemonUrl = process.env.DAEMON_URL ?? `http://${signetHost}:${signetPort}`; +const signetUrl = process.env.SIGNET_URL ?? `http://${signetHost}:${signetPort}`; // Basic auth configuration (disabled by default) const authUsername = process.env.UI_AUTH_USERNAME; @@ -75,7 +75,7 @@ const apiPaths = [ // SSE proxy for /events endpoint (no timeout, streaming) const sseProxy = createProxyMiddleware({ - target: daemonUrl, + target: signetUrl, changeOrigin: true, proxyTimeout: 0, timeout: 0, @@ -99,7 +99,7 @@ const sseProxy = createProxyMiddleware({ // API proxy for standard endpoints const apiProxy = createProxyMiddleware({ - target: daemonUrl, + target: signetUrl, changeOrigin: true, proxyTimeout: 10_000, pathFilter: apiPaths, @@ -142,5 +142,5 @@ app.get('*', (_req, res) => { app.listen(port, host, () => { const authStatus = isAuthEnabled ? ' [Basic Auth Enabled]' : ''; - console.log(`Signet UI listening on http://${host}:${port} (proxying ${daemonUrl})${authStatus}`); + console.log(`Signet UI listening on http://${host}:${port} (proxying ${signetUrl})${authStatus}`); }); diff --git a/docker-compose.yml b/docker-compose.yml index 5432049..5350d0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: environment: UI_BIND_PORT: ${UI_BIND_PORT:-4174} UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} - DAEMON_URL: ${DAEMON_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} + SIGNET_URL: ${SIGNET_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 368ac14..e9262b4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -216,7 +216,7 @@ Docker Compose works out of the box with no `.env` file required. To customize s SIGNET_PORT=3001 UI_PORT=8080 docker compose up --build # Or set explicit URLs for complex networking -DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build +SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build ``` ### Daemon Variables (`signet`) @@ -241,12 +241,12 @@ DAEMON_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c |----------|-------------|---------| | `UI_BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` (all interfaces) | | `UI_PORT` | Port for the React UI | `4174` | -| `SIGNET_HOST` | Hostname where daemon is accessible (used for `DAEMON_URL` if not set) | `localhost` (or `signet` in Docker) | -| `SIGNET_PORT` | Port where daemon is accessible (used for `DAEMON_URL` if not set) | `3000` | -| `DAEMON_URL` | Internal URL to reach the daemon. Defaults to `http://${SIGNET_HOST}:${SIGNET_PORT}` | `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 `DAEMON_URL` to proxy API requests to the daemon +- **UI → Daemon**: The UI uses `SIGNET_URL` to proxy API requests to the daemon - **Daemon → User**: The daemon uses `EXTERNAL_URL` to send authorization redirect URLs **Network binding:** From a05ca32a5c4d71126a21609c9792d1d3ba8bda48 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:00:16 +0100 Subject: [PATCH 58/72] Drop support for env.BASE_URL --- apps/signet/src/daemon/authorize.ts | 4 ++-- apps/signet/src/daemon/run.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 { // Support both new (SIGNET_*) and legacy (AUTH_*) env var names const port = process.env.SIGNET_BIND_PORT ? parseInt(process.env.SIGNET_BIND_PORT) : 3000; - const baseUrl = this.config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL; + const baseUrl = this.config.baseUrl ?? process.env.UI_URL; const bindHost = process.env.SIGNET_BIND_ADDRESS ?? '0.0.0.0'; logger.info(`Starting HTTP server on ${bindHost}:${port}...`); this.httpServer = new HttpServer({ From b2d25da111cd0a47d2add547a1fea8378a9fe48f Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:00:36 +0100 Subject: [PATCH 59/72] Replace EXTERNAL_URL with UI_URL --- docker-compose.yml | 2 +- docs/CONFIGURATION.md | 10 +++++----- docs/DEPLOYMENT.md | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5350d0f..175393d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: DATABASE_URL: ${DATABASE_URL:-file:/app/config/signet.db} SIGNET_BIND_PORT: ${SIGNET_BIND_PORT:-3000} SIGNET_BIND_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} - EXTERNAL_URL: ${EXTERNAL_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} + UI_URL: ${UI_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} ports: - "${SIGNET_BIND_PORT:-3000}:3000" healthcheck: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e9262b4..6f183fa 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -216,7 +216,7 @@ Docker Compose works out of the box with no `.env` file required. To customize s SIGNET_PORT=3001 UI_PORT=8080 docker compose up --build # Or set explicit URLs for complex networking -SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker compose up --build +SIGNET_URL=http://signet.local:3000 UI_URL=https://ui.example.com docker compose up --build ``` ### Daemon Variables (`signet`) @@ -226,9 +226,9 @@ SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c | `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` | -| `UI_HOST` | Hostname where UI is accessible (used for `EXTERNAL_URL` if not set) | `localhost` | -| `UI_PORT` | Port where UI is accessible (used for `EXTERNAL_URL` if not set) | `4174` | -| `EXTERNAL_URL` | Public URL of the UI (for authorization flow). Defaults to `http://${UI_HOST}:${UI_PORT}` | `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` | @@ -247,7 +247,7 @@ SIGNET_URL=http://signet.local:3000 EXTERNAL_URL=https://ui.example.com docker c **How the services communicate:** - **UI → Daemon**: The UI uses `SIGNET_URL` to proxy API requests to the daemon -- **Daemon → User**: The daemon uses `EXTERNAL_URL` to send authorization redirect URLs +- **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 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 | From 78574c758fe6a8390f2475c9ee1e1785300c9352 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:13:40 +0100 Subject: [PATCH 60/72] Align docker-compose vars + update healthcheck to use bind variables --- docker-compose.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 175393d..ff6ea32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,11 +14,12 @@ services: DATABASE_URL: ${DATABASE_URL:-file:/app/config/signet.db} SIGNET_BIND_PORT: ${SIGNET_BIND_PORT:-3000} SIGNET_BIND_ADDRESS: ${SIGNET_BIND_ADDRESS:-0.0.0.0} - UI_URL: ${UI_URL:-http://${UI_HOST:-localhost}:${UI_PORT:-4174}} + SIGNET_URL: ${SIGNET_URL:-http://${SIGNET_BIND_PORT:-signet}:${SIGNET_BIND_ADDRESS:-3000}} + ports: - "${SIGNET_BIND_PORT:-3000}:3000" healthcheck: - test: ["CMD-SHELL", "wget -qO- http://localhost: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 @@ -35,7 +36,7 @@ services: environment: UI_BIND_PORT: ${UI_BIND_PORT:-4174} UI_BIND_ADDRESS: ${UI_BIND_ADDRESS:-0.0.0.0} - SIGNET_URL: ${SIGNET_URL:-http://${SIGNET_HOST:-signet}:${SIGNET_PORT:-3000}} + UI_URL: ${UI_URL:-http://${UI_BIND_PORT:-localhost}:${UI_BIND_ADDRESS:-4174}} UI_AUTH_USERNAME: ${UI_AUTH_USERNAME:-} UI_AUTH_PASSWORD: ${UI_AUTH_PASSWORD:-} ports: From d043cb44c7cfc5769832b6ca58c87980fc2d89c6 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:22:17 +0100 Subject: [PATCH 61/72] Load dotenv dynamically. And do not fail in production --- apps/signet-ui/server.mjs | 13 ++++++++++--- apps/signet/src/daemon/index.ts | 13 ++++++++++--- apps/signet/src/index.ts | 13 ++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 22a2e62..12b7a6d 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -1,4 +1,3 @@ -import { config as dotenvConfig } from 'dotenv'; import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import path from 'node:path'; @@ -11,8 +10,16 @@ import { readFileSync, existsSync } from 'node:fs'; import { homedir } from 'node:os'; import auth from 'basic-auth'; -// Load .env from repository root (two levels up) -dotenvConfig({ path: path.resolve(__dirname, '../../.env') }); +// 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(); diff --git a/apps/signet/src/daemon/index.ts b/apps/signet/src/daemon/index.ts index 4336224..b7ad294 100644 --- a/apps/signet/src/daemon/index.ts +++ b/apps/signet/src/daemon/index.ts @@ -1,8 +1,15 @@ -import { config as dotenvConfig } from 'dotenv'; import { resolve } from 'path'; -// Load .env from repository root (three levels up from this file's location) -dotenvConfig({ path: resolve(__dirname, '../../../../.env') }); +// 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 { + const dotenv = await import('dotenv'); + dotenv.config({ path: resolve(__dirname, '../../../../.env') }); + } catch { + // dotenv not available, skip .env loading (production mode) + } +} import 'websocket-polyfill'; import { runDaemon } from './run.js'; diff --git a/apps/signet/src/index.ts b/apps/signet/src/index.ts index ea38856..978f2ae 100644 --- a/apps/signet/src/index.ts +++ b/apps/signet/src/index.ts @@ -1,12 +1,19 @@ #!/usr/bin/env node -import { config as dotenvConfig } from 'dotenv'; 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) -dotenvConfig({ path: resolve(__dirname, '../../../.env') }); +// 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 { + const dotenv = await import('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'; From a986944ae71033bda669896f6151915519f63fed Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:25:38 +0100 Subject: [PATCH 62/72] Use require as top-level await requires ES2022 or newer --- apps/signet/src/daemon/index.ts | 3 ++- apps/signet/src/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/signet/src/daemon/index.ts b/apps/signet/src/daemon/index.ts index b7ad294..a33d7c7 100644 --- a/apps/signet/src/daemon/index.ts +++ b/apps/signet/src/daemon/index.ts @@ -4,7 +4,8 @@ import { resolve } from 'path'; // In production (NODE_ENV=production), dotenv may not be installed if (process.env.NODE_ENV !== 'production') { try { - const dotenv = await import('dotenv'); + // 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) diff --git a/apps/signet/src/index.ts b/apps/signet/src/index.ts index 978f2ae..0a7a14f 100644 --- a/apps/signet/src/index.ts +++ b/apps/signet/src/index.ts @@ -8,7 +8,8 @@ import { join } from 'path'; // In production (NODE_ENV=production), dotenv may not be installed if (process.env.NODE_ENV !== 'production') { try { - const dotenv = await import('dotenv'); + // 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) From f5a59e29136c600739008c8b5c4a6fc5703e9eaa Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:28:50 +0100 Subject: [PATCH 63/72] Add signet_URL to UI (for proxying) --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index ff6ea32..b51abac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: 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: From 86ee4fce28ccc177b0060e64bdfec9f9a218e231 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:33:06 +0100 Subject: [PATCH 64/72] Replace config api token by env variable + update docs --- apps/signet-ui/server.mjs | 24 +--- apps/signet/src/config/config.ts | 7 - apps/signet/src/daemon/run.ts | 151 ++++++++++++---------- docs/CONFIGURATION.md | 23 ++++ packages/signet-types/src/config/types.ts | 2 - 5 files changed, 110 insertions(+), 97 deletions(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 12b7a6d..6f37022 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -2,14 +2,11 @@ import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; 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); -import { readFileSync, existsSync } from 'node:fs'; -import { homedir } from 'node:os'; -import auth from 'basic-auth'; - // 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') { @@ -35,21 +32,12 @@ const authUsername = process.env.UI_AUTH_USERNAME; const authPassword = process.env.UI_AUTH_PASSWORD; const isAuthEnabled = authUsername && authPassword; -// Load API token from daemon config file -let apiToken = null; -const configPath = process.env.SIGNET_CONFIG ?? path.join(homedir(), '.signet', 'config.json'); -if (existsSync(configPath)) { - try { - const config = JSON.parse(readFileSync(configPath, 'utf8')); - apiToken = config.apiToken; - if (apiToken) { - console.log('✓ Loaded API token from daemon config'); - } - } catch (err) { - console.warn('⚠️ Failed to load daemon config:', err.message); - } +// 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('⚠️ Daemon config not found at', configPath); + console.warn('⚠️ No SIGNET_API_TOKEN set - requests to daemon may fail if authentication is required'); } // Shared error handler for proxies diff --git a/apps/signet/src/config/config.ts b/apps/signet/src/config/config.ts index b808210..4483249 100644 --- a/apps/signet/src/config/config.ts +++ b/apps/signet/src/config/config.ts @@ -35,7 +35,6 @@ export async function loadConfig(configPath: string): Promise { keys: {}, verbose: false, jwtSecret: generateSecret(32), - apiToken: generateSecret(32), allowedOrigins: [ 'http://localhost:4174', 'http://localhost:3000', @@ -74,12 +73,6 @@ export async function loadConfig(configPath: string): Promise { needsSave = true; } - // Generate API token if not present - if (!config.apiToken) { - config.apiToken = generateSecret(32); - needsSave = true; - } - // Set default allowed origins if not present if (!config.allowedOrigins) { config.allowedOrigins = [ diff --git a/apps/signet/src/daemon/run.ts b/apps/signet/src/daemon/run.ts index 11757e6..6cb8db7 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -8,10 +8,6 @@ import { printServerInfo } from './lib/network.js'; import { requestAuthorization } from './authorize.js'; import type { DaemonBootstrapConfig } from './types.js'; import { checkRequestPermission, type RpcMethod, type ApprovalType } from './lib/acl.js'; -import { TTLCache, getAllCacheStats } from './lib/ttl-cache.js'; -import { extractEventKind } from './lib/parse.js'; -import { toErrorMessage } from './lib/errors.js'; -import { logger, setLogEntryEmitter } from './lib/logger.js'; import { KeyService, RequestService, @@ -54,21 +50,23 @@ try { // Catch unhandled errors to prevent silent crashes and provide visibility process.on('uncaughtException', (error: Error) => { - logger.error('Uncaught exception', { - error: error.message, - stack: error.stack, - }); + console.error('=== UNCAUGHT EXCEPTION ==='); + console.error('Time:', new Date().toISOString()); + console.error('Error:', error.message); + console.error('Stack:', error.stack); + console.error('=========================='); // Don't exit - let the process continue if possible // The error is logged and we can investigate }); -process.on('unhandledRejection', (reason: unknown, _promise: Promise) => { - const reasonStr = reason instanceof Error ? reason.message : String(reason); - const stack = reason instanceof Error ? reason.stack : undefined; - logger.error('Unhandled rejection', { - reason: reasonStr, - ...(stack ? { stack } : {}), - }); +process.on('unhandledRejection', (reason: unknown, promise: Promise) => { + console.error('=== UNHANDLED REJECTION ==='); + console.error('Time:', new Date().toISOString()); + console.error('Reason:', reason); + if (reason instanceof Error) { + console.error('Stack:', reason.stack); + } + console.error('==========================='); // Don't exit - just log for investigation }); @@ -81,35 +79,28 @@ const LOG_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days const HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes // Rate limiting for auto-approval logging: 1 log per method per 5 seconds per app -// Using TTLCache ensures automatic cleanup of old entries const AUTO_APPROVAL_LOG_INTERVAL_MS = 5 * 1000; // 5 seconds -const autoApprovalLogTimestamps = new TTLCache('auto-approval-log', { - ttlMs: AUTO_APPROVAL_LOG_INTERVAL_MS, - maxSize: 10_000, // Safety cap -}); +const autoApprovalLogTimestamps = new Map(); function shouldLogAutoApproval(keyUserId: number, method: string): boolean { const key = `${keyUserId}:${method}`; + const now = Date.now(); + const lastLog = autoApprovalLogTimestamps.get(key); - // If key exists and hasn't expired, don't log - if (autoApprovalLogTimestamps.has(key)) { - return false; + if (!lastLog || now - lastLog >= AUTO_APPROVAL_LOG_INTERVAL_MS) { + autoApprovalLogTimestamps.set(key, now); + return true; } - - // Key doesn't exist or expired - log and set new entry - autoApprovalLogTimestamps.set(key, true); - return true; + return false; } function buildAuthorizationCallback( keyName: string, connectionManager: ConnectionManager ) { - const keyLogger = logger.child({ key: keyName }); - return async ({ id, method, pubkey, params }: PermitCallbackParams): Promise => { const humanPubkey = npubEncode(pubkey); - keyLogger.info('Request received', { requestId: id, from: humanPubkey, method }); + console.log(`[${keyName}] Request ${id} from ${humanPubkey} to ${method}`); const primaryParam = Array.isArray(params) ? params[0] : undefined; const result = await checkRequestPermission( @@ -121,7 +112,9 @@ function buildAuthorizationCallback( if (result.permitted !== undefined) { const accessType = result.autoApproved ? 'auto-approved' : 'granted'; - keyLogger.info(`Access ${result.permitted ? accessType : 'denied'} via ACL`, { npub: humanPubkey }); + console.log( + `[${keyName}] Access ${result.permitted ? accessType : 'denied'} via ACL for ${humanPubkey}` + ); // Log all permitted requests (with rate limiting) // This includes both trust-level auto-approvals and explicit permission grants @@ -129,20 +122,20 @@ function buildAuthorizationCallback( if (shouldLogAutoApproval(result.keyUserId, method)) { // Log asynchronously to avoid blocking logAutoApproval(result.keyUserId, method, primaryParam, keyName, id, pubkey, result.autoApproved, result.approvalType).catch(err => { - logger.error('Failed to log auto-approval', { error: toErrorMessage(err) }); + console.error('Failed to log auto-approval:', err); }); } } return result.permitted; } - keyLogger.info('No ACL decision, proceeding to authorization request', { npub: humanPubkey }); + console.log(`[${keyName}] No ACL decision for ${humanPubkey}, proceeding to authorization request`); try { await requestAuthorization(connectionManager, keyName, pubkey, id, method, primaryParam); return true; } catch (error) { - keyLogger.info('Authorization rejected', { error: toErrorMessage(error) }); + console.log(`[${keyName}] Authorization rejected: ${(error as Error).message}`); return false; } }; @@ -167,7 +160,18 @@ async function logAutoApproval( const paramsStr = typeof params === 'string' ? params : JSON.stringify(params); // Extract event kind for sign_event - const eventKind = method === 'sign_event' ? extractEventKind(paramsStr) : undefined; + let eventKind: number | undefined; + if (method === 'sign_event' && paramsStr) { + try { + const parsed = JSON.parse(paramsStr); + const event = Array.isArray(parsed) ? parsed[0] : parsed; + if (event && typeof event.kind === 'number') { + eventKind = event.kind; + } + } catch { + // Ignore parse errors + } + } // Create request record (so it appears in Activity page) await requestRepository.createAutoApproved({ @@ -276,9 +280,6 @@ class Daemon { this.eventService = new EventService(); setEventService(this.eventService); - // Wire up logger to emit SSE events for real-time log streaming - setLogEntryEmitter((entry) => this.eventService.emitLogEntry(entry)); - // Initialize connection manager (generates bunker URIs and sends auth_url responses) this.connectionManager = new ConnectionManager({ key: config.admin.key, @@ -316,7 +317,7 @@ class Daemon { if (backend) { backend.addAppSubscription(appId, relays); } else { - logger.warn('No backend for key, cannot create subscription', { key: keyName, source: 'nostrconnect' }); + console.log(`[nostrconnect] Warning: No backend for key "${keyName}", cannot create subscription`); } }); @@ -329,11 +330,11 @@ class Daemon { } public async start(): Promise { - logger.info('Connecting to relays...'); + console.log('Connecting to relays...'); // RelayPool connects lazily, but let's log what we're configured with const relayCount = this.pool.getRelays().length; - logger.info('Relay configuration', { count: relayCount, relays: this.pool.getRelays() }); + console.log(`Configured with ${relayCount} relays: ${this.pool.getRelays().join(', ')}`); this.subscriptionManager.start(); this.pool.startMonitoring(); // Start sleep/wake detection @@ -341,7 +342,7 @@ class Daemon { // Handle pool events (sleep/wake, reset) this.pool.on((event) => { if (event.type === 'sleep-detected') { - logger.info('System wake detected, refreshing connections', { source: 'killswitch' }); + console.log('[KillSwitch] System wake detected, refreshing connections'); this.adminCommandService?.refresh(); } if (event.type === 'pool-reset') { @@ -394,7 +395,7 @@ class Daemon { }); this.eventService.emitAdminEvent(adminLogRepository.toActivityEntry(adminLog)); - logger.info('Signet ready to serve requests'); + console.log('Signet ready to serve requests.'); } private startCleanupTasks(): void { @@ -422,16 +423,18 @@ class Daemon { const uptimeHours = Math.floor(status.uptime / 3600); const uptimeMinutes = Math.floor((status.uptime % 3600) / 60); - logger.info('Health status', { - uptime: `${uptimeHours}h ${uptimeMinutes}m`, - heapMB: status.memory.heapMB, - rssMB: status.memory.rssMB, - sseClients: status.sseClients, - relays: `${status.relays.connected}/${status.relays.total}`, - activeKeys: status.keys.active, - subscriptions: status.subscriptions, - ...(status.lastPoolReset ? { lastPoolReset: status.lastPoolReset } : {}), - }); + console.log('=== HEALTH STATUS ==='); + console.log(`Time: ${new Date().toISOString()}`); + console.log(`Uptime: ${uptimeHours}h ${uptimeMinutes}m`); + console.log(`Memory: ${status.memory.heapMB}MB heap, ${status.memory.rssMB}MB RSS`); + console.log(`SSE clients: ${status.sseClients}`); + console.log(`Relay connections: ${status.relays.connected}/${status.relays.total}`); + console.log(`Active keys: ${status.keys.active}`); + console.log(`Managed subscriptions: ${status.subscriptions}`); + if (status.lastPoolReset) { + console.log(`Last pool reset: ${status.lastPoolReset}`); + } + console.log('===================='); } private getHealthStatus(): HealthStatus { @@ -459,7 +462,6 @@ class Daemon { subscriptions: this.subscriptionManager.getSubscriptionCount(), sseClients: this.eventService.getSubscriberCount(), lastPoolReset: this.lastPoolReset?.toISOString() ?? null, - caches: getAllCacheStats(), }; } @@ -471,11 +473,11 @@ class Daemon { const requestMaxAge = new Date(Date.now() - REQUEST_MAX_AGE_MS); const deletedRequests = await requestRepository.cleanupExpired(requestMaxAge); if (deletedRequests > 0) { - logger.info('Cleaned up expired requests', { count: deletedRequests, maxAge: '24 hours' }); + console.log(`Cleaned up ${deletedRequests} expired request(s) older than 24 hours`); statsChanged = true; } } catch (error) { - logger.error('Failed to cleanup old requests', { error: toErrorMessage(error) }); + console.error('Failed to cleanup old requests:', error); } // Cleanup old logs (older than 30 days) @@ -483,11 +485,11 @@ class Daemon { const logMaxAge = new Date(Date.now() - LOG_MAX_AGE_MS); const deletedLogs = await logRepository.cleanupExpired(logMaxAge); if (deletedLogs > 0) { - logger.info('Cleaned up old logs', { count: deletedLogs, maxAge: '30 days' }); + console.log(`Cleaned up ${deletedLogs} log(s) older than 30 days`); statsChanged = true; } } catch (error) { - logger.error('Failed to cleanup old logs', { error: toErrorMessage(error) }); + console.error('Failed to cleanup old logs:', error); } // Cleanup old admin logs (older than 30 days) @@ -495,10 +497,10 @@ class Daemon { const adminLogMaxAge = new Date(Date.now() - LOG_MAX_AGE_MS); const deletedAdminLogs = await adminLogRepository.cleanupExpired(adminLogMaxAge); if (deletedAdminLogs > 0) { - logger.info('Cleaned up old admin logs', { count: deletedAdminLogs, maxAge: '30 days' }); + console.log(`Cleaned up ${deletedAdminLogs} admin log(s) older than 30 days`); } } catch (error) { - logger.error('Failed to cleanup old admin logs', { error: toErrorMessage(error) }); + console.error('Failed to cleanup old admin logs:', error); } // Cleanup expired connection tokens @@ -506,10 +508,10 @@ class Daemon { const tokenService = getConnectionTokenService(); const deletedTokens = await tokenService.cleanupExpiredTokens(); if (deletedTokens > 0) { - logger.info('Cleaned up expired connection tokens', { count: deletedTokens }); + console.log(`Cleaned up ${deletedTokens} expired connection token(s)`); } } catch (error) { - logger.error('Failed to cleanup connection tokens', { error: toErrorMessage(error) }); + console.error('Failed to cleanup connection tokens:', error); } // Emit stats update if anything changed @@ -521,7 +523,7 @@ class Daemon { private async startConfiguredKeys(): Promise { const activeKeys = this.keyService.getActiveKeys(); const names = Object.keys(activeKeys); - logger.info('Starting keys', { keys: names.length > 0 ? names : ['(none)'] }); + console.log('Starting keys:', names.join(', ') || '(none)'); for (const [name, secret] of Object.entries(activeKeys)) { await this.startKey(name, secret); @@ -550,7 +552,7 @@ class Daemon { if (secret.startsWith('nsec1')) { const decoded = nip19Decode(secret); if (decoded.type !== 'nsec') { - logger.warn('Cannot start key: Invalid nsec', { key: name }); + console.log(`Cannot start key ${name}: Invalid nsec`); return; } secretBytes = decoded.data as Uint8Array; @@ -570,9 +572,9 @@ class Daemon { backend.start(); this.backends.set(name, backend); - logger.info('Key online', { key: name }); + console.log(`Key "${name}" online.`); } catch (error) { - logger.error('Failed to start key', { key: name, error: toErrorMessage(error) }); + console.log(`Failed to start key ${name}: ${(error as Error).message}`); } } @@ -581,7 +583,7 @@ class Daemon { if (backend) { backend.stop(); this.backends.delete(name); - logger.info('Key locked', { key: name }); + console.log(`Key "${name}" locked.`); } } @@ -590,13 +592,22 @@ class Daemon { 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'; - logger.info(`Starting HTTP server on ${bindHost}:${port}...`); + 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'); + } + this.httpServer = new HttpServer({ host: bindHost, port, baseUrl, jwtSecret: this.config.jwtSecret, - apiToken: this.config.apiToken, + apiToken, allowedOrigins: this.config.allowedOrigins ?? [], requireAuth: this.config.requireAuth ?? false, connectionManager: this.connectionManager, @@ -617,7 +628,7 @@ class Daemon { private loadKeyMaterial(keyName: string, nsec: string): void { this.keyService.loadKeyMaterial(keyName, nsec); this.startKey(keyName, nsec).catch((error) => { - logger.error('Failed to start key', { key: keyName, error: toErrorMessage(error) }); + console.log(`Failed to start key ${keyName}: ${(error as Error).message}`); }); } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6f183fa..bebbd96 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -161,6 +161,29 @@ 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 +``` + +The daemon validates this token in the `X-API-Token` header sent by the UI proxy. Without a matching token, requests from the UI proxy will be rejected when `requireAuth` is enabled. + ### `admin.secret` Secret included in the bunker connection URI. Used to validate connection attempts from NIP-46 clients. diff --git a/packages/signet-types/src/config/types.ts b/packages/signet-types/src/config/types.ts index dc6e347..ff46d3f 100644 --- a/packages/signet-types/src/config/types.ts +++ b/packages/signet-types/src/config/types.ts @@ -68,8 +68,6 @@ export interface ConfigFile { allowedOrigins?: string[]; /** Require authentication for API access (default: false for local use) */ requireAuth?: boolean; - /** API token for server-to-server authentication (e.g., UI proxy to daemon) */ - apiToken?: string; /** Kill switch configuration for remote admin commands */ killSwitch?: KillSwitchConfig; } From da81357c8da9857ddb0663cb61447f9f8c23c2c2 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:34:49 +0100 Subject: [PATCH 65/72] Update env example --- .env.example | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 4b2db2f..76e5a1e 100644 --- a/.env.example +++ b/.env.example @@ -2,19 +2,34 @@ # Copy to .env only if you need to change the defaults # Bind addresses (which network interface to listen on) -# Signet Daemon port (default: 3000) -SIGNET_PORT=3000 +# 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_HOST=0.0.0.0 +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_HOST=0.0.0.0 +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= From 19b91195f501afd8570930c4bb3b8d549ae5ccba Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:44:29 +0100 Subject: [PATCH 66/72] Introduce proxy authentication middleware to not bypass existing JWT/CSRF/rate limiting --- apps/signet/src/daemon/http/server.ts | 32 +++++++++++--------- apps/signet/src/daemon/lib/auth.ts | 42 ++++++++++++++++++--------- docs/CONFIGURATION.md | 16 +++++++++- 3 files changed, 63 insertions(+), 27 deletions(-) diff --git a/apps/signet/src/daemon/http/server.ts b/apps/signet/src/daemon/http/server.ts index 7dab119..2772977 100644 --- a/apps/signet/src/daemon/http/server.ts +++ b/apps/signet/src/daemon/http/server.ts @@ -5,6 +5,7 @@ import Handlebars from 'handlebars'; import { registerAuthPlugins, createAuthMiddleware, + createProxyAuthMiddleware, createRateLimitMiddleware, createCsrfMiddleware, generateCsrfToken, @@ -127,11 +128,15 @@ export class HttpServer { } private async setupRoutes(): Promise { + // 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, - this.config.apiToken + this.config.requireAuth ); + const csrfMiddleware = createCsrfMiddleware(); const rateLimitAuth = createRateLimitMiddleware('auth'); const rateLimitKeys = createRateLimitMiddleware('keys'); @@ -149,7 +154,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 +166,14 @@ export class HttpServer { connectionManager: this.config.connectionManager, nostrConfig: this.config.nostrConfig, relayService: this.config.relayService, - }, { 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 +182,7 @@ export class HttpServer { registerKeysRoutes(this.fastify, { keyService: this.config.keyService, }, { - auth: [authMiddleware], + auth: [proxyAuthMiddleware, authMiddleware], csrf: [csrfMiddleware], rateLimit: [rateLimitKeys], }); @@ -185,25 +191,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 +217,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/lib/auth.ts b/apps/signet/src/daemon/lib/auth.ts index db3a656..f0fea5a 100644 --- a/apps/signet/src/daemon/lib/auth.ts +++ b/apps/signet/src/daemon/lib/auth.ts @@ -242,16 +242,40 @@ 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) - * @param apiToken - Optional API token for server-to-server authentication */ export function createAuthMiddleware( fastify: FastifyInstance, - requireAuth: boolean = true, - apiToken?: string + requireAuth: boolean = true ) { return async function authMiddleware( request: FastifyRequest, @@ -262,16 +286,7 @@ export function createAuthMiddleware( return; } - // Check for API token in X-API-Token header (for UI proxy) - if (apiToken) { - const requestApiToken = request.headers['x-api-token'] as string | undefined; - if (requestApiToken && timingSafeEqual(requestApiToken, apiToken)) { - // Valid API token - allow access - return; - } - } - - // Fall back to JWT token validation + // Require JWT token validation for user authentication const payload = await verifyToken(fastify, request); if (!payload) { @@ -399,6 +414,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/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index bebbd96..b480746 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -182,7 +182,21 @@ Set this token in your environment or `.env` file: SIGNET_API_TOKEN=your_generated_token_here ``` -The daemon validates this token in the `X-API-Token` header sent by the UI proxy. Without a matching token, requests from the UI proxy will be rejected when `requireAuth` is enabled. +**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` From 4a1fcef295d6aeaf409e3fe110260ff9909881f7 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:47:57 +0100 Subject: [PATCH 67/72] Add express rate limiter to protect Basic-Auth from bruce-force attempts --- apps/signet-ui/Dockerfile | 2 +- apps/signet-ui/package.json | 1 + apps/signet-ui/server.mjs | 20 +++++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/signet-ui/Dockerfile b/apps/signet-ui/Dockerfile index 1003e78..5ba12d0 100644 --- a/apps/signet-ui/Dockerfile +++ b/apps/signet-ui/Dockerfile @@ -15,7 +15,7 @@ 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 basic-auth@2 +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_BIND_PORT=4174 diff --git a/apps/signet-ui/package.json b/apps/signet-ui/package.json index 28c05e6..57e2b5c 100644 --- a/apps/signet-ui/package.json +++ b/apps/signet-ui/package.json @@ -20,6 +20,7 @@ "debug": "^4.3.4", "dotenv": "^16.6.1", "express": "^4.19.2", + "express-rate-limit": "^7.5.0", "focus-trap-react": "^11.0.4", "html5-qrcode": "^2.3.8", "http-proxy-middleware": "^3.0.5", diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index 6f37022..a3506bf 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -1,5 +1,6 @@ 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'; @@ -32,6 +33,19 @@ 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) { @@ -108,8 +122,12 @@ const apiProxy = createProxyMiddleware({ } }); -// Basic authentication middleware +// 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); From ab1c44c1f4359be6e4f18a2af4d9fbae95d23d62 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Sat, 10 Jan 2026 23:48:27 +0100 Subject: [PATCH 68/72] Update lockfile --- pnpm-lock.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44e3990..cd1bf95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,9 @@ importers: express: specifier: ^4.19.2 version: 4.21.2 + express-rate-limit: + specifier: ^7.5.0 + version: 7.5.1(express@4.21.2) focus-trap-react: specifier: ^11.0.4 version: 11.0.4(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2194,6 +2197,12 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -6168,6 +6177,10 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@7.5.1(express@4.21.2): + dependencies: + express: 4.21.2 + express@4.21.2: dependencies: accepts: 1.3.8 From 732bca5ebd1515317b34507839eb34232742a8d7 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Mon, 12 Jan 2026 09:01:15 +0100 Subject: [PATCH 69/72] Update startWebAuth --- apps/signet/src/daemon/lib/network.ts | 4 +- apps/signet/src/daemon/run.ts | 154 ++++++++++++-------------- 2 files changed, 74 insertions(+), 84 deletions(-) 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 6cb8db7..47a97a8 100644 --- a/apps/signet/src/daemon/run.ts +++ b/apps/signet/src/daemon/run.ts @@ -8,6 +8,10 @@ import { printServerInfo } from './lib/network.js'; import { requestAuthorization } from './authorize.js'; import type { DaemonBootstrapConfig } from './types.js'; import { checkRequestPermission, type RpcMethod, type ApprovalType } from './lib/acl.js'; +import { TTLCache, getAllCacheStats } from './lib/ttl-cache.js'; +import { extractEventKind } from './lib/parse.js'; +import { toErrorMessage } from './lib/errors.js'; +import { logger, setLogEntryEmitter } from './lib/logger.js'; import { KeyService, RequestService, @@ -50,23 +54,21 @@ try { // Catch unhandled errors to prevent silent crashes and provide visibility process.on('uncaughtException', (error: Error) => { - console.error('=== UNCAUGHT EXCEPTION ==='); - console.error('Time:', new Date().toISOString()); - console.error('Error:', error.message); - console.error('Stack:', error.stack); - console.error('=========================='); + logger.error('Uncaught exception', { + error: error.message, + stack: error.stack, + }); // Don't exit - let the process continue if possible // The error is logged and we can investigate }); -process.on('unhandledRejection', (reason: unknown, promise: Promise) => { - console.error('=== UNHANDLED REJECTION ==='); - console.error('Time:', new Date().toISOString()); - console.error('Reason:', reason); - if (reason instanceof Error) { - console.error('Stack:', reason.stack); - } - console.error('==========================='); +process.on('unhandledRejection', (reason: unknown, _promise: Promise) => { + const reasonStr = reason instanceof Error ? reason.message : String(reason); + const stack = reason instanceof Error ? reason.stack : undefined; + logger.error('Unhandled rejection', { + reason: reasonStr, + ...(stack ? { stack } : {}), + }); // Don't exit - just log for investigation }); @@ -79,28 +81,35 @@ const LOG_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days const HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes // Rate limiting for auto-approval logging: 1 log per method per 5 seconds per app +// Using TTLCache ensures automatic cleanup of old entries const AUTO_APPROVAL_LOG_INTERVAL_MS = 5 * 1000; // 5 seconds -const autoApprovalLogTimestamps = new Map(); +const autoApprovalLogTimestamps = new TTLCache('auto-approval-log', { + ttlMs: AUTO_APPROVAL_LOG_INTERVAL_MS, + maxSize: 10_000, // Safety cap +}); function shouldLogAutoApproval(keyUserId: number, method: string): boolean { const key = `${keyUserId}:${method}`; - const now = Date.now(); - const lastLog = autoApprovalLogTimestamps.get(key); - if (!lastLog || now - lastLog >= AUTO_APPROVAL_LOG_INTERVAL_MS) { - autoApprovalLogTimestamps.set(key, now); - return true; + // If key exists and hasn't expired, don't log + if (autoApprovalLogTimestamps.has(key)) { + return false; } - return false; + + // Key doesn't exist or expired - log and set new entry + autoApprovalLogTimestamps.set(key, true); + return true; } function buildAuthorizationCallback( keyName: string, connectionManager: ConnectionManager ) { + const keyLogger = logger.child({ key: keyName }); + return async ({ id, method, pubkey, params }: PermitCallbackParams): Promise => { const humanPubkey = npubEncode(pubkey); - console.log(`[${keyName}] Request ${id} from ${humanPubkey} to ${method}`); + keyLogger.info('Request received', { requestId: id, from: humanPubkey, method }); const primaryParam = Array.isArray(params) ? params[0] : undefined; const result = await checkRequestPermission( @@ -112,9 +121,7 @@ function buildAuthorizationCallback( if (result.permitted !== undefined) { const accessType = result.autoApproved ? 'auto-approved' : 'granted'; - console.log( - `[${keyName}] Access ${result.permitted ? accessType : 'denied'} via ACL for ${humanPubkey}` - ); + keyLogger.info(`Access ${result.permitted ? accessType : 'denied'} via ACL`, { npub: humanPubkey }); // Log all permitted requests (with rate limiting) // This includes both trust-level auto-approvals and explicit permission grants @@ -122,20 +129,20 @@ function buildAuthorizationCallback( if (shouldLogAutoApproval(result.keyUserId, method)) { // Log asynchronously to avoid blocking logAutoApproval(result.keyUserId, method, primaryParam, keyName, id, pubkey, result.autoApproved, result.approvalType).catch(err => { - console.error('Failed to log auto-approval:', err); + logger.error('Failed to log auto-approval', { error: toErrorMessage(err) }); }); } } return result.permitted; } - console.log(`[${keyName}] No ACL decision for ${humanPubkey}, proceeding to authorization request`); + keyLogger.info('No ACL decision, proceeding to authorization request', { npub: humanPubkey }); try { await requestAuthorization(connectionManager, keyName, pubkey, id, method, primaryParam); return true; } catch (error) { - console.log(`[${keyName}] Authorization rejected: ${(error as Error).message}`); + keyLogger.info('Authorization rejected', { error: toErrorMessage(error) }); return false; } }; @@ -160,18 +167,7 @@ async function logAutoApproval( const paramsStr = typeof params === 'string' ? params : JSON.stringify(params); // Extract event kind for sign_event - let eventKind: number | undefined; - if (method === 'sign_event' && paramsStr) { - try { - const parsed = JSON.parse(paramsStr); - const event = Array.isArray(parsed) ? parsed[0] : parsed; - if (event && typeof event.kind === 'number') { - eventKind = event.kind; - } - } catch { - // Ignore parse errors - } - } + const eventKind = method === 'sign_event' ? extractEventKind(paramsStr) : undefined; // Create request record (so it appears in Activity page) await requestRepository.createAutoApproved({ @@ -280,6 +276,9 @@ class Daemon { this.eventService = new EventService(); setEventService(this.eventService); + // Wire up logger to emit SSE events for real-time log streaming + setLogEntryEmitter((entry) => this.eventService.emitLogEntry(entry)); + // Initialize connection manager (generates bunker URIs and sends auth_url responses) this.connectionManager = new ConnectionManager({ key: config.admin.key, @@ -317,7 +316,7 @@ class Daemon { if (backend) { backend.addAppSubscription(appId, relays); } else { - console.log(`[nostrconnect] Warning: No backend for key "${keyName}", cannot create subscription`); + logger.warn('No backend for key, cannot create subscription', { key: keyName, source: 'nostrconnect' }); } }); @@ -330,11 +329,11 @@ class Daemon { } public async start(): Promise { - console.log('Connecting to relays...'); + logger.info('Connecting to relays...'); // RelayPool connects lazily, but let's log what we're configured with const relayCount = this.pool.getRelays().length; - console.log(`Configured with ${relayCount} relays: ${this.pool.getRelays().join(', ')}`); + logger.info('Relay configuration', { count: relayCount, relays: this.pool.getRelays() }); this.subscriptionManager.start(); this.pool.startMonitoring(); // Start sleep/wake detection @@ -342,7 +341,7 @@ class Daemon { // Handle pool events (sleep/wake, reset) this.pool.on((event) => { if (event.type === 'sleep-detected') { - console.log('[KillSwitch] System wake detected, refreshing connections'); + logger.info('System wake detected, refreshing connections', { source: 'killswitch' }); this.adminCommandService?.refresh(); } if (event.type === 'pool-reset') { @@ -395,7 +394,7 @@ class Daemon { }); this.eventService.emitAdminEvent(adminLogRepository.toActivityEntry(adminLog)); - console.log('Signet ready to serve requests.'); + logger.info('Signet ready to serve requests'); } private startCleanupTasks(): void { @@ -423,18 +422,16 @@ class Daemon { const uptimeHours = Math.floor(status.uptime / 3600); const uptimeMinutes = Math.floor((status.uptime % 3600) / 60); - console.log('=== HEALTH STATUS ==='); - console.log(`Time: ${new Date().toISOString()}`); - console.log(`Uptime: ${uptimeHours}h ${uptimeMinutes}m`); - console.log(`Memory: ${status.memory.heapMB}MB heap, ${status.memory.rssMB}MB RSS`); - console.log(`SSE clients: ${status.sseClients}`); - console.log(`Relay connections: ${status.relays.connected}/${status.relays.total}`); - console.log(`Active keys: ${status.keys.active}`); - console.log(`Managed subscriptions: ${status.subscriptions}`); - if (status.lastPoolReset) { - console.log(`Last pool reset: ${status.lastPoolReset}`); - } - console.log('===================='); + logger.info('Health status', { + uptime: `${uptimeHours}h ${uptimeMinutes}m`, + heapMB: status.memory.heapMB, + rssMB: status.memory.rssMB, + sseClients: status.sseClients, + relays: `${status.relays.connected}/${status.relays.total}`, + activeKeys: status.keys.active, + subscriptions: status.subscriptions, + ...(status.lastPoolReset ? { lastPoolReset: status.lastPoolReset } : {}), + }); } private getHealthStatus(): HealthStatus { @@ -462,6 +459,7 @@ class Daemon { subscriptions: this.subscriptionManager.getSubscriptionCount(), sseClients: this.eventService.getSubscriberCount(), lastPoolReset: this.lastPoolReset?.toISOString() ?? null, + caches: getAllCacheStats(), }; } @@ -473,11 +471,11 @@ class Daemon { const requestMaxAge = new Date(Date.now() - REQUEST_MAX_AGE_MS); const deletedRequests = await requestRepository.cleanupExpired(requestMaxAge); if (deletedRequests > 0) { - console.log(`Cleaned up ${deletedRequests} expired request(s) older than 24 hours`); + logger.info('Cleaned up expired requests', { count: deletedRequests, maxAge: '24 hours' }); statsChanged = true; } } catch (error) { - console.error('Failed to cleanup old requests:', error); + logger.error('Failed to cleanup old requests', { error: toErrorMessage(error) }); } // Cleanup old logs (older than 30 days) @@ -485,11 +483,11 @@ class Daemon { const logMaxAge = new Date(Date.now() - LOG_MAX_AGE_MS); const deletedLogs = await logRepository.cleanupExpired(logMaxAge); if (deletedLogs > 0) { - console.log(`Cleaned up ${deletedLogs} log(s) older than 30 days`); + logger.info('Cleaned up old logs', { count: deletedLogs, maxAge: '30 days' }); statsChanged = true; } } catch (error) { - console.error('Failed to cleanup old logs:', error); + logger.error('Failed to cleanup old logs', { error: toErrorMessage(error) }); } // Cleanup old admin logs (older than 30 days) @@ -497,10 +495,10 @@ class Daemon { const adminLogMaxAge = new Date(Date.now() - LOG_MAX_AGE_MS); const deletedAdminLogs = await adminLogRepository.cleanupExpired(adminLogMaxAge); if (deletedAdminLogs > 0) { - console.log(`Cleaned up ${deletedAdminLogs} admin log(s) older than 30 days`); + logger.info('Cleaned up old admin logs', { count: deletedAdminLogs, maxAge: '30 days' }); } } catch (error) { - console.error('Failed to cleanup old admin logs:', error); + logger.error('Failed to cleanup old admin logs', { error: toErrorMessage(error) }); } // Cleanup expired connection tokens @@ -508,10 +506,10 @@ class Daemon { const tokenService = getConnectionTokenService(); const deletedTokens = await tokenService.cleanupExpiredTokens(); if (deletedTokens > 0) { - console.log(`Cleaned up ${deletedTokens} expired connection token(s)`); + logger.info('Cleaned up expired connection tokens', { count: deletedTokens }); } } catch (error) { - console.error('Failed to cleanup connection tokens:', error); + logger.error('Failed to cleanup connection tokens', { error: toErrorMessage(error) }); } // Emit stats update if anything changed @@ -523,7 +521,7 @@ class Daemon { private async startConfiguredKeys(): Promise { const activeKeys = this.keyService.getActiveKeys(); const names = Object.keys(activeKeys); - console.log('Starting keys:', names.join(', ') || '(none)'); + logger.info('Starting keys', { keys: names.length > 0 ? names : ['(none)'] }); for (const [name, secret] of Object.entries(activeKeys)) { await this.startKey(name, secret); @@ -552,7 +550,7 @@ class Daemon { if (secret.startsWith('nsec1')) { const decoded = nip19Decode(secret); if (decoded.type !== 'nsec') { - console.log(`Cannot start key ${name}: Invalid nsec`); + logger.warn('Cannot start key: Invalid nsec', { key: name }); return; } secretBytes = decoded.data as Uint8Array; @@ -572,9 +570,9 @@ class Daemon { backend.start(); this.backends.set(name, backend); - console.log(`Key "${name}" online.`); + logger.info('Key online', { key: name }); } catch (error) { - console.log(`Failed to start key ${name}: ${(error as Error).message}`); + logger.error('Failed to start key', { key: name, error: toErrorMessage(error) }); } } @@ -583,7 +581,7 @@ class Daemon { if (backend) { backend.stop(); this.backends.delete(name); - console.log(`Key "${name}" locked.`); + logger.info('Key locked', { key: name }); } } @@ -592,22 +590,14 @@ class Daemon { 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'); - } + logger.info('Starting HTTP server', { port, host: bindHost }); this.httpServer = new HttpServer({ - host: bindHost, port, + host: bindHost, baseUrl, jwtSecret: this.config.jwtSecret, - apiToken, allowedOrigins: this.config.allowedOrigins ?? [], requireAuth: this.config.requireAuth ?? false, connectionManager: this.connectionManager, @@ -622,13 +612,13 @@ class Daemon { }); await this.httpServer.start(); - await printServerInfo(port); + await printServerInfo(bindHost, port); } private loadKeyMaterial(keyName: string, nsec: string): void { this.keyService.loadKeyMaterial(keyName, nsec); this.startKey(keyName, nsec).catch((error) => { - console.log(`Failed to start key ${keyName}: ${(error as Error).message}`); + logger.error('Failed to start key', { key: keyName, error: toErrorMessage(error) }); }); } @@ -646,4 +636,4 @@ class Daemon { })), }); } -} +} \ No newline at end of file From 5ee59d306eb75d0baddb4eeba256ba0224d6a8b5 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Mon, 12 Jan 2026 09:19:41 +0100 Subject: [PATCH 70/72] Disable authRateLimited (it's kicking in) --- apps/signet-ui/server.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/signet-ui/server.mjs b/apps/signet-ui/server.mjs index a3506bf..7a6736c 100644 --- a/apps/signet-ui/server.mjs +++ b/apps/signet-ui/server.mjs @@ -125,7 +125,7 @@ const apiProxy = createProxyMiddleware({ // Basic authentication middleware with rate limiting if (isAuthEnabled) { // Apply rate limiter first - app.use(authRateLimiter); + //app.use(authRateLimiter); // Then check credentials app.use((req, res, next) => { From 700e669e4ef261a75d80319f70c781f96ea52228 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Thu, 12 Feb 2026 17:29:55 +0100 Subject: [PATCH 71/72] Fix conflicts + repair auth in dev server --- apps/signet-ui/vite.config.ts | 38 +++++++++++++- apps/signet/package.json | 2 +- pnpm-lock.yaml | 93 ++++++++++++++++++++++------------- 3 files changed, 96 insertions(+), 37 deletions(-) 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/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/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: From fd2d8a397f9a90bb38ef8d1cf764152a3749e1b2 Mon Sep 17 00:00:00 2001 From: digitalbase Date: Thu, 12 Feb 2026 17:32:46 +0100 Subject: [PATCH 72/72] Fix hard coded sqlite dir --- apps/signet/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/signet/Dockerfile b/apps/signet/Dockerfile index 3d043f3..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