add nostr signer setup tools

cleanup bakery config
This commit is contained in:
hzrd149
2025-03-28 08:34:51 +00:00
parent 9d3c9963c8
commit 5e3e63a6ff
22 changed files with 271 additions and 204 deletions

View File

@@ -2,7 +2,7 @@
DATA_PATH=./data DATA_PATH=./data
# the port to use # the port to use
PORT=3000 PORT=9272
# the address to the tor SOCKS5 proxy to enable connections to .onion addresses # the address to the tor SOCKS5 proxy to enable connections to .onion addresses
# TOR_PROXY="127.0.0.1:9050" # TOR_PROXY="127.0.0.1:9050"

View File

@@ -9,9 +9,9 @@
"scripts": { "scripts": {
"prepack": "tsc", "prepack": "tsc",
"start": "node .", "start": "node .",
"dev": "nodemon --loader @swc-node/register/esm src/index.ts", "dev": "DATA_PATH=./data nodemon --loader @swc-node/register/esm src/index.ts",
"mcp": "mcp-inspector node . --mcp --port 8080", "mcp": "mcp-inspector node . --mcp",
"mcp-debug": "mcp-inspector node --inspect-brk . --mcp --port 8080", "mcp-debug": "mcp-inspector node --inspect-brk . --mcp",
"build": "tsc", "build": "tsc",
"test": "vitest run", "test": "vitest run",
"format": "prettier -w .", "format": "prettier -w .",
@@ -60,6 +60,7 @@
"nostr-tools": "^2.11.0", "nostr-tools": "^2.11.0",
"pac-proxy-agent": "^7.2.0", "pac-proxy-agent": "^7.2.0",
"process-streams": "^1.0.3", "process-streams": "^1.0.3",
"qrcode-terminal": "^0.12.0",
"rx-nostr": "^3.5.0", "rx-nostr": "^3.5.0",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"streamx": "^2.22.0", "streamx": "^2.22.0",
@@ -81,6 +82,7 @@
"@types/hash-sum": "^1.0.2", "@types/hash-sum": "^1.0.2",
"@types/lodash.throttle": "^4.1.9", "@types/lodash.throttle": "^4.1.9",
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
"@types/qrcode-terminal": "^0.12.2",
"@types/web-push": "^3.6.4", "@types/web-push": "^3.6.4",
"@types/ws": "^8.18.0", "@types/ws": "^8.18.0",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",

17
pnpm-lock.yaml generated
View File

@@ -101,6 +101,9 @@ importers:
process-streams: process-streams:
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3 version: 1.0.3
qrcode-terminal:
specifier: ^0.12.0
version: 0.12.0
rx-nostr: rx-nostr:
specifier: ^3.5.0 specifier: ^3.5.0
version: 3.5.0 version: 3.5.0
@@ -159,6 +162,9 @@ importers:
'@types/node': '@types/node':
specifier: ^22.13.14 specifier: ^22.13.14
version: 22.13.14 version: 22.13.14
'@types/qrcode-terminal':
specifier: ^0.12.2
version: 0.12.2
'@types/web-push': '@types/web-push':
specifier: ^3.6.4 specifier: ^3.6.4
version: 3.6.4 version: 3.6.4
@@ -1204,6 +1210,9 @@ packages:
'@types/prismjs@1.26.5': '@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
'@types/qrcode-terminal@0.12.2':
resolution: {integrity: sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==}
'@types/qs@6.9.18': '@types/qs@6.9.18':
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
@@ -2663,6 +2672,10 @@ packages:
pump@3.0.2: pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
qrcode-terminal@0.12.0:
resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==}
hasBin: true
qs@6.13.0: qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
@@ -4406,6 +4419,8 @@ snapshots:
'@types/prismjs@1.26.5': {} '@types/prismjs@1.26.5': {}
'@types/qrcode-terminal@0.12.2': {}
'@types/qs@6.9.18': {} '@types/qs@6.9.18': {}
'@types/range-parser@1.2.7': {} '@types/range-parser@1.2.7': {}
@@ -6176,6 +6191,8 @@ snapshots:
end-of-stream: 1.4.4 end-of-stream: 1.4.4
once: 1.4.0 once: 1.4.0
qrcode-terminal@0.12.0: {}
qs@6.13.0: qs@6.13.0:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0

View File

@@ -14,7 +14,6 @@ import Database from "./database.js";
import { NIP_11_SOFTWARE_URL, SENSITIVE_KINDS } from "../const.js"; import { NIP_11_SOFTWARE_URL, SENSITIVE_KINDS } from "../const.js";
import { OWNER_PUBKEY, PORT } from "../env.js"; import { OWNER_PUBKEY, PORT } from "../env.js";
import ConfigManager from "../modules/config/config-manager.js";
import ControlApi from "../modules/control/control-api.js"; import ControlApi from "../modules/control/control-api.js";
import ConfigActions from "../modules/control/config-actions.js"; import ConfigActions from "../modules/control/config-actions.js";
import ReceiverActions from "../modules/control/receiver-actions.js"; import ReceiverActions from "../modules/control/receiver-actions.js";
@@ -43,7 +42,7 @@ import Switchboard from "../modules/switchboard/switchboard.js";
import Gossip from "../modules/gossip.js"; import Gossip from "../modules/gossip.js";
import database from "../services/database.js"; import database from "../services/database.js";
import secrets from "../services/secrets.js"; import secrets from "../services/secrets.js";
import config from "../services/config.js"; import bakeryConfig from "../services/config.js";
import logStore from "../services/log-store.js"; import logStore from "../services/log-store.js";
import stateManager from "../services/state.js"; import stateManager from "../services/state.js";
import eventCache from "../services/event-cache.js"; import eventCache from "../services/event-cache.js";
@@ -63,7 +62,7 @@ type EventMap = {
export default class App extends EventEmitter<EventMap> { export default class App extends EventEmitter<EventMap> {
running = false; running = false;
config: ConfigManager; config: typeof bakeryConfig;
secrets: SecretsManager; secrets: SecretsManager;
state: ApplicationStateManager; state: ApplicationStateManager;
signer: SimpleSigner; signer: SimpleSigner;
@@ -95,18 +94,12 @@ export default class App extends EventEmitter<EventMap> {
constructor() { constructor() {
super(); super();
this.config = config; this.config = bakeryConfig;
this.secrets = secrets; this.secrets = secrets;
this.signer = bakerySigner; this.signer = bakerySigner;
// copy the vapid public key over to config so the web ui can access it
// TODO: this should be moved to another place
this.secrets.on("updated", () => {
this.config.data.vapidPublicKey = this.secrets.get("vapidPublicKey");
});
// set owner pubkey from env variable // set owner pubkey from env variable
if (!this.config.data.owner && OWNER_PUBKEY) { if (!this.config.data.owner && OWNER_PUBKEY) {
this.config.setField("owner", OWNER_PUBKEY); this.config.setField("owner", OWNER_PUBKEY);

View File

@@ -1,5 +1,6 @@
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { Adapter, Low, LowSync, SyncAdapter } from "lowdb"; import { LowSync, SyncAdapter } from "lowdb";
import { JSONFileSync } from "lowdb/node";
type EventMap<T> = { type EventMap<T> = {
/** fires when file is loaded */ /** fires when file is loaded */
@@ -11,61 +12,17 @@ type EventMap<T> = {
saved: [T]; saved: [T];
}; };
export class ReactiveJsonFile<T extends object> extends EventEmitter<EventMap<T>> implements Low<T> { export class ReactiveJsonFile<T extends object> extends EventEmitter<EventMap<T>> implements LowSync<T> {
protected db: Low<T>;
adapter: Adapter<T>;
data: T;
constructor(adapter: Adapter<T>, defaultData: T) {
super();
this.adapter = adapter;
this.db = new Low<T>(adapter, defaultData);
this.data = this.createProxy();
}
private createProxy() {
return (this.data = new Proxy(this.db.data, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set: (target, p, newValue, receiver) => {
Reflect.set(target, p, newValue, receiver);
this.emit("changed", target as T, String(p), newValue);
this.emit("updated", target as T);
return true;
},
}));
}
async read() {
await this.db.read();
this.emit("loaded", this.db.data);
this.emit("updated", this.db.data);
this.createProxy();
}
async write() {
await this.db.write();
this.emit("saved", this.db.data);
}
update(fn: (data: T) => unknown) {
return this.db.update(fn);
}
}
export class ReactiveJsonFileSync<T extends object> extends EventEmitter<EventMap<T>> implements LowSync<T> {
protected db: LowSync<T>; protected db: LowSync<T>;
adapter: SyncAdapter<T>; adapter: SyncAdapter<T>;
data: T; data: T;
constructor(adapter: SyncAdapter<T>, defaultData: T) { constructor(path: string, defaultData: T) {
super(); super();
this.adapter = adapter; this.adapter = new JSONFileSync<T>(path);
this.db = new LowSync<T>(adapter, defaultData); this.db = new LowSync<T>(this.adapter, defaultData);
this.data = this.createProxy(); this.data = this.createProxy();
} }
@@ -97,4 +54,18 @@ export class ReactiveJsonFileSync<T extends object> extends EventEmitter<EventMa
update(fn: (data: T) => unknown) { update(fn: (data: T) => unknown) {
return this.db.update(fn); return this.db.update(fn);
} }
setDefaults(defaults: Partial<T>) {
// explicitly set default values if fields are not set
for (const [key, value] of Object.entries(defaults)) {
// @ts-expect-error
if (this.data[key] === undefined) this.data[key] = value;
}
this.write();
}
setField(field: keyof T, value: T[keyof T]) {
this.data[field] = value;
this.write();
}
} }

View File

@@ -1,7 +1,10 @@
import { EncryptedDirectMessage } from "nostr-tools/kinds"; import { EncryptedDirectMessage } from "nostr-tools/kinds";
export const DEFAULT_PORT = 9272;
export const SENSITIVE_KINDS = [EncryptedDirectMessage]; export const SENSITIVE_KINDS = [EncryptedDirectMessage];
export const NIP_11_SOFTWARE_URL = "git+https://github.com/hzrd149/bakery.git"; export const NIP_11_SOFTWARE_URL = "git+https://github.com/hzrd149/bakery.git";
export const OUTBOUND_PROXY_TYPES = ["SOCKS5", "HTTP"]; export const OUTBOUND_PROXY_TYPES = ["SOCKS5", "HTTP"];
export const DEFAULT_NOSTR_CONNECT_RELAYS = ["wss://relay.nsec.app"];

View File

@@ -1,17 +1,19 @@
import "dotenv/config"; import "dotenv/config";
import { mkdirp } from "mkdirp"; import { mkdirp } from "mkdirp";
import { normalizeURL } from "applesauce-core/helpers"; import { normalizeURL } from "applesauce-core/helpers";
import { homedir } from "os";
import { join } from "path";
import { OUTBOUND_PROXY_TYPES } from "./const.js"; import { DEFAULT_PORT, OUTBOUND_PROXY_TYPES } from "./const.js";
import { normalizeToHexPubkey } from "./helpers/nip19.js"; import { normalizeToHexPubkey } from "./helpers/nip19.js";
import args from "./args.js"; import args from "./args.js";
export const OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined; export const OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined;
export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS; export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS;
export const DATA_PATH = process.env.DATA_PATH || "./data"; export const DATA_PATH = process.env.DATA_PATH || join(homedir(), ".bakery");
await mkdirp(DATA_PATH); await mkdirp(DATA_PATH);
export const PORT = parseInt(args.values.port ?? process.env.PORT ?? "") || 3000; export const PORT = parseInt(args.values.port ?? process.env.PORT ?? "") || DEFAULT_PORT;
// I2P config // I2P config
export const I2P_PROXY = process.env.I2P_PROXY; export const I2P_PROXY = process.env.I2P_PROXY;

View File

@@ -57,12 +57,14 @@ app.express.get("*", (req, res) => {
res.sendFile(path.resolve(appDir, "index.html")); res.sendFile(path.resolve(appDir, "index.html"));
}); });
// log uncaught errors // catch unhandled errors
process.on("uncaughtException", (error) => {
if (!IS_MCP) console.error("Uncaught Exception:", error);
});
// Catch unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => { process.on("unhandledRejection", (reason, promise) => {
if (reason instanceof Error) { if (!IS_MCP) console.error("Unhandled Promise Rejection:", reason);
console.log("Unhandled Rejection");
console.log(reason);
} else console.log("Unhandled Rejection at:", promise, "reason:", reason);
}); });
// start the app // start the app
@@ -87,13 +89,3 @@ async function shutdown() {
} }
process.on("SIGINT", shutdown); process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown); process.on("SIGTERM", shutdown);
// catch unhandled errors
process.on("uncaughtException", (error) => {
if (!IS_MCP) console.error("Uncaught Exception:", error);
});
// 2. Catch unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
if (!IS_MCP) console.error("Unhandled Promise Rejection:", reason);
});

