diff --git a/Dockerfile b/Dockerfile index eb07271..1e6f378 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,34 @@ FROM node:20.11-bullseye AS build + WORKDIR /app -# Copy and install all dependencies +# Copy package files and install dependencies COPY package*.json ./ -COPY tsconfig*.json ./ -RUN npm install -g npm@latest && npm install +RUN npm install + +# Copy application files COPY . . + +# Generate prisma client and build the application RUN npx prisma generate RUN npm run build -# Optional: prune dev dependencies after build -RUN npm prune --omit=dev - +# Runtime stage FROM node:20.11-alpine as runtime + WORKDIR /app + RUN apk update && \ apk add --no-cache openssl && \ rm -rf /var/cache/apk/* -# Copy only what is needed -COPY --from=build /app/package*.json ./ -COPY --from=build /app/node_modules ./node_modules -COPY --from=build /app/dist ./dist -COPY --from=build /app/node_modules/.prisma ./node_modules/.prisma +# Copy built files from the build stage +COPY --from=build /app . + +# Install only runtime dependencies +RUN npm install --only=production EXPOSE 3000 + ENTRYPOINT [ "node", "./dist/index.js" ] CMD ["start"] \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index 0b0deca..7a595b7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,18 +1,26 @@ /** * CLI client for interacting with nsecBunker - * Supports: - * - Signing events (NIP-46) - * - Creating new accounts - * - Managing authorization flows - * - Publishing to relays * - * Uses NDK for Nostr protocol interactions + * This client provides command-line functionality for: + * - Signing events using NIP-46 protocol + * - Creating new Nostr accounts + * - Managing authorization flows + * - Publishing signed events to relays + * + * Security Model: + * - Uses local private key for client authentication + * - Communicates with nsecBunker through encrypted channels + * - Supports NIP-05 identifier resolution + * - Implements NIP-46 signing protocol + * + * @module nsecbunker-client */ import "websocket-polyfill"; import NDK, { NDKUser, NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk'; import fs from 'fs'; +// Command line argument parsing const args = process.argv; const command = process.argv[2]; let remotePubkey = process.argv[3]; @@ -23,6 +31,7 @@ let signer: NDKNip46Signer; let ndk: NDK; let remoteUser: NDKUser; +// Parse relay list from command line arguments const relaysIndex = args.findIndex(arg => arg === '--relays'); let relays: string[] = []; @@ -30,6 +39,9 @@ if (relaysIndex !== -1 && args[relaysIndex + 1]) { relays = args[relaysIndex + 1].split(','); } +/** + * Display usage instructions when no command is provided + */ if (!command) { console.log('Usage: node src/client.js [--dont-publish] [--debug] [--pk ]'); console.log(''); @@ -42,12 +54,16 @@ if (!command) { process.exit(1); } +/** + * Creates and configures a new NDK instance + * Connects to nsecBunker relay and any additional specified relays + */ async function createNDK(): Promise { const ndk = new NDK({ explicitRelayUrls: [ - 'wss://relay.nsecbunker.com', - ...relays - ], + 'wss://relay.nsecbunker.com', + ...relays + ], enableOutboxModel: false }); if (debug) { @@ -58,15 +74,19 @@ async function createNDK(): Promise { return ndk; } -// switch (command) { -// case 'ping': -// ping(remotePubkey); - +/** + * Gets the path for storing the client's private key + * Uses home directory for cross-platform compatibility + */ function getPrivateKeyPath() { const home = process.env.HOME || process.env.USERPROFILE; return `${home}/.nsecbunker-client-private.key`; } +/** + * Saves the client's private key to disk + * Creates directory if it doesn't exist + */ function savePrivateKey(pk: string) { const path = getPrivateKeyPath(); if (!fs.existsSync(path)) { @@ -75,6 +95,10 @@ function savePrivateKey(pk: string) { fs.writeFileSync(`${path}/private.key`, pk); } +/** + * Loads the client's private key from disk + * Returns undefined if key doesn't exist + */ function loadPrivateKey(): string | undefined { const path = getPrivateKeyPath(); if (!fs.existsSync(path)) { @@ -83,15 +107,14 @@ function loadPrivateKey(): string | undefined { return fs.readFileSync(`${path}/private.key`).toString(); } - +// Main execution block (async () => { let remoteUser: NDKUser; ndk = await createNDK(); - // if this is the create_account command and we have something that doesn't look like an npub as the remotePubkey, use NDKUser.fromNip05 to get the npub + // Handle NIP-05 identifier resolution if (command === 'create_account' && !remotePubkey.startsWith("npub")) { - // see if we have a username@domain let [ username, domain ] = remotePubkey.split('@'); if (!domain) { @@ -109,7 +132,7 @@ function loadPrivateKey(): string | undefined { remoteUser = u; remotePubkey = remoteUser.pubkey; } else { - // check if we have a @ so we try to get the npub from nip05 + // Handle NIP-05 resolution for existing accounts if (remotePubkey.includes('@')) { const u = await NDKUser.fromNip05(remotePubkey); if (!u) { @@ -123,9 +146,8 @@ function loadPrivateKey(): string | undefined { } } - + // Initialize local signer let localSigner: NDKPrivateKeySigner; - const pk = loadPrivateKey(); if (pk) { @@ -135,15 +157,18 @@ function loadPrivateKey(): string | undefined { savePrivateKey(localSigner.privateKey!); } + // Setup NIP-46 signer and NDK instance signer = new NDKNip46Signer(ndk, remoteUser.pubkey, localSigner); if (debug) console.log(`local pubkey`, (await localSigner.user()).npub); if (debug) console.log(`remote pubkey`, remotePubkey); ndk.signer = signer; + // Handle OAuth-like authorization flow signer.on("authUrl", (url) => { console.log(`Go to ${url} to authorize this request`); }); + // Route to appropriate command handler switch (command) { case "sign": return signFlow(); case "create_account": return createAccountFlow(); @@ -153,6 +178,11 @@ function loadPrivateKey(): string | undefined { } })(); +/** + * Handles account creation flow + * Parses username, domain, and optional email + * Creates new account through nsecBunker + */ async function createAccountFlow() { const [ username, domain, email ] = content.split(',').map((s) => s.trim()); try { @@ -164,6 +194,11 @@ async function createAccountFlow() { } } +/** + * Handles event signing flow + * Supports both JSON event objects and simple text content + * Optionally publishes signed events to relays + */ function signFlow() { setTimeout(async () => { try { @@ -177,6 +212,7 @@ function signFlow() { let event; + // Parse event from JSON or create new kind:1 event try { const json = JSON.parse(content); event = new NDKEvent(ndk, json); @@ -193,6 +229,7 @@ function signFlow() { } as NostrEvent); } + // Sign and optionally publish event try { await event.sign(); if (debug) { @@ -201,9 +238,9 @@ function signFlow() { console.log(event.sig); } - if (!dontPublish) { - const relaysPublished = await event.publish(); - } + if (!dontPublish) { + const relaysPublished = await event.publish(); + } process.exit(0); } catch(e) { diff --git a/src/commands/add.ts b/src/commands/add.ts index 1b19515..271783b 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -16,6 +16,13 @@ interface IOpts { name: string; } +/** + * Encrypts and saves an nsec to the configuration + * @param config Path to config file + * @param nsec The nsec to encrypt + * @param passphrase Encryption passphrase + * @param name Key name identifier + */ export async function saveEncrypted(config: string, nsec: string, passphrase: string, name: string) { const { iv, data } = encryptNsec(nsec, passphrase); const currentConfig = await getCurrentConfig(config); @@ -25,6 +32,10 @@ export async function saveEncrypted(config: string, nsec: string, passphrase: st saveCurrentConfig(config, currentConfig); } +/** + * Interactive command to add a new nsec + * Prompts user for passphrase and nsec + */ export async function addNsec(opts: IOpts) { const name = opts.name; const config = opts.config; @@ -34,19 +45,16 @@ export async function addNsec(opts: IOpts) { output: process.stdout }); + // Prompt for passphrase and nsec console.log(`nsecBunker uses a passphrase to encrypt your nsec when stored on-disk.\n` + `Every time you restart it, you will need to type in this password.` + `\n`); rl.question(`Enter a passphrase: `, (passphrase: string) => { rl.question(`Enter the nsec for ${name}: `, (nsec: string) => { - let decoded; try { decoded = nip19.decode(nsec); - // const hexpubkey = getPublicKey(decoded.data as string); - // const npub = nip19.npubEncode(hexpubkey); saveEncrypted(config, nsec, passphrase, name); - rl.close(); } catch (e: any) { console.log(e.message); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 2bc8ebb..ba6cb3c 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -1,12 +1,20 @@ /** - * Initial setup command for nsecBunker - * Configures the first administrator npub that will have - * remote control access to the bunker + * Initial Setup Command + * + * Handles first-time setup of nsecBunker: + * - Administrator configuration + * - Initial npub registration + * - Basic security settings */ import readline from 'readline'; import { getCurrentConfig, saveCurrentConfig } from '../config/index.js'; +/** + * Runs the initial setup process + * Prompts for administrator npub and saves to config + * @param config - Path to config file + */ export async function setup(config: string) { const currentConfig = await getCurrentConfig(config); const rl = readline.createInterface({ @@ -16,9 +24,9 @@ export async function setup(config: string) { console.log(`You need at least one administrator to remotely control nsecBunker. This should probably be your own npub.\n`); + // Prompt for admin npub rl.question(`Enter an administrator npub: `, (npub: string) => { currentConfig.admin.npubs.push(npub); - saveCurrentConfig(config, currentConfig); rl.close(); diff --git a/src/commands/start.ts b/src/commands/start.ts index 121ae89..f4ccffb 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -113,8 +113,8 @@ async function nip89announcement(configData: IConfig) { } /** - * This command starts the nsecbunkerd process with an (optional) - * admin interface over websockets or redis. + * Main start command implementation + * @param opts Configuration options for starting the daemon */ export async function start(opts: IOpts) { const configData = await getCurrentConfig(opts.config); diff --git a/src/config/index.ts b/src/config/index.ts index deeda69..c7dd40c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,17 +1,20 @@ /** * Configuration management for nsecBunker - * Handles reading/writing config files and provides type definitions - * for the configuration schema + * + * This module handles the reading, writing, and type definitions for the nsecbunker.json + * configuration file. The configuration controls all aspects of the bunker's operation, + * including admin access, relay connections, and key storage. */ import { readFileSync, writeFileSync } from 'fs'; import { NDKPrivateKeySigner, NDKUserProfile } from '@nostr-dev-kit/ndk'; import { IAdminOpts } from '../daemon/admin'; - import { version } from '../../package.json'; +// Generate a default private key for bunker communication const generatedKey = NDKPrivateKeySigner.generate(); +// LNBits wallet integration configuration export type LNBitsWalletConfig = { url: string, key: string, @@ -22,7 +25,12 @@ export interface IWalletConfig { lnbits?: LNBitsWalletConfig; } +/** + * Configuration for individual domains + * Used when creating new users and managing NIP-05 verifications + */ export interface DomainConfig { + // The file pointing to the domain's NIP-05 verification nip05: string; nip89?: { profile: Record; @@ -33,21 +41,41 @@ export interface DomainConfig { defaultProfile?: Record; }; +/** + * Main configuration interface for nsecBunker + * All properties are optional unless otherwise noted in the documentation + */ export interface IConfig { + // Nostr relay configuration for NIP-46 requests nostr: { relays: string[]; }; + // Admin configuration for bunker management admin: IAdminOpts; + // Port for OAuth-like authentication flow authPort?: number; + // Host for OAuth-like authentication flow authHost?: string; + // Database URI for storing bunker data database: string; + // Path for log file storage logs: string; + // Storage for encrypted and unencrypted keys + // Format: keys.$keyId.iv + keys.$keyId.data (encrypted) + // keys.$keyId.key (unencrypted) keys: Record; + // URL for OAuth-like authentication access baseUrl?: string; + // Enable detailed logging when true verbose: boolean; + // Allowed domains for user creation domains?: Record; } +/** + * Default configuration used when no config file exists + * Provides minimal setup for basic bunker operation + */ const defaultConfig: IConfig = { nostr: { relays: [ @@ -56,24 +84,29 @@ const defaultConfig: IConfig = { ] }, admin: { - npubs: [], - adminRelays: [ + npubs: [], // Admin NPUBs allowed to manage the bunker + adminRelays: [ // Relays for admin commands "wss://relay.nsecbunker.com" ], - key: generatedKey.privateKey!, + key: generatedKey.privateKey!, // Auto-generated bunker private key notifyAdminsOnBoot: true, }, database: 'sqlite://nsecbunker.db', logs: './nsecbunker.log', - keys: {}, + keys: {}, // Empty key storage by default verbose: false, }; +/** + * Reads and processes the configuration file + * Creates default config if none exists + * Ensures all required properties are present + */ async function getCurrentConfig(config: string): Promise { try { const configFileContents = readFileSync(config, 'utf8'); - // add new config options to the config file + // Update config with current version and defaults const currentConfig = JSON.parse(configFileContents); currentConfig.version = version; currentConfig.admin.notifyAdminsOnBoot ??= true; @@ -81,15 +114,20 @@ async function getCurrentConfig(config: string): Promise { return currentConfig; } catch (err: any) { if (err.code === 'ENOENT') { + // Create new config file with defaults if none exists await saveCurrentConfig(config, defaultConfig); return defaultConfig; } else { console.error(`Error reading config file: ${err.message}`); - process.exit(1); // Kill the process if there is an error parsing the JSON + process.exit(1); } } } +/** + * Saves the current configuration to disk + * Automatically includes the current bunker version + */ export function saveCurrentConfig(config: string, currentConfig: any) { try { currentConfig.version = version; @@ -97,8 +135,8 @@ export function saveCurrentConfig(config: string, currentConfig: any) { writeFileSync(config, configString); } catch (err: any) { console.error(`Error writing config file: ${err.message}`); - process.exit(1); // Kill the process if there is an error parsing the JSON + process.exit(1); } } - export {getCurrentConfig}; + diff --git a/src/daemon/admin/commands/account/wallet.ts b/src/daemon/admin/commands/account/wallet.ts index 35d73aa..9b68128 100644 --- a/src/daemon/admin/commands/account/wallet.ts +++ b/src/daemon/admin/commands/account/wallet.ts @@ -1,9 +1,24 @@ +/** + * Wallet management functionality for user accounts. + * Handles wallet generation and LN Address creation through LNBits integration. + */ + import axios from "axios"; import createDebug from "debug"; import { IWalletConfig, LNBitsWalletConfig } from "../../../../config"; const debug = createDebug("nsecbunker:wallet"); +/** + * Generates a wallet based on the provided configuration. + * Currently supports LNBits wallet generation. + * + * @param walletConfig - Wallet configuration object + * @param username - User's username + * @param domain - Domain name + * @param npub - User's nostr public key + * @returns Lightning address in the format username@domain + */ export async function generateWallet( walletConfig: IWalletConfig, username: string, @@ -16,6 +31,16 @@ export async function generateWallet( } } +/** + * Creates a new wallet in LNBits for the specified user. + * + * @param lnbitsConfig - LNBits-specific configuration + * @param username - User's username + * @param domain - Domain name + * @param npub - User's nostr public key + * @returns Lightning address in the format username@domain + * @throws Will throw an error if the LNBits API request fails + */ export async function generateLNBitsWallet( lnbitsConfig: LNBitsWalletConfig, username: string, @@ -52,6 +77,19 @@ export async function generateLNBitsWallet( ); } +/** + * Generates a Lightning Address by registering with a nostdress server. + * + * @param username - User's username + * @param domain - Domain name + * @param userInvoiceKey - LNBits invoice/read key + * @param userNpub - User's nostr public key + * @param kind - Type of Lightning implementation (e.g., 'lnbits') + * @param host - Base URL of the Lightning implementation + * @param nostdressUrl - URL of the nostdress server + * @returns Lightning address in the format username@domain + * @throws Will throw an error if the nostdress API request fails + */ export async function generateLNAddress( username: string, domain: string, diff --git a/src/daemon/admin/commands/create_account.ts b/src/daemon/admin/commands/create_account.ts index e4632d6..6a1d1bc 100644 --- a/src/daemon/admin/commands/create_account.ts +++ b/src/daemon/admin/commands/create_account.ts @@ -12,6 +12,22 @@ import createDebug from "debug"; const debug = createDebug("nsecbunker:createAccount"); +/** + * Account Creation Command Handler + * + * Handles the creation of new user accounts. + * Features: + * - Key generation + * - Initial permissions setup + * - Profile creation + */ + +/** + * Validates the creation of a new nostr account by checking: + * 1. Username is provided + * 2. Domain exists in config + * 3. Username isn't already taken in the domain's nip05 file + */ export async function validate(currentConfig, username: string, domain: string, email?: string) { if (!username) { throw new Error('username is required'); @@ -35,6 +51,11 @@ const emptyNip05File = { relays: {}, } +/** + * Retrieves the current NIP-05 verification file for a domain + * NIP-05 files map usernames to their corresponding nostr public keys + * and contain relay information for the domain + */ async function getCurrentNip05File(currentConfig: any, domain: string) { try { const nip05File = currentConfig.domains[domain].nip05; @@ -46,7 +67,8 @@ async function getCurrentNip05File(currentConfig: any, domain: string) { } /** - * Adds an entry to the nip05 file for the domain + * Updates the domain's NIP-05 file with a new username->pubkey mapping + * Also adds relay information for NIP-46 (Nostr Connect) support */ async function addNip05(currentConfig: IConfig, username: string, domain: string, pubkey: Hexpubkey) { const currentNip05s = await getCurrentNip05File(currentConfig, domain); @@ -99,6 +121,12 @@ async function validateDomain(domain: string | undefined, admin: AdminInterface, return domain; } +/** + * Main entry point for account creation + * 1. Validates username and domain + * 2. Requests authorization for the creation + * 3. If authorized, calls createAccountReal to perform the actual creation + */ export default async function createAccount(admin: AdminInterface, req: NDKRpcRequest) { let [ username, domain, email ] = req.params as [ string?, string?, string? ]; @@ -136,7 +164,19 @@ export default async function createAccount(admin: AdminInterface, req: NDKRpcRe } /** - * This is where the real work of creating the private key, wallet, nip-05, granting access, etc happen + * Performs the actual account creation after authorization: + * 1. Generates a new key pair for the user + * 2. Creates and saves NIP-05 verification + * 3. Sets up a lightning wallet if configured + * 4. Creates initial nostr profile + * 5. Saves the private key in the bunker's config + * 6. Grants initial permissions to the requesting client + * + * @param admin - The admin interface for managing the bunker + * @param req - The original RPC request + * @param username - Validated username + * @param domain - Validated domain + * @param email - Optional email for the account */ export async function createAccountReal( admin: AdminInterface, @@ -217,6 +257,13 @@ export async function createAccountReal( } } +/** + * Grants initial permissions to the key that created the account + * Allows for: + * - Nostr Connect authentication + * - Signing events (all kinds) + * - Encryption/decryption operations + */ async function grantPermissions(req: NDKRpcRequest, keyName: string) { await allowAllRequestsFromKey(req.pubkey, keyName, "connect"); await allowAllRequestsFromKey(req.pubkey, keyName, "sign_event", undefined, undefined, { kind: 'all' }); diff --git a/src/daemon/admin/commands/create_new_key.ts b/src/daemon/admin/commands/create_new_key.ts index 306962c..80f217e 100644 --- a/src/daemon/admin/commands/create_new_key.ts +++ b/src/daemon/admin/commands/create_new_key.ts @@ -4,27 +4,40 @@ import { saveEncrypted } from "../../../commands/add.js"; import { nip19 } from 'nostr-tools'; import { setupSkeletonProfile } from "../../lib/profile.js"; +/** + * Creates a new Nostr key or imports an existing one, saves it encrypted, and sets up a basic profile + * @param {AdminInterface} admin - The admin interface instance handling the request + * @param {NDKRpcRequest} req - The RPC request containing the parameters + * @returns {Promise} - Returns the RPC response with the new npub + * @throws {Error} If required parameters are missing or if unlockKey method is not available + */ export default async function createNewKey(admin: AdminInterface, req: NDKRpcRequest) { + // Extract parameters: keyName (for storage), passphrase (for encryption), and optional nsec (existing key) const [ keyName, passphrase, _nsec ] = req.params as [ string, string, string? ]; + // Validate required parameters if (!keyName || !passphrase) throw new Error("Invalid params"); if (!admin.loadNsec) throw new Error("No unlockKey method"); let key; if (_nsec) { + // Import existing key from nsec key = new NDKPrivateKeySigner(nip19.decode(_nsec).data as string); } else { + // Generate new key pair key = NDKPrivateKeySigner.generate(); + // Create basic profile for new key setupSkeletonProfile(key); - console.log(`setting up skeleton profile for ${keyName}`); } + // Get user information and encode private key const user = await key.user(); const nsec = nip19.nsecEncode(key.privateKey!); + // Save the encrypted key to config await saveEncrypted( admin.configFile, nsec, @@ -32,8 +45,10 @@ export default async function createNewKey(admin: AdminInterface, req: NDKRpcReq keyName ); + // Load the key into active use await admin.loadNsec(keyName, nsec); + // Prepare and send response with public key const result = JSON.stringify({ npub: user.npub, }); diff --git a/src/daemon/admin/commands/create_new_policy.ts b/src/daemon/admin/commands/create_new_policy.ts index e924c43..aa7287f 100644 --- a/src/daemon/admin/commands/create_new_policy.ts +++ b/src/daemon/admin/commands/create_new_policy.ts @@ -1,7 +1,23 @@ +/** + * Policy Creation Command Handler + * + * Manages the creation of new access policies. + * Features: + * - Policy naming and expiration + * - Rule creation and management + * - Usage tracking setup + */ + import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; import AdminInterface from "../index.js"; import prisma from "../../../db.js"; +/** + * Creates a new access policy with associated rules + * @param admin - Admin interface instance + * @param req - The RPC request containing policy details + * @returns Response indicating success + */ export default async function createNewPolicy(admin: AdminInterface, req: NDKRpcRequest) { const [ _policy ] = req.params as [ string ]; @@ -9,6 +25,7 @@ export default async function createNewPolicy(admin: AdminInterface, req: NDKRpc const policy = JSON.parse(_policy); + // Create the base policy record const policyRecord = await prisma.policy.create({ data: { name: policy.name, @@ -16,6 +33,7 @@ export default async function createNewPolicy(admin: AdminInterface, req: NDKRpc } }); + // Create associated rules for (const rule of policy.rules) { await prisma.policyRule.create({ data: { diff --git a/src/daemon/admin/commands/create_new_token.ts b/src/daemon/admin/commands/create_new_token.ts index df145a2..f5208d4 100644 --- a/src/daemon/admin/commands/create_new_token.ts +++ b/src/daemon/admin/commands/create_new_token.ts @@ -1,26 +1,57 @@ +/** + * Token Creation Command Handler + * + * Manages the creation of access tokens. + * Features: + * - Token generation + * - Policy association + * - Expiration management + * - Client tracking + */ + import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; import AdminInterface from "../index.js"; import prisma from "../../../db.js"; +/** + * Creates a new access token with associated policy + * @param admin - Admin interface instance + * @param req - The RPC request containing token details + * @returns Response indicating success + */ export default async function createNewToken(admin: AdminInterface, req: NDKRpcRequest) { const [ keyName, clientName, policyId, durationInHours ] = req.params as [ string, string, string, string? ]; if (!clientName || !policyId) throw new Error("Invalid params"); - const policy = await prisma.policy.findUnique({ where: { id: parseInt(policyId) }, include: { rules: true } }); + // Validate policy exists + const policy = await prisma.policy.findUnique({ + where: { id: parseInt(policyId) }, + include: { rules: true } + }); if (!policy) throw new Error("Policy not found"); console.log({clientName, policy, durationInHours}); + // Generate random token const token = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); + + // Prepare token data const data: any = { - keyName, clientName, policyId, + keyName, + clientName, + policyId, createdBy: req.pubkey, token }; - if (durationInHours) data.expiresAt = new Date(Date.now() + (parseInt(durationInHours) * 60 * 60 * 1000)); + // Add expiration if duration specified + if (durationInHours) { + data.expiresAt = new Date(Date.now() + (parseInt(durationInHours) * 60 * 60 * 1000)); + } + + // Create token record const tokenRecord = await prisma.token.create({data}); if (!tokenRecord) throw new Error("Token not created"); diff --git a/src/daemon/admin/commands/rename_key_user.ts b/src/daemon/admin/commands/rename_key_user.ts index f2c4ab3..c65fb5c 100644 --- a/src/daemon/admin/commands/rename_key_user.ts +++ b/src/daemon/admin/commands/rename_key_user.ts @@ -1,26 +1,37 @@ +/** + * Key User Rename Command Handler + * + * Manages the renaming of key users. + * Features: + * - User description updates + * - Key association management + */ + import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; import AdminInterface from "../index.js"; import prisma from "../../../db.js"; +/** + * Updates the description for a key user + * @param admin - Admin interface instance + * @param req - The RPC request containing new name + * @returns Response indicating success + */ export default async function renameKeyUser(admin: AdminInterface, req: NDKRpcRequest) { - const [ keyUserPubkey, name ] = req.params as [ string, string ]; + const [ keyUserId, description ] = req.params as [ string, string ]; - if (!keyUserPubkey || !name) throw new Error("Invalid params"); + if (!keyUserId || !description) throw new Error("Invalid params"); - const keyUser = await prisma.keyUser.findFirst({ - where: { - userPubkey: keyUserPubkey, - } - }); - - if (!keyUser) throw new Error("Key user not found"); + const keyUserIdInt = parseInt(keyUserId); + if (isNaN(keyUserIdInt)) throw new Error("Invalid params"); + // Update user description await prisma.keyUser.update({ where: { - id: keyUser.id, + id: keyUserIdInt, }, data: { - description: name, + description, } }); diff --git a/src/daemon/admin/commands/revoke_user.ts b/src/daemon/admin/commands/revoke_user.ts index 409015e..768726c 100644 --- a/src/daemon/admin/commands/revoke_user.ts +++ b/src/daemon/admin/commands/revoke_user.ts @@ -1,7 +1,23 @@ +/** + * User Revocation Command Handler + * + * Manages the revocation of user access. + * Features: + * - User access revocation + * - Timestamp tracking + * - Permission removal + */ + import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; import AdminInterface from "../index.js"; import prisma from "../../../db.js"; +/** + * Revokes access for a specific user + * @param admin - Admin interface instance + * @param req - The RPC request containing user ID + * @returns Response indicating success + */ export default async function revokeUser(admin: AdminInterface, req: NDKRpcRequest) { const [ keyUserId ] = req.params as [ string ]; @@ -10,6 +26,7 @@ export default async function revokeUser(admin: AdminInterface, req: NDKRpcReque const keyUserIdInt = parseInt(keyUserId); if (isNaN(keyUserIdInt)) throw new Error("Invalid params"); + // Update user record with revocation timestamp await prisma.keyUser.update({ where: { id: keyUserIdInt, diff --git a/src/daemon/admin/commands/unlock_key.ts b/src/daemon/admin/commands/unlock_key.ts index 03c8cbd..bf479c0 100644 --- a/src/daemon/admin/commands/unlock_key.ts +++ b/src/daemon/admin/commands/unlock_key.ts @@ -1,6 +1,14 @@ import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; import AdminInterface from "../index.js"; +/** + * Handles key unlocking requests for the admin interface. + * + * @param {AdminInterface} admin - The admin interface instance handling the request + * @param {NDKRpcRequest} req - The RPC request containing the key name and passphrase + * @returns {Promise} A promise that resolves when the response is sent + * @throws {Error} If params are invalid or unlockKey method is not available + */ export default async function unlockKey(admin: AdminInterface, req: NDKRpcRequest) { const [ keyName, passphrase ] = req.params as [ string, string ]; diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts index 4db9cc2..810715c 100644 --- a/src/daemon/admin/index.ts +++ b/src/daemon/admin/index.ts @@ -33,10 +33,17 @@ export type IAdminOpts = { const allowNewKeys = true; /** - * This class represents the admin interface for the nsecbunker daemon. - * - * It provides an interface for a UI to manage the daemon over nostr. + * Admin Interface Implementation + * + * This class provides the administrative interface for managing the nsecBunker daemon. + * It handles: + * - Request validation from admins + * - Key management + * - Token management + * - User management + * - Policy enforcement */ + class AdminInterface { private npubs: string[]; private ndk: NDK; @@ -162,21 +169,31 @@ class AdminInterface { } } + /** + * Validates incoming requests to ensure they're from authorized admins + * Allows create_account requests if new keys are enabled + * @param req - The incoming RPC request + */ private async validateRequest(req: NDKRpcRequest): Promise { - // if this request is of type create_account, allow it - // TODO: require some POW to prevent spam + // Allow create_account requests if enabled if (req.method === 'create_account' && allowNewKeys) { console.log(`allowing create_account request`); return; } + // Validate admin privileges if (!await validateRequestFromAdmin(req, this.npubs)) { throw new Error('You are not designated to administrate this bunker'); } } /** - * Command to list tokens + * Retrieves token information for a specific key + * Returns formatted token data including: + * - Token status + * - Policy information + * - User details + * @param req - The RPC request containing key name */ private async reqGetKeyTokens(req: NDKRpcRequest) { const keyName = req.params[0]; @@ -192,6 +209,7 @@ class AdminInterface { }, }); + // Format and return token data const keys = await this.getKeys!(); const key = keys.find((k) => k.name === keyName); @@ -201,22 +219,20 @@ class AdminInterface { const npub = key.npub; - const result = JSON.stringify(tokens.map((t) => { - return { - id: t.id, - key_name: t.keyName, - client_name: t.clientName, - token: [ npub, t.token ].join('#'), - policy_id: t.policyId, - policy_name: t.policy?.name, - created_at: t.createdAt, - updated_at: t.updatedAt, - expires_at: t.expiresAt, - redeemed_at: t.redeemedAt, - redeemed_by: t.KeyUser?.description, - time_until_expiration: t.expiresAt ? (t.expiresAt.getTime() - Date.now()) / 1000 : null, - }; - })); + const result = JSON.stringify(tokens.map((t) => ({ + id: t.id, + key_name: t.keyName, + client_name: t.clientName, + token: [ npub, t.token ].join('#'), + policy_id: t.policyId, + policy_name: t.policy?.name, + created_at: t.createdAt, + updated_at: t.updatedAt, + expires_at: t.expiresAt, + redeemed_at: t.redeemedAt, + redeemed_by: t.KeyUser?.description, + time_until_expiration: t.expiresAt ? (t.expiresAt.getTime() - Date.now()) / 1000 : null, + }))); return this.rpc.sendResponse(req.id, req.pubkey, result, 24134); } diff --git a/src/daemon/admin/validations/request-from-admin.ts b/src/daemon/admin/validations/request-from-admin.ts index 38ccd04..b0fc59c 100644 --- a/src/daemon/admin/validations/request-from-admin.ts +++ b/src/daemon/admin/validations/request-from-admin.ts @@ -1,6 +1,22 @@ +/** + * Admin Request Validation Module + * + * Validates incoming requests to ensure they come from authorized administrators. + * Handles: + * - Pubkey validation + * - Permission checking + * - Request authentication + */ + import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; +/** + * Validates that a request comes from an authorized admin + * @param req - The incoming RPC request + * @param npubs - List of authorized admin npubs + * @returns boolean indicating if request is valid + */ export async function validateRequestFromAdmin( req: NDKRpcRequest, npubs: string[], @@ -12,6 +28,7 @@ export async function validateRequestFromAdmin( return false; } + // Convert npubs to hex format for comparison const hexpubkeys = npubs.map((npub) => nip19.decode(npub).data as string); return hexpubkeys.includes(hexpubkey); diff --git a/src/daemon/authorize.ts b/src/daemon/authorize.ts index cb9721b..de12c73 100644 --- a/src/daemon/authorize.ts +++ b/src/daemon/authorize.ts @@ -10,7 +10,14 @@ let baseUrl: string | undefined | null; /** * Attempts to contact an admin to approve this request. * - * Returns a promise that is resolved with true|false when a response has been received. + * @param admin - The admin interface instance to handle authorization + * @param keyName - Optional identifier for the key being authorized + * @param remotePubkey - The public key of the remote party requesting authorization + * @param requestId - Unique identifier for this authorization request + * @param method - The method being requested + * @param param - Optional parameters for the request, can be a string or NDKEvent + * @returns Promise resolving to a string when authorization is complete + * @throws Will reject if authorization is denied */ export async function requestAuthorization( admin: AdminInterface, @@ -38,6 +45,17 @@ export async function requestAuthorization( }); } +/** + * Handles the authorization flow when communicating directly with an admin + * + * @param adminInterface - The admin interface instance + * @param keyName - Optional identifier for the key being authorized + * @param remotePubkey - The public key of the remote party + * @param method - The method being requested + * @param param - Optional parameters for the request + * @param resolve - Promise resolution callback + * @param reject - Promise rejection callback + */ async function adminAuthFlow(adminInterface, keyName, remotePubkey, method, param, resolve, reject) { const requestedPerm = await adminInterface.requestPermission(keyName, remotePubkey, method, param); @@ -50,6 +68,16 @@ async function adminAuthFlow(adminInterface, keyName, remotePubkey, method, para } } +/** + * Creates a database record for the authorization request + * + * @param keyName - Optional identifier for the key being authorized + * @param requestId - Unique identifier for this authorization request + * @param remotePubkey - The public key of the remote party + * @param method - The method being requested + * @param param - Optional parameters for the request + * @returns The created request record + */ async function createRecord( keyName: string | undefined, requestId: string, @@ -82,6 +110,17 @@ async function createRecord( return request; } +/** + * Handles the authorization flow when using a web-based approval process + * + * @param baseUrl - Base URL for the authorization endpoint + * @param admin - The admin interface instance + * @param remotePubkey - The public key of the remote party + * @param requestId - Unique identifier for this authorization request + * @param request - The request record from the database + * @param resolve - Promise resolution callback + * @param reject - Promise rejection callback + */ export function urlAuthFlow( baseUrl: string, admin: AdminInterface, @@ -121,6 +160,13 @@ export function urlAuthFlow( }, 100); } +/** + * Generates the URL for a pending authorization request + * + * @param baseUrl - Base URL for the authorization endpoint + * @param request - The request record from the database + * @returns The complete URL for the authorization request + */ function generatePendingAuthUrl(baseUrl: string, request: Request): string { return [ baseUrl, diff --git a/src/daemon/backend/index.ts b/src/daemon/backend/index.ts index 7661a09..2a2707e 100644 --- a/src/daemon/backend/index.ts +++ b/src/daemon/backend/index.ts @@ -2,6 +2,17 @@ import NDK, { NDKNip46Backend, NDKPrivateKeySigner, Nip46PermitCallback } from ' import prisma from '../../db.js'; import type {FastifyInstance} from "fastify"; +/** + * Backend Service Implementation + * + * This file implements the core backend functionality for the nsecBunker service. + * It handles: + * - Token validation and application + * - User authentication + * - Permission management + * - Integration with NDK (Nostr Development Kit) + */ + export class Backend extends NDKNip46Backend { public baseUrl?: string; public fastify: FastifyInstance; @@ -37,17 +48,29 @@ export class Backend extends NDKNip46Backend { return tokenRecord; } + /** + * Applies a token to a user, setting up their permissions + * Flow: + * 1. Validates the provided token + * 2. Creates or updates user record + * 3. Sets up basic connection permissions + * 4. Applies policy rules from token + * + * @param userPubkey - The user's public key + * @param token - The token to apply + */ async applyToken(userPubkey: string, token: string): Promise { const tokenRecord = await this.validateToken(token); const keyName = tokenRecord.keyName; - // Upsert the KeyUser with the given remotePubkey + // Create or update user record const upsertedUser = await prisma.keyUser.upsert({ where: { unique_key_user: { keyName, userPubkey } }, update: { }, create: { keyName, userPubkey, description: tokenRecord.clientName }, }); + // Set up basic connect permission await prisma.signingCondition.create({ data: { keyUserId: upsertedUser.id, @@ -56,7 +79,7 @@ export class Backend extends NDKNip46Backend { } }); - // Go through the rules of this policy and apply them to the user + // Apply policy rules for (const rule of tokenRecord!.policy!.rules) { const signingConditionQuery: any = { method: rule.method }; @@ -74,6 +97,7 @@ export class Backend extends NDKNip46Backend { }); } + // Update token status await prisma.token.update({ where: { id: tokenRecord.id }, data: { diff --git a/src/daemon/backend/publish-event.ts b/src/daemon/backend/publish-event.ts index 3302523..eb97f91 100644 --- a/src/daemon/backend/publish-event.ts +++ b/src/daemon/backend/publish-event.ts @@ -1,7 +1,19 @@ import { NDKNip46Backend } from "@nostr-dev-kit/ndk"; import { IEventHandlingStrategy } from '@nostr-dev-kit/ndk'; +/** + * Strategy for handling event publication requests in a Nostr NIP-46 backend. + * Implements the IEventHandlingStrategy interface for processing publish commands. + */ export default class PublishEventHandlingStrategy implements IEventHandlingStrategy { + /** + * Handles the publication of a Nostr event. + * @param backend - The NIP-46 backend instance handling the request + * @param id - The request identifier + * @param remotePubkey - The public key of the remote client + * @param params - Array of parameters for the event creation + * @returns A promise that resolves to the stringified Nostr event or undefined if signing fails + */ async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]): Promise { const event = await backend.signEvent(remotePubkey, params); if (!event) return undefined; diff --git a/src/daemon/lib/acl/index.ts b/src/daemon/lib/acl/index.ts index 42af57d..c9fe802 100644 --- a/src/daemon/lib/acl/index.ts +++ b/src/daemon/lib/acl/index.ts @@ -1,6 +1,17 @@ import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk'; import prisma from '../../../db.js'; +/** + * Access Control List Implementation + * + * This module handles permission management and request validation. + * Features: + * - Method-specific permissions + * - Event kind filtering + * - Token-based access control + * - User permission management + */ + export async function checkIfPubkeyAllowed( keyName: string, remotePubkey: string, @@ -74,6 +85,12 @@ export type IAllowScope = { kind?: number | 'all'; }; +/** + * Converts a request into a signing condition query + * Handles special cases for different methods, especially sign_event + * @param method - The requested method + * @param payload - Optional payload or NostrEvent + */ export function requestToSigningConditionQuery(method: IMethod, payload?: string | NostrEvent) { const signingConditionQuery: any = { method }; @@ -86,6 +103,12 @@ export function requestToSigningConditionQuery(method: IMethod, payload?: string return signingConditionQuery; } +/** + * Converts an allow scope into a signing condition query + * Used for creating new permissions + * @param method - The method to allow + * @param scope - Optional scope restrictions + */ export function allowScopeToSigningConditionQuery(method: string, scope?: IAllowScope) { const signingConditionQuery: any = { method }; @@ -96,6 +119,16 @@ export function allowScopeToSigningConditionQuery(method: string, scope?: IAllow return signingConditionQuery; } +/** + * Grants permissions to a key for specific methods + * Creates or updates user record and signing conditions + * @param remotePubkey - The public key to grant permissions to + * @param keyName - The key name to grant permissions for + * @param method - The method to allow + * @param param - Optional parameters + * @param description - Optional user description + * @param allowScope - Optional scope restrictions + */ export async function allowAllRequestsFromKey( remotePubkey: string, keyName: string, @@ -105,14 +138,14 @@ export async function allowAllRequestsFromKey( allowScope?: IAllowScope, ): Promise { try { - // Upsert the KeyUser with the given remotePubkey + // Upsert the KeyUser record const upsertedUser = await prisma.keyUser.upsert({ where: { unique_key_user: { keyName, userPubkey: remotePubkey } }, update: { }, create: { keyName, userPubkey: remotePubkey, description }, }); - // Create a new SigningCondition for the given KeyUser and set allowed to true + // Create signing condition const signingConditionQuery = allowScopeToSigningConditionQuery(method, allowScope); await prisma.signingCondition.create({ data: { diff --git a/src/daemon/lib/profile.ts b/src/daemon/lib/profile.ts index f433452..c4004a2 100644 --- a/src/daemon/lib/profile.ts +++ b/src/daemon/lib/profile.ts @@ -1,9 +1,23 @@ +/** + * Profile Management Module + * + * Handles user profile setup and management. + * Features: + * - Profile creation + * - Relay configuration + * - Profile encryption + */ + import NDK, { NDKEvent, NDKPrivateKeySigner, NostrEvent, type NDKUserProfile } from "@nostr-dev-kit/ndk"; import * as CryptoJS from 'crypto-js'; import createDebug from "debug"; const debug = createDebug("nsecbunker:profile"); +/** + * Default relay list for new profiles + * Includes popular and reliable relays + */ const explicitRelayUrls = [ 'wss://purplepag.es', 'wss://relay.damus.io', diff --git a/src/daemon/run.ts b/src/daemon/run.ts index 262a150..904a6f4 100644 --- a/src/daemon/run.ts +++ b/src/daemon/run.ts @@ -140,6 +140,18 @@ export default async function run(config: DaemonConfig) { await daemon.start(); } +/** + * Daemon Service Runner + * + * This class initializes and manages the main nsecBunker daemon process. + * Responsibilities: + * - Configuration management + * - Key management and encryption + * - Web server setup + * - Admin interface initialization + * - Nostr relay connections + */ + class Daemon { private config: DaemonConfig; private activeKeys: Record; @@ -147,35 +159,54 @@ class Daemon { private ndk: NDK; public fastify: FastifyInstance; + /** + * Initialize daemon with configuration + * Sets up: + * - Admin interface + * - Web server (Fastify) + * - NDK connection + */ constructor(config: DaemonConfig) { this.config = config; this.activeKeys = config.keys; this.adminInterface = new AdminInterface(config.admin, config.configFile); + // Set up admin interface methods this.adminInterface.getKeys = getKeys(config); this.adminInterface.getKeyUsers = getKeyUsers(config); this.adminInterface.unlockKey = this.unlockKey.bind(this); this.adminInterface.loadNsec = this.loadNsec.bind(this); + // Initialize web server this.fastify = Fastify({ logger: true }); this.fastify.register(FastifyFormBody); + // Set up NDK connection this.ndk = new NDK({ explicitRelayUrls: config.nostr.relays, }); + + // Set up relay event handlers this.ndk.pool.on('relay:connect', (r) => console.log(`✅ Connected to ${r.url}`) ); this.ndk.pool.on('relay:notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); }); - this.ndk.pool.on('relay:disconnect', (r) => { console.log(`🚫 Disconnected from ${r.url}`); }); } + /** + * Initializes the web authentication server if configured + * Sets up routes for: + * - Request authorization + * - Request processing + * - User registration + */ async startWebAuth() { if (!this.config.authPort) return; const urlPrefix = new URL(this.config.baseUrl as string).pathname.replace(/\/+$/, ''); + // Configure view engine this.fastify.register(FastifyView, { engine: { handlebars: Handlebars, @@ -185,8 +216,10 @@ class Daemon { } }); + // Start server this.fastify.listen({ port: this.config.authPort, host: this.config.authHost }); + // Set up routes this.fastify.get('/requests/:id', authorizeRequestWebHandler); this.fastify.post('/requests/:id', processRequestWebHandler); this.fastify.post('/register/:id', processRegistrationWebHandler); diff --git a/src/daemon/web/authorize.ts b/src/daemon/web/authorize.ts index e54fa2a..c54ac24 100644 --- a/src/daemon/web/authorize.ts +++ b/src/daemon/web/authorize.ts @@ -1,3 +1,13 @@ +/** + * Authorization and Registration Handler + * + * This file handles web-based authorization flows including: + * - User authentication via cookies or username/password + * - Request validation and processing + * - New account registration + * - Permission management for nostr keys + */ + import prisma from "../../db"; import bcrypt from "bcrypt"; import { IAllowScope, allowAllRequestsFromKey } from "../lib/acl"; @@ -7,7 +17,9 @@ import { validateRegistration } from "./registration-validations"; const debug = createDebug("nsecbunker:authorize"); /** - * TODO: This is still nto being used as no JWT is ever created + * Validates a JWT cookie for user authentication + * @param request - The incoming request object containing cookies + * @returns boolean - True if cookie is valid, false otherwise */ async function validateAuthCookie(request) { const cookies = request.cookies || {}; @@ -28,6 +40,12 @@ async function validateAuthCookie(request) { return true; } +/** + * Retrieves and validates a request record from the database + * @param request - Request containing the ID parameter + * @throws Error if request not found or already processed + * @returns The validated request record + */ async function getAndValidateStateOfRequest(request) { const record = await prisma.request.findUnique({ where: { id: request.params.id } @@ -40,9 +58,13 @@ async function getAndValidateStateOfRequest(request) { return record; } - /** - * Generates the view to authorize a request + * Web handler for displaying the authorization UI + * Routes to different templates based on request type: + * - create_account -> createAccount template + * - other methods -> authorizeRequest template + * @param request - The incoming HTTP request + * @param reply - The reply object for rendering views */ export async function authorizeRequestWebHandler(request, reply) { try { @@ -71,7 +93,12 @@ export async function authorizeRequestWebHandler(request, reply) { } /** - * Validates + authenticates a request POSTed to the authorize endpoint + * Validates user authentication for a request + * Checks either cookie auth or username/password combo + * @param request - The incoming request with auth details + * @param record - The request record to validate against + * @returns The user record if valid + * @throws Error if validation fails */ export async function validateRequest(request, record) { if (await validateAuthCookie(request)) { @@ -108,6 +135,16 @@ export async function validateRequest(request, record) { return userRecord; } +/** + * Processes an authorization request after validation + * Steps: + * 1. Validates request and user credentials + * 2. Marks request as allowed + * 3. Sets up permissions for the remote pubkey + * 4. Adds sign_event capability for connect requests + * @param request - The incoming HTTP request + * @param reply - The reply object for responses + */ export async function processRequestWebHandler(request, reply) { const record = await prisma.request.findUnique({ where: { id: request.params.id } @@ -159,6 +196,18 @@ export async function processRequestWebHandler(request, reply) { return { ok: true, pubkey: userRecord.pubkey }; } +/** + * Handles new user registration requests + * Flow: + * 1. Validates registration data + * 2. Updates request with allowed status + * 3. Waits for key generation + * 4. Creates user record + * 5. Sets up permissions + * 6. Handles redirect if callback URL provided + * @param request - The registration request + * @param reply - The reply object for responses + */ export async function processRegistrationWebHandler(request, reply) { try { const record = await getAndValidateStateOfRequest(request); @@ -250,6 +299,16 @@ export async function processRegistrationWebHandler(request, reply) { } } +/** + * Helper function to create a new user record + * Creates a hashed password and stores user details in database + * @param username - User's username + * @param domain - User's domain + * @param pubkey - User's public key + * @param email - User's email address + * @param password - User's plain text password + * @returns The created user record + */ async function createUserRecord( username: string, domain: string, diff --git a/src/daemon/web/registration-validations.ts b/src/daemon/web/registration-validations.ts index 182b65c..e8795fd 100644 --- a/src/daemon/web/registration-validations.ts +++ b/src/daemon/web/registration-validations.ts @@ -1,25 +1,49 @@ +/** + * Registration Validation Module + * + * Handles validation of new user registrations. + * Validates: + * - Username uniqueness + * - Password strength + * - Email format and uniqueness + */ + import prisma from "../../db"; +/** + * Validates a new user registration request + * @param request - The registration request + * @param record - The existing request record + * @throws Error if validation fails + */ export async function validateRegistration(request, record) { - // validate username uniqueness const body = request.body; const { username, domain, email, password } = body; + // Check username uniqueness const userRecord = await prisma.user.findUnique({ where: { username, domain } }); - if (userRecord) throw new Error("Username already exists. If this is your account, please login instead."); + if (userRecord) { + throw new Error("Username already exists. If this is your account, please login instead."); + } - // validate password length - if (password.length < 8) throw new Error("Password is too short"); + // Validate password strength + if (password.length < 8) { + throw new Error("Password is too short"); + } - // validate email (if present) + // Validate email if provided if (email) { - if (!email.includes("@")) throw new Error("Invalid email address"); + if (!email.includes("@")) { + throw new Error("Invalid email address"); + } - // validate email uniqueness (if one was provided) + // Check email uniqueness const emailRecord = await prisma.user.findFirst({ where: { email } }); - if (emailRecord) throw new Error("Email already exists"); + if (emailRecord) { + throw new Error("Email already exists"); + } } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c8b3620..d0d4991 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,22 @@ #!/usr/bin/env node + +/** + * nsecBunker - A secure key management system for Nostr + * + * @description + * A CLI application that provides secure management of Nostr private keys (nsec). + * The bunker acts as a secure gateway, allowing controlled access to signing operations + * while keeping private keys encrypted at rest. + * + * @commands + * - setup: Initialize a new nsecBunker configuration file + * - start: Launch the nsecBunker service daemon + * - add: Securely store a new private key in the bunker + * + * @environment + * ADMIN_NPUBS - Comma-separated list of administrator public keys (npub) + */ + import 'websocket-polyfill'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -7,13 +25,36 @@ import { setup } from './commands/setup.js'; import { addNsec } from './commands/add.js'; import { start } from './commands/start.js'; +/** + * Administrator public keys can be specified via ADMIN_NPUBS environment variable + * Format: npub1,npub2,npub3 + */ const adminNpubs = process.env.ADMIN_NPUBS ? process.env.ADMIN_NPUBS.split(',') : []; const argv = yargs(hideBin(process.argv)) + /** + * Setup Command + * Initializes a new nsecBunker configuration with secure defaults + * + * Generated config includes: + * - admin.key: Auto-generated admin authentication key + * - nostr.relays: List of default Nostr relays + * - database: URI for persistent storage + * - logs: Path for application logs + */ .command('setup', 'Setup nsecBunker', {}, (argv) => { setup(argv.config as string); }) + /** + * Start Command + * Launches the nsecBunker service with specified configuration + * + * @options + * --verbose, -v: Enable detailed logging output + * --key : Enable specific named keys (can specify multiple) + * --admin, -a : Additional admin public keys (can specify multiple) + */ .command('start', 'Start nsecBunker', (yargs) => { yargs .option('verbose', { @@ -35,13 +76,34 @@ const argv = yargs(hideBin(process.argv)) }); }, (argv) => { start({ - keys: argv.key as string[], - verbose: argv.verbose as boolean, - config: argv.config as string, - adminNpubs: [...new Set([...((argv.admin||[]) as string[]), ...adminNpubs])] + keys: argv.key as string[], // List of specific keys to enable + verbose: argv.verbose as boolean, // Verbose logging flag + config: argv.config as string, // Configuration file path + adminNpubs: [...new Set([...((argv.admin||[]) as string[]), ...adminNpubs])] // Deduplicated admin NPUBs }); }) + /** + * Add Command + * Securely stores a new private key in the bunker + * + * Storage format in config: + * ```json + * { + * "keys": { + * "": { + * "iv": "initialization vector", + * "data": "encrypted key data", + * // or + * "key": "unencrypted key (if encryption disabled)" + * } + * } + * } + * ``` + * + * @options + * --name, -n: Unique identifier for the key (required) + */ .command('add', 'Add an nsec', (yargs) => { yargs .option('name', { @@ -57,6 +119,7 @@ const argv = yargs(hideBin(process.argv)) }); }) + // Global options available to all commands .options({ 'config': { alias: 'c', diff --git a/src/utils/dm-user.ts b/src/utils/dm-user.ts index 0f922e4..bd21ce6 100644 --- a/src/utils/dm-user.ts +++ b/src/utils/dm-user.ts @@ -1,18 +1,36 @@ +/** + * Direct Message Utility + * + * Handles sending encrypted direct messages to users via Nostr. + * Supports both npub and NDKUser recipients. + */ + import NDK, { NDKUser, NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk"; +/** + * Sends an encrypted direct message to a user + * @param ndk - NDK instance for sending messages + * @param recipient - Target user (npub string or NDKUser) + * @param content - Message content to send + * @returns The sent NDKEvent + */ export async function dmUser(ndk: NDK, recipient: NDKUser | string, content: string): Promise { let targetUser; + // Convert string recipient to NDKUser if needed if (typeof recipient === 'string') { targetUser = new NDKUser({ npub: recipient }); } else if (recipient instanceof NDKUser) { targetUser = recipient; } + // Create and encrypt the event const event = new NDKEvent(ndk, { kind: 4, content } as NostrEvent); event.tag(targetUser); await event.encrypt(targetUser); await event.sign(); + + // Attempt to publish try { await event.publish(); } catch (e) {