mirror of
https://github.com/aljazceru/bakery.git
synced 2025-12-17 20:55:02 +01:00
add nostr signer setup tools
cleanup bakery config
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
DATA_PATH=./data
|
||||
|
||||
# the port to use
|
||||
PORT=3000
|
||||
PORT=9272
|
||||
|
||||
# the address to the tor SOCKS5 proxy to enable connections to .onion addresses
|
||||
# TOR_PROXY="127.0.0.1:9050"
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"scripts": {
|
||||
"prepack": "tsc",
|
||||
"start": "node .",
|
||||
"dev": "nodemon --loader @swc-node/register/esm src/index.ts",
|
||||
"mcp": "mcp-inspector node . --mcp --port 8080",
|
||||
"mcp-debug": "mcp-inspector node --inspect-brk . --mcp --port 8080",
|
||||
"dev": "DATA_PATH=./data nodemon --loader @swc-node/register/esm src/index.ts",
|
||||
"mcp": "mcp-inspector node . --mcp",
|
||||
"mcp-debug": "mcp-inspector node --inspect-brk . --mcp",
|
||||
"build": "tsc",
|
||||
"test": "vitest run",
|
||||
"format": "prettier -w .",
|
||||
@@ -60,6 +60,7 @@
|
||||
"nostr-tools": "^2.11.0",
|
||||
"pac-proxy-agent": "^7.2.0",
|
||||
"process-streams": "^1.0.3",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"rx-nostr": "^3.5.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"streamx": "^2.22.0",
|
||||
@@ -81,6 +82,7 @@
|
||||
"@types/hash-sum": "^1.0.2",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@types/ws": "^8.18.0",
|
||||
"nodemon": "^3.1.9",
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -101,6 +101,9 @@ importers:
|
||||
process-streams:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
qrcode-terminal:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0
|
||||
rx-nostr:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
@@ -159,6 +162,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.13.14
|
||||
version: 22.13.14
|
||||
'@types/qrcode-terminal':
|
||||
specifier: ^0.12.2
|
||||
version: 0.12.2
|
||||
'@types/web-push':
|
||||
specifier: ^3.6.4
|
||||
version: 3.6.4
|
||||
@@ -1204,6 +1210,9 @@ packages:
|
||||
'@types/prismjs@1.26.5':
|
||||
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':
|
||||
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
|
||||
|
||||
@@ -2663,6 +2672,10 @@ packages:
|
||||
pump@3.0.2:
|
||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||
|
||||
qrcode-terminal@0.12.0:
|
||||
resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==}
|
||||
hasBin: true
|
||||
|
||||
qs@6.13.0:
|
||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -4406,6 +4419,8 @@ snapshots:
|
||||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/qrcode-terminal@0.12.2': {}
|
||||
|
||||
'@types/qs@6.9.18': {}
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
@@ -6176,6 +6191,8 @@ snapshots:
|
||||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
|
||||
qrcode-terminal@0.12.0: {}
|
||||
|
||||
qs@6.13.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
@@ -14,7 +14,6 @@ import Database from "./database.js";
|
||||
import { NIP_11_SOFTWARE_URL, SENSITIVE_KINDS } from "../const.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 ConfigActions from "../modules/control/config-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 database from "../services/database.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 stateManager from "../services/state.js";
|
||||
import eventCache from "../services/event-cache.js";
|
||||
@@ -63,7 +62,7 @@ type EventMap = {
|
||||
|
||||
export default class App extends EventEmitter<EventMap> {
|
||||
running = false;
|
||||
config: ConfigManager;
|
||||
config: typeof bakeryConfig;
|
||||
secrets: SecretsManager;
|
||||
state: ApplicationStateManager;
|
||||
signer: SimpleSigner;
|
||||
@@ -95,18 +94,12 @@ export default class App extends EventEmitter<EventMap> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.config = config;
|
||||
this.config = bakeryConfig;
|
||||
|
||||
this.secrets = secrets;
|
||||
|
||||
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
|
||||
if (!this.config.data.owner && OWNER_PUBKEY) {
|
||||
this.config.setField("owner", OWNER_PUBKEY);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { Adapter, Low, LowSync, SyncAdapter } from "lowdb";
|
||||
import { LowSync, SyncAdapter } from "lowdb";
|
||||
import { JSONFileSync } from "lowdb/node";
|
||||
|
||||
type EventMap<T> = {
|
||||
/** fires when file is loaded */
|
||||
@@ -11,61 +12,17 @@ type EventMap<T> = {
|
||||
saved: [T];
|
||||
};
|
||||
|
||||
export class ReactiveJsonFile<T extends object> extends EventEmitter<EventMap<T>> implements Low<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> {
|
||||
export class ReactiveJsonFile<T extends object> extends EventEmitter<EventMap<T>> implements LowSync<T> {
|
||||
protected db: LowSync<T>;
|
||||
adapter: SyncAdapter<T>;
|
||||
|
||||
data: T;
|
||||
|
||||
constructor(adapter: SyncAdapter<T>, defaultData: T) {
|
||||
constructor(path: string, defaultData: T) {
|
||||
super();
|
||||
|
||||
this.adapter = adapter;
|
||||
this.db = new LowSync<T>(adapter, defaultData);
|
||||
this.adapter = new JSONFileSync<T>(path);
|
||||
this.db = new LowSync<T>(this.adapter, defaultData);
|
||||
|
||||
this.data = this.createProxy();
|
||||
}
|
||||
@@ -97,4 +54,18 @@ export class ReactiveJsonFileSync<T extends object> extends EventEmitter<EventMa
|
||||
update(fn: (data: T) => unknown) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { EncryptedDirectMessage } from "nostr-tools/kinds";
|
||||
|
||||
export const DEFAULT_PORT = 9272;
|
||||
export const SENSITIVE_KINDS = [EncryptedDirectMessage];
|
||||
|
||||
export const NIP_11_SOFTWARE_URL = "git+https://github.com/hzrd149/bakery.git";
|
||||
|
||||
export const OUTBOUND_PROXY_TYPES = ["SOCKS5", "HTTP"];
|
||||
|
||||
export const DEFAULT_NOSTR_CONNECT_RELAYS = ["wss://relay.nsec.app"];
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import "dotenv/config";
|
||||
import { mkdirp } from "mkdirp";
|
||||
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 args from "./args.js";
|
||||
|
||||
export const OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined;
|
||||
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);
|
||||
|
||||
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
|
||||
export const I2P_PROXY = process.env.I2P_PROXY;
|
||||
|
||||
22
src/index.ts
22
src/index.ts
@@ -57,12 +57,14 @@ app.express.get("*", (req, res) => {
|
||||
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) => {
|
||||
if (reason instanceof Error) {
|
||||
console.log("Unhandled Rejection");
|
||||
console.log(reason);
|
||||
} else console.log("Unhandled Rejection at:", promise, "reason:", reason);
|
||||
if (!IS_MCP) console.error("Unhandled Promise Rejection:", reason);
|
||||
});
|
||||
|
||||
// start the app
|
||||
@@ -87,13 +89,3 @@ async function shutdown() {
|
||||
}
|
||||
process.on("SIGINT", 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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export default class ConfigActions implements ControlMessageHandler {
|
||||
const field = message[3];
|
||||
const value = message[4];
|
||||
|
||||
// @ts-expect-error
|
||||
this.app.config.setField(field, value);
|
||||
return true;
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Server } from "http";
|
||||
import { logger } from "../../../logger.js";
|
||||
import { getIPAddresses } from "../../../helpers/ip.js";
|
||||
import ConfigManager from "../../config/config-manager.js";
|
||||
import config from "../../../services/config.js";
|
||||
import bakeryConfig from "../../../services/config.js";
|
||||
|
||||
import TorInbound from "./tor.js";
|
||||
import I2PInbound from "./i2p.js";
|
||||
@@ -29,7 +28,7 @@ export default class InboundNetworkManager {
|
||||
this.tor = new TorInbound();
|
||||
this.i2p = new I2PInbound();
|
||||
|
||||
this.listenToAppConfig(config);
|
||||
this.listenToAppConfig(bakeryConfig);
|
||||
}
|
||||
|
||||
private getAddress() {
|
||||
@@ -41,7 +40,7 @@ export default class InboundNetworkManager {
|
||||
return address;
|
||||
}
|
||||
|
||||
private update(cfg = config.data) {
|
||||
private update(cfg = bakeryConfig.data) {
|
||||
if (!this.running) return;
|
||||
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 */
|
||||
listenToAppConfig(config: ConfigManager) {
|
||||
listenToAppConfig(config: typeof bakeryConfig) {
|
||||
config.on("updated", this.update.bind(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ import { PacProxyAgent } from "pac-proxy-agent";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import { logger } from "../../../logger.js";
|
||||
import ConfigManager from "../../config/config-manager.js";
|
||||
import HyperOutbound from "./hyper.js";
|
||||
import TorOutbound from "./tor.js";
|
||||
import I2POutbound from "./i2p.js";
|
||||
import bakeryConfig from "../../../services/config.js";
|
||||
|
||||
export default class OutboundNetworkManager {
|
||||
log = logger.extend("Network:Outbound");
|
||||
@@ -95,7 +95,7 @@ function FindProxyForURL(url, host)
|
||||
updateAgentThrottle: () => void = _throttle(this.updateAgent.bind(this), 100);
|
||||
|
||||
/** A helper method to make the manager run off of the app config */
|
||||
listenToAppConfig(config: ConfigManager) {
|
||||
listenToAppConfig(config: typeof bakeryConfig) {
|
||||
config.on("updated", (c) => {
|
||||
this.enableHyperConnections = c.hyperEnabled && c.enableHyperConnections;
|
||||
this.enableTorConnections = c.enableTorConnections;
|
||||
|
||||
@@ -8,7 +8,7 @@ import webPush from "web-push";
|
||||
import { logger } from "../../logger.js";
|
||||
import App from "../../app/index.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";
|
||||
|
||||
export type NotificationsManagerState = {
|
||||
@@ -73,7 +73,7 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
|
||||
/** Whether a notification should be sent */
|
||||
shouldNotify(event: NostrEvent) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { PrivateNodeConfig } from "@satellite-earth/core/types";
|
||||
|
||||
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) => {
|
||||
observer.next(config.data);
|
||||
const listener = (c: PrivateNodeConfig) => observer.next(c);
|
||||
config.on("updated", listener);
|
||||
return () => config.off("updated", listener);
|
||||
observer.next(bakeryConfig.data);
|
||||
const listener = (c: BakeryConfig) => observer.next(c);
|
||||
bakeryConfig.on("updated", listener);
|
||||
return () => bakeryConfig.off("updated", listener);
|
||||
});
|
||||
|
||||
@@ -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 (relays.length === 0) {
|
||||
this.log(`Failed to find mailboxes for ${person.pubkey}`);
|
||||
const contacts = await this.app.contactBook.loadContactsEvent(
|
||||
person.pubkey,
|
||||
arrayFallback(ownerMailboxes?.inboxes, BOOTSTRAP_RELAYS),
|
||||
|
||||
@@ -1,10 +1,59 @@
|
||||
import { animals, colors, adjectives, uniqueNamesGenerator } from "unique-names-generator";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
import ConfigManager from "../modules/config/config-manager.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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
import server from "./server.js";
|
||||
import config from "../config.js";
|
||||
import bakeryConfig from "../config.js";
|
||||
import { normalizeToHexPubkey } from "../../helpers/nip19.js";
|
||||
import { requestLoader } from "../loaders.js";
|
||||
import { kinds } from "nostr-tools";
|
||||
@@ -10,7 +10,7 @@ server.resource("owner_pubkey", "pubkey://owner", async (uri) => ({
|
||||
contents: [
|
||||
{
|
||||
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: [
|
||||
{
|
||||
uri: uri.href,
|
||||
text: JSON.stringify(config.data),
|
||||
text: JSON.stringify(bakeryConfig.data),
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -1,37 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import server from "../server.js";
|
||||
import { ownerAccount$ } from "../../owner.js";
|
||||
import { NostrConnectSigner } from "applesauce-signers";
|
||||
import { NostrConnectAccount } from "applesauce-accounts/accounts";
|
||||
import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
|
||||
import config from "../../config.js";
|
||||
import bakeryConfig, { bakeryConfigSchema } from "../../config.js";
|
||||
|
||||
server.tool(
|
||||
"set_owner_nostr_connect_uri",
|
||||
"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);
|
||||
server.tool("get_bakery_config", "Gets the current configuration for the bakery", {}, async () => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(bakeryConfig.data) }] };
|
||||
});
|
||||
|
||||
return { content: [{ type: "text", text: "Connected to the nostr signer" }] };
|
||||
} catch (error: any) {
|
||||
return { content: [{ type: "text", text: "Error connecting to the nostr signer: " + error.message }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"set_owner_pubkey",
|
||||
"Sets the owner of the bakery",
|
||||
{ pubkey: z.string().transform((hex) => normalizeToHexPubkey(hex, true)) },
|
||||
async ({ pubkey }) => {
|
||||
config.setField("owner", pubkey);
|
||||
|
||||
return { content: [{ type: "text", text: "Owner set" }] };
|
||||
server.tool<typeof bakeryConfigSchema.shape>(
|
||||
"update_bakery_config",
|
||||
"Updates the bakery config with the provided config",
|
||||
// @ts-expect-error
|
||||
bakeryConfigSchema.partial(),
|
||||
async (config) => {
|
||||
bakeryConfig.update((data) => {
|
||||
return { ...data, ...config };
|
||||
});
|
||||
|
||||
return { content: [{ type: "text", text: "Updated config" }] };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import z from "zod";
|
||||
import server from "../server.js";
|
||||
import { ownerFactory, ownerPublish } from "../../owner.js";
|
||||
import { requestLoader } from "../../loaders.js";
|
||||
import config from "../../config.js";
|
||||
import bakeryConfig from "../../config.js";
|
||||
import eventCache from "../../event-cache.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"),
|
||||
},
|
||||
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);
|
||||
if (!results) throw new Error("Failed to publish event to relays");
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ import "./config.js";
|
||||
import "./connection.js";
|
||||
import "./database.js";
|
||||
import "./events.js";
|
||||
import "./signer.js";
|
||||
|
||||
88
src/services/mcp/tools/signer.ts
Normal file
88
src/services/mcp/tools/signer.ts
Normal 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" }] };
|
||||
});
|
||||
@@ -10,16 +10,45 @@ import { nostrConnectPublish, nostrConnectSubscription } from "../helpers/apples
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import eventCache from "./event-cache.js";
|
||||
import { requestLoader } from "./loaders.js";
|
||||
import config from "./config.js";
|
||||
import bakeryConfig from "./config.js";
|
||||
import { rxNostr } from "./rx-nostr.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { NostrConnectAccount } from "applesauce-accounts/accounts";
|
||||
import { DEFAULT_NOSTR_CONNECT_RELAYS } from "../const.js";
|
||||
|
||||
NostrConnectSigner.subscriptionMethod = nostrConnectSubscription;
|
||||
NostrConnectSigner.publishMethod = nostrConnectPublish;
|
||||
|
||||
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);
|
||||
|
||||
// Update account when secrets change
|
||||
@@ -71,9 +100,9 @@ export async function ownerPublish(event: NostrEvent, relays?: string[]) {
|
||||
eventCache.addEvent(event);
|
||||
|
||||
// publish event to owners outboxes
|
||||
if (config.data.owner) {
|
||||
if (bakeryConfig.data.owner) {
|
||||
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()));
|
||||
} catch (error) {
|
||||
// Failed to publish to outboxes, ignore error for now
|
||||
|
||||
Reference in New Issue
Block a user