View File

@@ -1,61 +0,0 @@
import { JSONFileSync } from "lowdb/node";
import _throttle from "lodash.throttle";
import { uniqueNamesGenerator, adjectives, colors, animals } from "unique-names-generator";
import { PrivateNodeConfig } from "@satellite-earth/core/types/private-node-config.js";
import { logger } from "../../logger.js";
import { ReactiveJsonFileSync } from "../../classes/json-file.js";
export const defaultConfig: PrivateNodeConfig = {
name: uniqueNamesGenerator({
dictionaries: [colors, adjectives, animals],
}),
description: "",
autoListen: false,
runReceiverOnBoot: true,
runScrapperOnBoot: false,
logsEnabled: true,
requireReadAuth: false,
publicAddresses: [],
hyperEnabled: false,
enableTorConnections: true,
enableI2PConnections: true,
enableHyperConnections: false,
routeAllTrafficThroughTor: false,
gossipEnabled: false,
gossipInterval: 10 * 60_000,
gossipBroadcastRelays: [],
};
export default class ConfigManager extends ReactiveJsonFileSync<PrivateNodeConfig> {
log = logger.extend("ConfigManager");
constructor(path: string) {
super(new JSONFileSync(path), defaultConfig);
this.on("loaded", (config) => {
// explicitly set default values if fields are not set
for (const [key, value] of Object.entries(defaultConfig)) {
// @ts-expect-error
if (config[key] === undefined) {
// @ts-expect-error
config[key] = value;
}
}
this.write();
});
}
setField(field: keyof PrivateNodeConfig, value: any) {
this.log(`Setting ${field} to ${value}`);
// @ts-expect-error
this.data[field] = value;
this.write();
}
}

