feat: implement NIP-09 event deletion for service announcements (v0.1.19)

This commit is contained in:
gzuuus
2025-03-25 23:44:18 +01:00
parent d44ab2fe27
commit ff9786c45a
5 changed files with 99 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ A bridge implementation that connects Model Context Protocol (MCP) servers to No
- Automatic service announcement using NIP-89 - Automatic service announcement using NIP-89
- Tool discovery and execution through DVM kind:5910/6910 events - Tool discovery and execution through DVM kind:5910/6910 events
- Job status updates and payment handling via kind:7000 events - Job status updates and payment handling via kind:7000 events
- Service announcement deletion using NIP-09
- Comprehensive error handling - Comprehensive error handling
## Configuration ## Configuration
@@ -47,6 +48,22 @@ For production:
bun run start bun run start
``` ```
### Deleting Service Announcements
To remove your service announcements from relays when shutting down or taking your service offline, you can use the `--delete-announcement` flag:
```bash
bun run start --delete-announcement
```
You can also provide an optional reason for the deletion:
```bash
bun run start --delete-announcement --reason "Service maintenance in progress"
```
This will send a NIP-09 deletion event (kind 5) to all connected relays, instructing them to remove your previously published service announcements.
## Testing ## Testing
Run the test suite: Run the test suite:

View File

@@ -14,6 +14,7 @@ import {
import { argv } from 'process'; import { argv } from 'process';
import type { Config } from './src/types'; import type { Config } from './src/types';
import { setConfigPath } from './src/config.js'; import { setConfigPath } from './src/config.js';
import { DVMBridge } from './src/dvm-bridge.js';
const defaultConfigPath = join(process.cwd(), 'config.dvmcp.yml'); const defaultConfigPath = join(process.cwd(), 'config.dvmcp.yml');
let configPath = defaultConfigPath; let configPath = defaultConfigPath;
@@ -118,9 +119,38 @@ const runApp = async () => {
await main.default(); await main.default();
}; };
const deleteAnnouncement = async () => {
// Get optional reason from command line arguments
const reasonIndex = argv.indexOf('--reason');
const reason = reasonIndex !== -1 && argv[reasonIndex + 1] ? argv[reasonIndex + 1] : undefined;
// Import DVMBridge and perform deletion
const bridge = new DVMBridge();
try {
console.log(`${CONFIG_EMOJIS.INFO} Deleting service announcement...`);
await bridge.deleteAnnouncement(reason);
console.log(`${CONFIG_EMOJIS.SUCCESS} Service announcement deleted successfully`);
process.exit(0);
} catch (error) {
console.error(`${CONFIG_EMOJIS.INFO} Failed to delete service announcement:`, error);
process.exit(1);
}
};
const cliMain = async () => { const cliMain = async () => {
if (argv.includes('--configure')) { if (argv.includes('--configure')) {
await configure(); await configure();
return;
}
if (argv.includes('--delete-announcement')) {
if (!existsSync(configPath)) {
console.error(`${CONFIG_EMOJIS.INFO} No configuration file found at ${configPath}`);
process.exit(1);
}
await deleteAnnouncement();
return;
} }
if (!existsSync(configPath)) { if (!existsSync(configPath)) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dvmcp/bridge", "name": "@dvmcp/bridge",
"version": "0.1.18", "version": "0.1.19",
"description": "Bridge connecting MCP servers to Nostr's DVM ecosystem", "description": "Bridge connecting MCP servers to Nostr's DVM ecosystem",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",

View File

@@ -7,6 +7,7 @@ import {
DVM_ANNOUNCEMENT_KIND, DVM_ANNOUNCEMENT_KIND,
TOOL_REQUEST_KIND, TOOL_REQUEST_KIND,
} from '@dvmcp/commons/constants'; } from '@dvmcp/commons/constants';
import type { Event } from 'nostr-tools/pure';
export const keyManager = createKeyManager(CONFIG.nostr.privateKey); export const keyManager = createKeyManager(CONFIG.nostr.privateKey);
@@ -57,4 +58,37 @@ export class NostrAnnouncer {
async updateAnnouncement() { async updateAnnouncement() {
await Promise.all([this.announceService(), this.announceRelayList()]); await Promise.all([this.announceService(), this.announceRelayList()]);
} }
/**
* Deletes the service announcement from relays using NIP-09
* @param reason Optional reason for deletion
* @returns The deletion event that was published
*/
async deleteAnnouncement(reason: string = 'Service offline'): Promise<Event> {
// First, query the relays to find our announcement event
const announcementFilter = {
kinds: [DVM_ANNOUNCEMENT_KIND],
authors: [keyManager.getPublicKey()],
};
const events = await this.relayHandler.queryEvents(announcementFilter);
// Create the deletion event (NIP-09)
const deletionEvent = keyManager.signEvent({
...keyManager.createEventTemplate(5), // kind 5 is for deletion requests
content: reason,
tags: [
// Add tags for each event to be deleted
...events.map(event => ['e', event.id]),
// Add the kind of events being deleted
['k', `${DVM_ANNOUNCEMENT_KIND}`],
],
});
// Publish the deletion event
await this.relayHandler.publishEvent(deletionEvent);
console.log(`Published deletion event for service announcement`);
return deletionEvent;
}
} }

View File

@@ -83,6 +83,23 @@ export class DVMBridge {
} }
} }
/**
* Deletes the service announcement from relays
* @param reason Optional reason for deletion
* @returns The deletion event that was published
*/
async deleteAnnouncement(reason?: string) {
console.log('Deleting service announcement from relays...');
try {
const deletionEvent = await this.nostrAnnouncer.deleteAnnouncement(reason);
console.log('Service announcement deleted successfully');
return deletionEvent;
} catch (error) {
console.error('Error deleting service announcement:', error);
throw error;
}
}
private async handleRequest(event: Event) { private async handleRequest(event: Event) {
try { try {
if (this.isWhitelisted(event.pubkey)) { if (this.isWhitelisted(event.pubkey)) {