mirror of
https://github.com/kind-0/nsecbunkerd.git
synced 2026-01-03 06:24:53 +00:00
adding a bunch of documentation to make sense of this tool
This commit is contained in:
parent
114cb73ef5
commit
9dfd83f62e
27
Dockerfile
27
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"]
|
||||
@ -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 <command> <remote-npub> <content> [--dont-publish] [--debug] [--pk <key>]');
|
||||
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<NDK> {
|
||||
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<NDK> {
|
||||
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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<string, string>;
|
||||
@ -33,21 +41,41 @@ export interface DomainConfig {
|
||||
defaultProfile?: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<string, any>;
|
||||
// URL for OAuth-like authentication access
|
||||
baseUrl?: string;
|
||||
// Enable detailed logging when true
|
||||
verbose: boolean;
|
||||
// Allowed domains for user creation
|
||||
domains?: Record<string, DomainConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<IConfig> {
|
||||
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<IConfig> {
|
||||
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};
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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<void>} - 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,
|
||||
});
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<void>} 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 ];
|
||||
|
||||
|
||||
@ -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<void> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<void> {
|
||||
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: {
|
||||
|
||||
@ -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<string|undefined> {
|
||||
const event = await backend.signEvent(remotePubkey, params);
|
||||
if (!event) return undefined;
|
||||
|
||||
@ -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<void> {
|
||||
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: {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<string, any>;
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/index.ts
71
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 <name>: Enable specific named keys (can specify multiple)
|
||||
* --admin, -a <npub>: 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": {
|
||||
* "<keyId>": {
|
||||
* "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',
|
||||
|
||||
@ -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<NDKEvent> {
|
||||
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user