add mcp actions

This commit is contained in:
hzrd149
2025-03-27 18:14:22 +00:00
parent 3448db9025
commit b3a43f8e3a
23 changed files with 442 additions and 68 deletions

12
.cursor/mcp.json Normal file
View File

@@ -0,0 +1,12 @@
{
"mcpServers": {
"bakery-dev": {
"command": "npx",
"args": ["nostrudel-bakery", "--mcp"],
"env": {
"DATA_DIR": "/home/robert/Projects/bakery/data",
"OWNER_PUBKEY": "npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr"
}
}
}
}

7
.vscode/launch.json vendored
View File

@@ -4,6 +4,13 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach",
"port": 9229,
"request": "attach",
"skipFiles": ["<node_internals>/**"],
"type": "node"
},
{
"type": "node",
"request": "launch",

View File

@@ -11,6 +11,7 @@
"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",
"build": "tsc",
"test": "vitest run",
"format": "prettier -w .",
@@ -32,10 +33,12 @@
"@modelcontextprotocol/sdk": "^1.8.0",
"@noble/hashes": "^1.7.1",
"@satellite-earth/core": "^0.5.0",
"applesauce-core": "^0.12.0",
"applesauce-factory": "^0.12.0",
"applesauce-loaders": "^0.12.0",
"applesauce-signers": "^0.12.0",
"applesauce-accounts": "next",
"applesauce-actions": "next",
"applesauce-core": "next",
"applesauce-factory": "next",
"applesauce-loaders": "next",
"applesauce-signers": "next",
"better-sqlite3": "^11.9.1",
"blossom-client-sdk": "^2.1.1",
"cors": "^2.8.5",

89
pnpm-lock.yaml generated
View File

@@ -20,18 +20,24 @@ importers:
'@satellite-earth/core':
specifier: ^0.5.0
version: 0.5.0(typescript@5.8.2)
applesauce-accounts:
specifier: next
version: 0.0.0-next-20250327153627(typescript@5.8.2)
applesauce-actions:
specifier: next
version: 0.0.0-next-20250327153627(typescript@5.8.2)
applesauce-core:
specifier: ^0.12.0
version: 0.12.1(typescript@5.8.2)
specifier: next
version: 0.0.0-next-20250327153627(typescript@5.8.2)
applesauce-factory:
specifier: ^0.12.0
version: 0.12.2(typescript@5.8.2)
specifier: next
version: 0.0.0-next-20250327153627(typescript@5.8.2)
applesauce-loaders:
specifier: ^0.12.0
version: 0.12.0(typescript@5.8.2)
specifier: next
version: 0.0.0-next-20250327153627(typescript@5.8.2)
applesauce-signers:
specifier: ^0.12.0
version: 0.12.0(typescript@5.8.2)
specifier: next
version: 0.0.0-next-20250327153627(typescript@5.8.2)
better-sqlite3:
specifier: ^11.9.1
version: 11.9.1
@@ -1288,20 +1294,26 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
applesauce-content@0.12.0:
resolution: {integrity: sha512-Utgowd2cY8ZSwp/F3J1p28KTX51UHZlb3t4n7yAnWxapzCoY2EZ1AJwfIMA5AQ8ohvp6M3KEWUYdpXdc8tW/Vg==}
applesauce-accounts@0.0.0-next-20250327153627:
resolution: {integrity: sha512-VH76fI5HNQGMz2weCJN1qXlvllBtHaTwvYJ3hhx1wOm9do05/BTncH0amh2nu9pcQ+kmCdq91K40MWmgFcElaw==}
applesauce-core@0.12.1:
resolution: {integrity: sha512-H9ahp0L4iriIrompC8mZTq4jmOZ1rBxBvQxy7GYIVYiYoRTUv0vOvjAnaOATpSWjLpqDYZbU3UZvYkPoQD3eKg==}
applesauce-actions@0.0.0-next-20250327153627:
resolution: {integrity: sha512-JYoIvFnXZgy6lB7rCNs7ExB+/Iw6ibbUdA4KaLzItELg0JeqKMhCk5NOWrp8uZF/odN5g6OqwdQ845NLc94+0g==}
applesauce-factory@0.12.2:
resolution: {integrity: sha512-tlFsw+5bvRmCT8XFCWZK5Aj8Aa/KiVzn162FWCvMWBbxQJ85tt43HBcQRBXB3RmF3AosTwiVtGloqxX1OoQTWA==}
applesauce-content@0.0.0-next-20250327153627:
resolution: {integrity: sha512-hMnZH4fTI9w74eIljC/+F7L9VS8QTBMr04AtzT5la34hXMlL2qm32MN9qm6myAW0CKg3N9ZfspHkRk7yDb8ivA==}
applesauce-loaders@0.12.0:
resolution: {integrity: sha512-GXX1kZMcK7Bw4y8yeZEL0ylROc4hodEWMNNwIQ2Y2wWBWfnwAiHoTq/yusp++DgpsbYDjPnPJGh8lgTvhHul7w==}
applesauce-core@0.0.0-next-20250327153627:
resolution: {integrity: sha512-58oyp8gTjYJcE+ddQ6UVdwmef92GjT/rUdq5OfOZWCLAZh3HZoAsw2xsAcXdzl0Did7zB9CUJfC5Mr7Ajc6jhA==}
applesauce-signers@0.12.0:
resolution: {integrity: sha512-fMUfGuOkazKcBpyVgVo3gSriAKEZ1xxZU8hiDi61L1FLh/etQEngqBoTHlHgXXSc7cNMMCxDvXGHpB6DeDg7ow==}
applesauce-factory@0.0.0-next-20250327153627:
resolution: {integrity: sha512-snQ4s/JLdwK84c6dq2zG5BFd1kds4WHFKmxHuNjslenTipexxwosid9OjpBiC6sHAhr5N6PsaY/3dqIxIkeRNA==}
applesauce-loaders@0.0.0-next-20250327153627:
resolution: {integrity: sha512-bSDMBqTYUPwLTbCnEXm4vMzqoOavK+9SNxjDcczMSTaYPISkDRQ5iBpttLSoMdMDpeeZqzU7oGFyeQw+1BPD6Q==}
applesauce-signers@0.0.0-next-20250327153627:
resolution: {integrity: sha512-yBfsw+xn2n/tBJWJt99wAwXEdHOVvC8PY2Rxs538kPi/uD3yIpQLKeQ5wrACt8cOSm0C8rnEQN3Rw+8dqUOcZg==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@@ -4493,13 +4505,34 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
applesauce-content@0.12.0(typescript@5.8.2):
applesauce-accounts@0.0.0-next-20250327153627(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
applesauce-signers: 0.0.0-next-20250327153627(typescript@5.8.2)
nanoid: 5.1.5
nostr-tools: 2.11.0(typescript@5.8.2)
rxjs: 7.8.2
transitivePeerDependencies:
- supports-color
- typescript
applesauce-actions@0.0.0-next-20250327153627(typescript@5.8.2):
dependencies:
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250327153627(typescript@5.8.2)
nostr-tools: 2.11.0(typescript@5.8.2)
rxjs: 7.8.2
transitivePeerDependencies:
- supports-color
- typescript
applesauce-content@0.0.0-next-20250327153627(typescript@5.8.2):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
applesauce-core: 0.12.1(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
mdast-util-find-and-replace: 3.0.2
nostr-tools: 2.11.0(typescript@5.8.2)
remark: 15.0.1
@@ -4510,7 +4543,7 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.12.1(typescript@5.8.2):
applesauce-core@0.0.0-next-20250327153627(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
'@scure/base': 1.2.4
@@ -4525,19 +4558,19 @@ snapshots:
- supports-color
- typescript
applesauce-factory@0.12.2(typescript@5.8.2):
applesauce-factory@0.0.0-next-20250327153627(typescript@5.8.2):
dependencies:
applesauce-content: 0.12.0(typescript@5.8.2)
applesauce-core: 0.12.1(typescript@5.8.2)
applesauce-content: 0.0.0-next-20250327153627(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
nanoid: 5.1.5
nostr-tools: 2.11.0(typescript@5.8.2)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-loaders@0.12.0(typescript@5.8.2):
applesauce-loaders@0.0.0-next-20250327153627(typescript@5.8.2):
dependencies:
applesauce-core: 0.12.1(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
nanoid: 5.1.5
nostr-tools: 2.11.0(typescript@5.8.2)
rx-nostr: 3.5.0
@@ -4546,12 +4579,12 @@ snapshots:
- supports-color
- typescript
applesauce-signers@0.12.0(typescript@5.8.2):
applesauce-signers@0.0.0-next-20250327153627(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
'@noble/secp256k1': 1.7.1
'@scure/base': 1.2.4
applesauce-core: 0.12.1(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
debug: 4.4.0(supports-color@5.5.0)
nanoid: 5.1.5
nostr-tools: 2.11.0(typescript@5.8.2)

View File

@@ -55,6 +55,7 @@ import { getDMRecipient } from "../helpers/direct-messages.js";
import { onConnection, onJSONMessage } from "../helpers/ws.js";
import QueryManager from "../modules/queries/manager.js";
import "../modules/queries/queries/index.js";
import bakerySigner from "../services/bakery.js";
type EventMap = {
listening: [];
@@ -98,16 +99,12 @@ export default class App extends EventEmitter<EventMap> {
this.secrets = secrets;
this.signer = new SimpleSigner(this.secrets.get("nostrKey"));
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");
if (this.signer.key !== this.secrets.get("nostrKey")) {
this.signer = new SimpleSigner(this.secrets.get("nostrKey"));
}
});
// set owner pubkey from env variable

View File

@@ -0,0 +1,35 @@
import { Nip07Interface } from "applesauce-signers";
import { EventTemplate, NostrEvent } from "nostr-tools";
import { Observable } from "rxjs";
export class ProxySigner<T extends Nip07Interface> implements Nip07Interface {
private _signer: T | undefined;
protected get signer(): T {
if (!this._signer) throw new Error(this.error || "Missing signer");
return this._signer;
}
get nip04() {
if (!this.signer.nip04) throw new Error("Signer does not support nip04");
return this.signer.nip04;
}
get nip44() {
if (!this.signer.nip44) throw new Error("Signer does not support nip44");
return this.signer.nip44;
}
constructor(
protected upstream: Observable<T | undefined>,
protected error?: string,
) {
this.upstream.subscribe((signer) => (this._signer = signer));
}
signEvent(template: EventTemplate): Promise<NostrEvent> | NostrEvent {
return this.signer.signEvent(template);
}
getPublicKey(): Promise<string> | string {
return this.signer.getPublicKey();
}
}

35
src/helpers/applesauce.ts Normal file
View File

@@ -0,0 +1,35 @@
import { NostrPublishMethod, NostrSubscriptionMethod } from "applesauce-signers";
import { lastValueFrom, Observable } from "rxjs";
import { rxNostr } from "../services/rx-nostr.js";
import { SimplePool } from "nostr-tools";
const pool = new SimplePool();
export const nostrConnectSubscription: NostrSubscriptionMethod = (filters, relays) => {
return new Observable((observer) => {
const sub = pool.subscribeMany(relays, filters, {
onevent: (event) => {
observer.next(event);
},
});
return () => sub.close();
});
// return new Observable((observer) => {
// const req = createRxForwardReq("nostr-connect");
// const observable = rxNostr.use(req, { on: { relays } });
// // hack to ensure subscription is active before sending filters
// const sub = observable.subscribe((p) => {
// observer.next(p.event);
// });
// req.emit(filters);
// return sub;
// });
};
export const nostrConnectPublish: NostrPublishMethod = async (event, relays) => {
await lastValueFrom(rxNostr.send(event, { on: { relays } }));
};

View File

@@ -12,6 +12,7 @@ import App from "./app/index.js";
import { PUBLIC_ADDRESS, IS_MCP } from "./env.js";
import { addListener, logger } from "./logger.js";
import { pathExists } from "./helpers/fs.js";
import "./services/owner.js";
// add durations plugin
dayjs.extend(duration);

View File

@@ -1,5 +1,5 @@
import WebSocket from "ws";
import { Observable, Subject, Subscription } from "rxjs";
import { Subject, Subscription } from "rxjs";
import { logger } from "../../logger.js";
import { Query, QueryClose, QueryData, QueryError } from "./types.js";

View File

@@ -0,0 +1,8 @@
import { ConnectionState } from "rx-nostr";
import { Query } from "../types.js";
import { connections$ } from "../../../services/rx-nostr.js";
export const ConnectionsQuery: Query<Record<string, ConnectionState>> = () => {
return connections$;
};

View File

@@ -1,5 +1,6 @@
import QueryManager from "../manager.js";
import { ConfigQuery } from "./config.js";
import { ConnectionsQuery } from "./connections.js";
import { LogsQuery } from "./logs.js";
import NetworkStateQuery from "./network-status.js";
import { ServicesQuery } from "./services.js";
@@ -8,3 +9,4 @@ QueryManager.types.set("network-status", NetworkStateQuery);
QueryManager.types.set("logs", LogsQuery);
QueryManager.types.set("services", ServicesQuery);
QueryManager.types.set("config", ConfigQuery);
QueryManager.types.set("connections", ConnectionsQuery);

View File

@@ -1,7 +1,10 @@
import { Observable } from "rxjs";
import _throttle from "lodash.throttle";
import { generateSecretKey } from "nostr-tools";
import EventEmitter from "events";
import { SerializedAccount } from "applesauce-accounts";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { NostrConnectAccountSignerData } from "applesauce-accounts/accounts";
import EventEmitter from "events";
import webPush from "web-push";
import crypto from "crypto";
import fs from "fs";
@@ -10,6 +13,7 @@ import { logger } from "../logger.js";
type Secrets = {
nostrKey: Uint8Array;
ownerAccount?: SerializedAccount<NostrConnectAccountSignerData, any>;
vapidPrivateKey: string;
vapidPublicKey: string;
hyperKey: Buffer;
@@ -18,6 +22,7 @@ type Secrets = {
};
type RawJson = Partial<{
nostrKey: string;
ownerAccount?: SerializedAccount<NostrConnectAccountSignerData, any>;
vapidPrivateKey: string;
vapidPublicKey: string;
hyperKey: string;
@@ -58,6 +63,17 @@ export default class SecretsManager extends EventEmitter<EventMap> {
this.write();
}
/** Subscribe to the value of a secret */
watch<T extends keyof Secrets>(key: T): Observable<Secrets[T]> {
return new Observable((observer) => {
observer.next(this.get(key));
this.on("changed", (k, value) => {
if (k === key) observer.next(value);
});
});
}
read() {
this.log("Loading secrets");
@@ -97,6 +113,8 @@ export default class SecretsManager extends EventEmitter<EventMap> {
secrets.i2pPrivateKey = json.i2pPrivateKey;
secrets.i2pPublicKey = json.i2pPublicKey;
secrets.ownerAccount = json.ownerAccount;
this.secrets = secrets;
this.emit("loaded");
@@ -116,6 +134,7 @@ export default class SecretsManager extends EventEmitter<EventMap> {
hyperKey: this.secrets.hyperKey?.toString("hex"),
i2pPrivateKey: this.secrets.i2pPrivateKey,
i2pPublicKey: this.secrets.i2pPublicKey,
ownerAccount: this.secrets.ownerAccount,
};
fs.writeFileSync(this.path, JSON.stringify(json, null, 2), { encoding: "utf-8" });

6
src/services/bakery.ts Normal file
View File

@@ -0,0 +1,6 @@
import { SimpleSigner } from "applesauce-signers";
import secrets from "./secrets.js";
const bakerySigner = new SimpleSigner(secrets.get("nostrKey"));
export default bakerySigner;

View File

@@ -1,5 +1,5 @@
import "./resources.js";
import "./tools.js";
import "./tools/index.js";
import server from "./server.js";
export default server;

View File

@@ -14,6 +14,15 @@ server.resource("owner pubkey", "pubkey://owner", async (uri) => ({
],
}));
server.resource("config", "config://app", async (uri) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify(config.data, null, 2),
},
],
}));
server.resource(
"user profile",
new ResourceTemplate("users://{pubkey}/profile", { list: undefined }),

View File

@@ -1,27 +0,0 @@
import z from "zod";
import { kinds } from "nostr-tools";
import server from "./server.js";
import eventCache from "../event-cache.js";
server.tool(
"Search events",
"Search events by kind and query",
{
query: z.string(),
kind: z.number().optional().default(kinds.ShortTextNote),
limit: z.number().optional().default(50),
},
async ({ query, kind, limit }) => {
const events = eventCache.getEventsForFilters([{ kinds: [kind], search: query, limit }]);
return {
content: events.map((event) => {
return {
type: "text",
text: JSON.stringify(event),
};
}),
};
},
);

View File

@@ -0,0 +1,32 @@
import z from "zod";
import server from "../server.js";
import { ownerActions, ownerPublish } from "../../owner.js";
import { FollowUser, UnfollowUser } from "applesauce-actions/actions";
server.tool(
"Follow user",
"Adds another users pubkey to the owners following list",
{ pubkey: z.string() },
async ({ pubkey }) => {
try {
await ownerActions.exec(FollowUser, pubkey).forEach(ownerPublish);
return { content: [{ type: "text", text: "Added user to following list" }] };
} catch (error) {
return { content: [{ type: "text", text: "Error following user" }] };
}
},
);
server.tool(
"Unfollow user",
"Removes another users pubkey from the owners following list",
{ pubkey: z.string() },
async ({ pubkey }) => {
try {
await ownerActions.exec(UnfollowUser, pubkey).forEach(ownerPublish);
return { content: [{ type: "text", text: "Removed user from following list" }] };
} catch (error) {
return { content: [{ type: "text", text: "Error unfollowing user" }] };
}
},
);

View File

@@ -0,0 +1,24 @@
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";
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);
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 }] };
}
},
);

View File

@@ -0,0 +1,14 @@
import database from "../../database.js";
import server from "../server.js";
server.tool("Total events", "Get the total number of events in the database", {}, async () => {
const result = database.db.prepare<[], { events: number }>(`SELECT COUNT(*) AS events FROM events`).get();
return { content: [{ type: "text", text: `Total events: ${result?.events ?? 0}` }] };
});
server.tool("Total users", "Get the total number of users in the database", {}, async () => {
const result = database.db
.prepare<[], { users: number }>(`SELECT COUNT(*) AS users FROM events GROUP BY pubkey`)
.get();
return { content: [{ type: "text", text: `Total users: ${result?.users ?? 0}` }] };
});

View File

@@ -0,0 +1,77 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { NoteBlueprint } from "applesauce-factory/blueprints";
import { EventTemplate } from "nostr-tools";
import z from "zod";
import server from "../server.js";
import { ownerFactory } from "../../owner.js";
import { rxNostr } from "../../rx-nostr.js";
import { requestLoader } from "../../loaders.js";
import config from "../../config.js";
import { lastValueFrom, toArray } from "rxjs";
server.tool(
"Sign draft event",
"Signs a draft note event with the owners pubkey",
{
draft: z.object({
content: z.string(),
created_at: z.number(),
tags: z.array(z.array(z.string())),
kind: z.number(),
}),
},
async ({ draft }) => {
const unsigned = await ownerFactory.stamp(draft);
return {
content: [
{
type: "text",
text: JSON.stringify(await ownerFactory.sign(unsigned)),
},
],
};
},
);
server.tool(
"Publish event",
"Publishes an event to the owners outbox relays",
{
event: z.object({
created_at: z.number(),
content: z.string(),
tags: z.array(z.array(z.string())),
kind: z.number(),
sig: z.string(),
pubkey: z.string().length(64),
}),
},
async ({ event }) => {
if (!config.data.owner) throw new Error("Owner not set");
const mailboxes = await requestLoader.mailboxes({ pubkey: config.data.owner });
const results = await lastValueFrom(rxNostr.send(event, { on: { relays: mailboxes.outboxes } }).pipe(toArray()));
return {
content: results.map((result) => ({
type: "text",
text: `${result.from} ${result.ok ? "Success" : "Failed"}: ${result.message}`,
})),
};
},
);
async function returnUnsigned(draft: EventTemplate | Promise<EventTemplate>): Promise<CallToolResult> {
return {
content: [{ type: "text", text: JSON.stringify(await ownerFactory.stamp(await draft)) }],
};
}
server.tool(
"Short text note draft",
"Create a short text note draft event",
{
content: z.string(),
},
async ({ content }) => returnUnsigned(ownerFactory.create(NoteBlueprint, content)),
);

View File

@@ -0,0 +1,4 @@
import "./actions.js";
import "./events.js";
import "./database.js";
import "./config.js";

83
src/services/owner.ts Normal file
View File

@@ -0,0 +1,83 @@
import { NostrConnectSigner } from "applesauce-signers/signers/nostr-connect-signer";
import { BehaviorSubject, filter, lastValueFrom, pairwise } from "rxjs";
import { ActionHub } from "applesauce-actions";
import { EventFactory } from "applesauce-factory";
import secrets from "./secrets.js";
import { ProxySigner } from "../classes/proxy-signer.js";
import { eventStore } from "./stores.js";
import { nostrConnectPublish, nostrConnectSubscription } from "../helpers/applesauce.js";
import { NostrEvent } from "nostr-tools";
import eventCache from "./event-cache.js";
import { requestLoader } from "./loaders.js";
import config from "./config.js";
import { rxNostr } from "./rx-nostr.js";
import { logger } from "../logger.js";
import { NostrConnectAccount } from "applesauce-accounts/accounts";
NostrConnectSigner.subscriptionMethod = nostrConnectSubscription;
NostrConnectSigner.publishMethod = nostrConnectPublish;
const log = logger.extend("Owner");
export const ownerAccount$ = new BehaviorSubject<NostrConnectAccount<any> | undefined>(undefined);
// Update account when secrets change
secrets.watch("ownerAccount").subscribe((json) => {
// only load the account if its a new account
if (json && json.id !== ownerAccount$.value?.id) {
log("Loading owner account");
const account = NostrConnectAccount.fromJSON(json);
ownerAccount$.next(account);
account.signer
.connect()
.then(() => {
log("Owner account connected");
})
.catch((error) => {
log("Error connecting to owner account", error);
});
}
});
// Save the account when it changes
ownerAccount$.pipe(filter((account) => account !== undefined)).subscribe((account) => {
if (secrets.get("ownerAccount")?.id !== account.id) {
log("Saving owner account", account.id);
secrets.set("ownerAccount", account.toJSON());
}
});
// close the previous signer and connect the new one
ownerAccount$.pipe(pairwise()).subscribe(([prev, current]) => {
if (prev) {
log("Closing previous signer");
prev.signer.close();
}
if (current) {
log("Connecting to signer");
current.signer.connect();
}
});
export const ownerSigner = new ProxySigner(ownerAccount$);
export const ownerFactory = new EventFactory({ signer: ownerSigner });
export const ownerActions = new ActionHub(eventStore, ownerFactory);
export async function ownerPublish(event: NostrEvent) {
// save event to local stores
eventStore.add(event);
eventCache.addEvent(event);
// publish event to owners outboxes
if (config.data.owner) {
try {
const mailboxes = await requestLoader.mailboxes({ pubkey: config.data.owner });
await lastValueFrom(rxNostr.send(event, { on: { relays: mailboxes.outboxes } }));
} catch (error) {
// Failed to publish to outboxes, ignore error for now
// TODO: this should retried at some point
}
}
}

View File

@@ -77,7 +77,7 @@ export class MigrationSet {
// save the migration
database.transaction(() => {
const result = database
.prepare<[string, number, number]>(`INSERT INTO migrations (name, version, date) VALUES (?1, ?2, ?3)`)
.prepare<[string, number, number]>(`INSERT INTO migrations (name, version, date) VALUES (?, ?, ?)`)
.run(this.name, script.version, unixNow());
const insertLog = database.prepare<[number | bigint, string]>(