Files
uptime-kuma/server/notification-providers/nostr.js
tionis 0981fee9b2 feat(nostr): switch to gift-wrapped events (#6677)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-12 04:23:52 +01:00

116 lines
4.1 KiB
JavaScript

const NotificationProvider = require("./notification-provider");
const { finalizeEvent, Relay, nip19, nip59 } = require("nostr-tools");
// polyfill WebSocket for nostr-tools
global.WebSocket = require("isomorphic-ws");
class Nostr extends NotificationProvider {
name = "nostr";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const senderPrivateKey = await this.getPrivateKey(notification.sender);
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
// Create NIP-59 gift-wrapped events for each recipient
// This uses NIP-17 kind 14 (private direct message) wrapped with NIP-59
// to prevent metadata leakage (sender/recipient public keys are hidden)
const createdAt = Math.floor(Date.now() / 1000);
const events = [];
for (const recipientPublicKey of recipientsPublicKeys) {
const event = {
kind: 14, // NIP-17 private direct message
created_at: createdAt,
tags: [["p", recipientPublicKey]],
content: msg,
};
try {
const wrappedEvent = nip59.wrapEvent(event, senderPrivateKey, recipientPublicKey);
events.push(wrappedEvent);
} catch (error) {
throw new Error(`Failed to create gift-wrapped event for recipient: ${error.message}`);
}
}
// Publish events to each relay
const relays = notification.relays.split("\n");
let successfulRelays = 0;
for (const relayUrl of relays) {
const relay = await Relay.connect(relayUrl);
let eventIndex = 0;
// Authenticate to the relay, if required
try {
await relay.publish(events[0]);
eventIndex = 1;
} catch (error) {
if (relay.challenge) {
await relay.auth(async (evt) => {
return finalizeEvent(evt, senderPrivateKey);
});
}
}
try {
for (let i = eventIndex; i < events.length; i++) {
await relay.publish(events[i]);
}
successfulRelays++;
} catch (error) {
console.error(`Failed to publish event to ${relayUrl}:`, error);
} finally {
relay.close();
}
}
// Report success or failure
if (successfulRelays === 0) {
throw Error("Failed to connect to any relays.");
}
return `${successfulRelays}/${relays.length} relays connected.`;
}
/**
* Get the private key for the sender
* @param {string} sender Sender to retrieve key for
* @returns {nip19.DecodeResult} Private key
*/
async getPrivateKey(sender) {
try {
const senderDecodeResult = await nip19.decode(sender);
const { data } = senderDecodeResult;
return data;
} catch (error) {
throw new Error(`Failed to decode private key for sender ${sender}: ${error.message}`);
}
}
/**
* Get public keys for recipients
* @param {string} recipients Newline delimited list of recipients
* @returns {Promise<nip19.DecodeResult[]>} Public keys
*/
async getPublicKeys(recipients) {
const recipientsList = recipients.split("\n");
const publicKeys = [];
for (const recipient of recipientsList) {
try {
const recipientDecodeResult = await nip19.decode(recipient);
const { type, data } = recipientDecodeResult;
if (type === "npub") {
publicKeys.push(data);
} else {
throw new Error(`Recipient ${recipient} is not an npub`);
}
} catch (error) {
throw new Error(`Error decoding recipient ${recipient}: ${error}`);
}
}
return publicKeys;
}
}
module.exports = Nostr;