View File

@@ -35,6 +35,7 @@ export default class ConfigActions implements ControlMessageHandler {
const field = message[3]; const field = message[3];
const value = message[4]; const value = message[4];
// @ts-expect-error
this.app.config.setField(field, value); this.app.config.setField(field, value);
return true; return true;

View File

@@ -1,8 +1,7 @@
import { Server } from "http"; import { Server } from "http";
import { logger } from "../../../logger.js"; import { logger } from "../../../logger.js";
import { getIPAddresses } from "../../../helpers/ip.js"; import { getIPAddresses } from "../../../helpers/ip.js";
import ConfigManager from "../../config/config-manager.js"; import bakeryConfig from "../../../services/config.js";
import config from "../../../services/config.js";
import TorInbound from "./tor.js"; import TorInbound from "./tor.js";
import I2PInbound from "./i2p.js"; import I2PInbound from "./i2p.js";
@@ -29,7 +28,7 @@ export default class InboundNetworkManager {
this.tor = new TorInbound(); this.tor = new TorInbound();
this.i2p = new I2PInbound(); this.i2p = new I2PInbound();
this.listenToAppConfig(config); this.listenToAppConfig(bakeryConfig);
} }
private getAddress() { private getAddress() {
@@ -41,7 +40,7 @@ export default class InboundNetworkManager {
return address; return address;
} }
private update(cfg = config.data) { private update(cfg = bakeryConfig.data) {
if (!this.running) return; if (!this.running) return;
const address = this.getAddress(); const address = this.getAddress();
@@ -60,7 +59,7 @@ export default class InboundNetworkManager {
} }
/** A helper method to make the manager run off of the app config */ /** A helper method to make the manager run off of the app config */
listenToAppConfig(config: ConfigManager) { listenToAppConfig(config: typeof bakeryConfig) {
config.on("updated", this.update.bind(this)); config.on("updated", this.update.bind(this));
} }

View File

@@ -2,10 +2,10 @@ import { PacProxyAgent } from "pac-proxy-agent";
import _throttle from "lodash.throttle"; import _throttle from "lodash.throttle";
import { logger } from "../../../logger.js"; import { logger } from "../../../logger.js";
import ConfigManager from "../../config/config-manager.js";
import HyperOutbound from "./hyper.js"; import HyperOutbound from "./hyper.js";
import TorOutbound from "./tor.js"; import TorOutbound from "./tor.js";
import I2POutbound from "./i2p.js"; import I2POutbound from "./i2p.js";
import bakeryConfig from "../../../services/config.js";
export default class OutboundNetworkManager { export default class OutboundNetworkManager {
log = logger.extend("Network:Outbound"); log = logger.extend("Network:Outbound");
@@ -95,7 +95,7 @@ function FindProxyForURL(url, host)
updateAgentThrottle: () => void = _throttle(this.updateAgent.bind(this), 100); updateAgentThrottle: () => void = _throttle(this.updateAgent.bind(this), 100);
/** A helper method to make the manager run off of the app config */ /** A helper method to make the manager run off of the app config */
listenToAppConfig(config: ConfigManager) { listenToAppConfig(config: typeof bakeryConfig) {
config.on("updated", (c) => { config.on("updated", (c) => {
this.enableHyperConnections = c.hyperEnabled && c.enableHyperConnections; this.enableHyperConnections = c.hyperEnabled && c.enableHyperConnections;
this.enableTorConnections = c.enableTorConnections; this.enableTorConnections = c.enableTorConnections;

View File

@@ -8,7 +8,7 @@ import webPush from "web-push";
import { logger } from "../../logger.js"; import { logger } from "../../logger.js";
import App from "../../app/index.js"; import App from "../../app/index.js";
import stateManager from "../../services/state.js"; import stateManager from "../../services/state.js";
import config from "../../services/config.js"; import bakeryConfig from "../../services/config.js";
import { getDMRecipient, getDMSender } from "../../helpers/direct-messages.js"; import { getDMRecipient, getDMSender } from "../../helpers/direct-messages.js";
export type NotificationsManagerState = { export type NotificationsManagerState = {
@@ -73,7 +73,7 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
/** Whether a notification should be sent */ /** Whether a notification should be sent */
shouldNotify(event: NostrEvent) { shouldNotify(event: NostrEvent) {
if (event.kind !== kinds.EncryptedDirectMessage) return; if (event.kind !== kinds.EncryptedDirectMessage) return;
if (getDMRecipient(event) !== config.data.owner) return; if (getDMRecipient(event) !== bakeryConfig.data.owner) return;
if (event.created_at > this.lastRead) return true; if (event.created_at > this.lastRead) return true;
} }

View File

@@ -1,13 +1,12 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { PrivateNodeConfig } from "@satellite-earth/core/types";
import { Query } from "../types.js"; import { Query } from "../types.js";
import config from "../../../services/config.js"; import bakeryConfig, { BakeryConfig } from "../../../services/config.js";
export const ConfigQuery: Query<PrivateNodeConfig> = () => export const ConfigQuery: Query<BakeryConfig> = () =>
new Observable((observer) => { new Observable((observer) => {
observer.next(config.data); observer.next(bakeryConfig.data);
const listener = (c: PrivateNodeConfig) => observer.next(c); const listener = (c: BakeryConfig) => observer.next(c);
config.on("updated", listener); bakeryConfig.on("updated", listener);
return () => config.off("updated", listener); return () => bakeryConfig.off("updated", listener);
}); });

View File

@@ -100,7 +100,6 @@ export default class Receiver extends EventEmitter<EventMap> {
// if the user does not have any mailboxes try to get the relays stored in the contact list // if the user does not have any mailboxes try to get the relays stored in the contact list
if (relays.length === 0) { if (relays.length === 0) {
this.log(`Failed to find mailboxes for ${person.pubkey}`);
const contacts = await this.app.contactBook.loadContactsEvent( const contacts = await this.app.contactBook.loadContactsEvent(
person.pubkey, person.pubkey,
arrayFallback(ownerMailboxes?.inboxes, BOOTSTRAP_RELAYS), arrayFallback(ownerMailboxes?.inboxes, BOOTSTRAP_RELAYS),

View File

@@ -1,10 +1,59 @@
import { animals, colors, adjectives, uniqueNamesGenerator } from "unique-names-generator";
import path from "node:path"; import path from "node:path";
import { z } from "zod";
import ConfigManager from "../modules/config/config-manager.js";
import { DATA_PATH } from "../env.js"; import { DATA_PATH } from "../env.js";
import { ReactiveJsonFile } from "../classes/json-file.js";
const config = new ConfigManager(path.join(DATA_PATH, "node.json")); export const bakeryConfigSchema = z.object({
name: z.string().default(""),
description: z.string().default(""),
config.read(); owner: z.string().optional(),
public_address: z.string().url().optional(),
export default config; // legacy config (unused)
requireReadAuth: z.boolean().default(false),
publicAddresses: z.array(z.string().url()).default([]),
autoListen: z.boolean().default(false),
logsEnabled: z.boolean().default(false),
// scrapper config
runReceiverOnBoot: z.boolean().default(true),
runScrapperOnBoot: z.boolean().default(false),
// nostr network config
bootstrap_relays: z.array(z.string().url()).optional(),
lookup_relays: z.array(z.string().url()).optional(),
// hyper config
hyperEnabled: z.boolean().default(false),
// tor config
enableTorConnections: z.boolean().default(true),
enableI2PConnections: z.boolean().default(true),
enableHyperConnections: z.boolean().default(false),
routeAllTrafficThroughTor: z.boolean().default(false),
// gossip config
gossipEnabled: z.boolean().default(false),
gossipInterval: z.number().default(10 * 60_000),
gossipBroadcastRelays: z.array(z.string().url()).default([]),
});
export type BakeryConfig = z.infer<typeof bakeryConfigSchema>;
const defaultConfig: BakeryConfig = bakeryConfigSchema.parse({});
const bakeryConfig = new ReactiveJsonFile<BakeryConfig>(path.join(DATA_PATH, "node.json"), defaultConfig);
bakeryConfig.read();
// explicitly set the default values
bakeryConfig.setDefaults(defaultConfig);
// set a new name if it is not set
if (!bakeryConfig.data.name) {
bakeryConfig.data.name = uniqueNamesGenerator({ dictionaries: [colors, adjectives, animals] });
}
export default bakeryConfig;

View File

@@ -1,7 +1,7 @@
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import server from "./server.js"; import server from "./server.js";
import config from "../config.js"; import bakeryConfig from "../config.js";
import { normalizeToHexPubkey } from "../../helpers/nip19.js"; import { normalizeToHexPubkey } from "../../helpers/nip19.js";
import { requestLoader } from "../loaders.js"; import { requestLoader } from "../loaders.js";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
@@ -10,7 +10,7 @@ server.resource("owner_pubkey", "pubkey://owner", async (uri) => ({
contents: [ contents: [
{ {
uri: uri.href, uri: uri.href,
text: config.data.owner ?? "undefined", text: bakeryConfig.data.owner ?? "undefined",
}, },
], ],
})); }));
@@ -19,7 +19,7 @@ server.resource("config", "config://app", async (uri) => ({
contents: [ contents: [
{ {
uri: uri.href, uri: uri.href,
text: JSON.stringify(config.data), text: JSON.stringify(bakeryConfig.data),
}, },
], ],
})); }));

View File

@@ -1,37 +1,20 @@
import { z } from "zod";
import server from "../server.js"; import server from "../server.js";
import { ownerAccount$ } from "../../owner.js"; import bakeryConfig, { bakeryConfigSchema } from "../../config.js";
import { NostrConnectSigner } from "applesauce-signers";
import { NostrConnectAccount } from "applesauce-accounts/accounts";
import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
import config from "../../config.js";
server.tool( server.tool("get_bakery_config", "Gets the current configuration for the bakery", {}, async () => {
"set_owner_nostr_connect_uri", return { content: [{ type: "text", text: JSON.stringify(bakeryConfig.data) }] };
"Sets the nostr connect URI that should be used to request signatures from the owners pubkey", });
{ uri: z.string().startsWith("bunker://") },
async ({ uri }) => {
try {
const signer = await NostrConnectSigner.fromBunkerURI(uri, {});
const pubkey = await signer.getPublicKey();
const account = new NostrConnectAccount(pubkey, signer);
ownerAccount$.next(account);
return { content: [{ type: "text", text: "Connected to the nostr signer" }] }; server.tool<typeof bakeryConfigSchema.shape>(
} catch (error: any) { "update_bakery_config",
return { content: [{ type: "text", text: "Error connecting to the nostr signer: " + error.message }] }; "Updates the bakery config with the provided config",
} // @ts-expect-error
}, bakeryConfigSchema.partial(),
); async (config) => {
bakeryConfig.update((data) => {
server.tool( return { ...data, ...config };
"set_owner_pubkey", });
"Sets the owner of the bakery",
{ pubkey: z.string().transform((hex) => normalizeToHexPubkey(hex, true)) }, return { content: [{ type: "text", text: "Updated config" }] };
async ({ pubkey }) => {
config.setField("owner", pubkey);
return { content: [{ type: "text", text: "Owner set" }] };
}, },
); );

View File

@@ -6,7 +6,7 @@ import z from "zod";
import server from "../server.js"; import server from "../server.js";
import { ownerFactory, ownerPublish } from "../../owner.js"; import { ownerFactory, ownerPublish } from "../../owner.js";
import { requestLoader } from "../../loaders.js"; import { requestLoader } from "../../loaders.js";
import config from "../../config.js"; import bakeryConfig from "../../config.js";
import eventCache from "../../event-cache.js"; import eventCache from "../../event-cache.js";
import { normalizeToHexPubkey } from "../../../helpers/nip19.js"; import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
@@ -52,9 +52,9 @@ server.tool(
relays: z.array(z.string().url()).optional().describe("An array of relays to publish to"), relays: z.array(z.string().url()).optional().describe("An array of relays to publish to"),
}, },
async ({ event, relays }) => { async ({ event, relays }) => {
if (!config.data.owner) throw new Error("Owner not set"); if (!bakeryConfig.data.owner) throw new Error("Owner not set");
relays = relays || (await requestLoader.mailboxes({ pubkey: config.data.owner })).outboxes; relays = relays || (await requestLoader.mailboxes({ pubkey: bakeryConfig.data.owner })).outboxes;
const results = await ownerPublish(event, relays); const results = await ownerPublish(event, relays);
if (!results) throw new Error("Failed to publish event to relays"); if (!results) throw new Error("Failed to publish event to relays");

View File

@@ -3,3 +3,4 @@ import "./config.js";
import "./connection.js"; import "./connection.js";
import "./database.js"; import "./database.js";
import "./events.js"; import "./events.js";
import "./signer.js";

View File

@@ -0,0 +1,88 @@
import { NostrConnectSigner } from "applesauce-signers/signers/nostr-connect-signer";
import { NostrConnectAccount } from "applesauce-accounts/accounts/nostr-connect-account";
import { z } from "zod";
import server from "../server.js";
import { ownerAccount$, setupSigner$, startSignerSetup, stopSignerSetup } from "../../owner.js";
import { DEFAULT_NOSTR_CONNECT_RELAYS } from "../../../const.js";
const qrcode = require("qrcode-terminal");
server.tool(
"connect_nostr_signer",
"Connects remote signer using a bunker:// URI",
{ uri: z.string().startsWith("bunker://") },
async ({ uri }) => {
if (ownerAccount$.value) return { content: [{ type: "text", text: "The owner already has a signer connected" }] };
const signer = await NostrConnectSigner.fromBunkerURI(uri, {});
const pubkey = await signer.getPublicKey();
const account = new NostrConnectAccount(pubkey, signer);
ownerAccount$.next(account);
return { content: [{ type: "text", text: "Connected to the signer" }] };
},
);
server.tool("disconnect_nostr_signer", "Disconnects and forgets the current signer", {}, async () => {
if (!ownerAccount$.value) return { content: [{ type: "text", text: "No signer connected" }] };
ownerAccount$.next(undefined);
return { content: [{ type: "text", text: "Disconnected from the signer" }] };
});
server.tool("nostr_signer_status", "Gets the status of the current signer", {}, async () => {
const account = ownerAccount$.getValue();
if (!account) return { content: [{ type: "text", text: "No signer connected" }] };
if (setupSigner$.value) {
return { content: [{ type: "text", text: "Signer setup in progress, waiting for the owner to connect" }] };
}
return {
content: [
{
type: "text",
text: [
`Pubkey: ${await account.getPublicKey()}`,
`Connected: ${account.signer.isConnected}`,
`Relays: ${account.signer.relays.join(", ")}`,
].join("\n"),
},
],
};
});
// signer setup tools
server.tool(
"create_signer_setup_link",
"Creates a nostrconnect:// URI for the owner to setup their signer",
{ relays: z.array(z.string().url()).default(DEFAULT_NOSTR_CONNECT_RELAYS) },
async ({ relays }) => {
const account = ownerAccount$.getValue();
if (account) return { content: [{ type: "text", text: "A signer is already connected" }] };
// Create a new signer if there isn't one already
if (!setupSigner$.value) startSignerSetup(relays);
const uri = setupSigner$.value!.getNostrConnectURI();
// Generate QR code
const qr = await new Promise<string>((resolve) => {
qrcode.generate(uri, { small: true }, (qr: string) => resolve(qr));
});
return {
content: [
{ type: "text", text: qr },
{ type: "text", text: `Nostr Connect URI: ${uri}` },
],
};
},
);
server.tool("abort_nostr_signer_setup", "Aborts the signer setup process", {}, async () => {
await stopSignerSetup();
return { content: [{ type: "text", text: "signer setup aborted" }] };
});

View File

@@ -10,16 +10,45 @@ import { nostrConnectPublish, nostrConnectSubscription } from "../helpers/apples
import { NostrEvent } from "nostr-tools"; import { NostrEvent } from "nostr-tools";
import eventCache from "./event-cache.js"; import eventCache from "./event-cache.js";
import { requestLoader } from "./loaders.js"; import { requestLoader } from "./loaders.js";
import config from "./config.js"; import bakeryConfig from "./config.js";
import { rxNostr } from "./rx-nostr.js"; import { rxNostr } from "./rx-nostr.js";
import { logger } from "../logger.js"; import { logger } from "../logger.js";
import { NostrConnectAccount } from "applesauce-accounts/accounts"; import { NostrConnectAccount } from "applesauce-accounts/accounts";
import { DEFAULT_NOSTR_CONNECT_RELAYS } from "../const.js";
NostrConnectSigner.subscriptionMethod = nostrConnectSubscription; NostrConnectSigner.subscriptionMethod = nostrConnectSubscription;
NostrConnectSigner.publishMethod = nostrConnectPublish; NostrConnectSigner.publishMethod = nostrConnectPublish;
const log = logger.extend("Owner"); const log = logger.extend("Owner");
/** A temp signer while the owner is setting up their signer */
export const setupSigner$ = new BehaviorSubject<NostrConnectSigner | undefined>(undefined);
export function startSignerSetup(relays = DEFAULT_NOSTR_CONNECT_RELAYS) {
if (setupSigner$.value) return setupSigner$.value;
const signer = new NostrConnectSigner({ relays });
setupSigner$.next(signer);
// async setup process
const p = signer.waitForSigner().then(async () => {
const pubkey = await signer.getPublicKey();
ownerAccount$.next(new NostrConnectAccount(pubkey, signer));
setupSigner$.next(undefined);
});
return p;
}
export async function stopSignerSetup() {
const signer = setupSigner$.getValue();
if (signer) {
signer.close();
setupSigner$.next(undefined);
}
}
/** The owner's account */
export const ownerAccount$ = new BehaviorSubject<NostrConnectAccount<any> | undefined>(undefined); export const ownerAccount$ = new BehaviorSubject<NostrConnectAccount<any> | undefined>(undefined);
// Update account when secrets change // Update account when secrets change
@@ -71,9 +100,9 @@ export async function ownerPublish(event: NostrEvent, relays?: string[]) {
eventCache.addEvent(event); eventCache.addEvent(event);
// publish event to owners outboxes // publish event to owners outboxes
if (config.data.owner) { if (bakeryConfig.data.owner) {
try { try {
relays = relays || (await requestLoader.mailboxes({ pubkey: config.data.owner })).outboxes; relays = relays || (await requestLoader.mailboxes({ pubkey: bakeryConfig.data.owner })).outboxes;
return await lastValueFrom(rxNostr.send(event, { on: { relays } }).pipe(toArray())); return await lastValueFrom(rxNostr.send(event, { on: { relays } }).pipe(toArray()));
} catch (error) { } catch (error) {
// Failed to publish to outboxes, ignore error for now // Failed to publish to outboxes, ignore error for now