UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit. Includes AI Guardrails to catch common mistakes during development.

233 lines (206 loc) 8.52 kB
import { hexToBytes } from "@noble/hashes/utils"; import type { NDKEvent } from "../../../events/index.js"; import type { NDK } from "../../../ndk/index.js"; import type { NDKUser } from "../../../user/index.js"; import type { NDKSigner } from "../../index.js"; import { NDKPrivateKeySigner } from "../../private-key/index.js"; import { NDKNostrRpc } from "../rpc.js"; import ConnectEventHandlingStrategy from "./connect.js"; import GetPublicKeyHandlingStrategy from "./get-public-key.js"; import Nip04DecryptHandlingStrategy from "./nip04-decrypt.js"; import Nip04EncryptHandlingStrategy from "./nip04-encrypt.js"; import Nip44DecryptHandlingStrategy from "./nip44-decrypt.js"; import Nip44EncryptHandlingStrategy from "./nip44-encrypt.js"; import PingEventHandlingStrategy from "./ping.js"; import SignEventHandlingStrategy from "./sign-event.js"; import SwitchRelaysEventHandlingStrategy from "./switch-relays.js"; export type NIP46Method = | "connect" | "sign_event" | "nip04_encrypt" | "nip04_decrypt" | "nip44_encrypt" | "nip44_decrypt" | "get_public_key" | "ping" | "switch_relays"; export type Nip46PermitCallbackParams = { /** * ID of the request */ id: string; pubkey: string; method: NIP46Method; // eslint-disable-next-line @typescript-eslint/no-explicit-any params?: any; }; export type Nip46PermitCallback = (params: Nip46PermitCallbackParams) => Promise<boolean>; export type Nip46ApplyTokenCallback = (pubkey: string, token: string) => Promise<void>; export interface IEventHandlingStrategy { handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]): Promise<string | undefined>; } /** * This class implements a NIP-46 backend, meaning that it will hold a private key * of the npub that wants to be published as. * * This backend is meant to be used by an NDKNip46Signer, which is the class that * should run client-side, where the user wants to sign events from. */ export class NDKNip46Backend { readonly ndk: NDK; readonly signer: NDKSigner; public localUser?: NDKUser; readonly debug: debug.Debugger; public rpc: NDKNostrRpc; private permitCallback: Nip46PermitCallback; public relayUrls: WebSocket["url"][]; /** * @param ndk The NDK instance to use * @param signer The signer for the private key that wants to be published as * @param permitCallback Callback executed when permission is requested */ public constructor( ndk: NDK, signer: NDKSigner, permitCallback: Nip46PermitCallback, relayUrls?: WebSocket["url"][], ); /** * @param ndk The NDK instance to use * @param privateKey The private key of the npub that wants to be published as * @param permitCallback Callback executed when permission is requested */ public constructor( ndk: NDK, privateKey: string, permitCallback: Nip46PermitCallback, relayUrls?: WebSocket["url"][], ); /** * @param ndk The NDK instance to use * @param privateKeyOrSigner The private key or signer of the npub that wants to be published as * @param permitCallback Callback executed when permission is requested */ public constructor( ndk: NDK, privateKeyOrSigner: string | NDKSigner, permitCallback: Nip46PermitCallback, relayUrls?: WebSocket["url"][], ) { this.ndk = ndk; if (privateKeyOrSigner instanceof Uint8Array) { this.signer = new NDKPrivateKeySigner(privateKeyOrSigner as Uint8Array); } else if (privateKeyOrSigner instanceof String) { this.signer = new NDKPrivateKeySigner(hexToBytes(privateKeyOrSigner as string)); } else if (privateKeyOrSigner instanceof NDKPrivateKeySigner) { this.signer = privateKeyOrSigner as NDKPrivateKeySigner; } else { throw new Error("Invalid signer"); } this.debug = ndk.debug.extend("nip46:backend"); this.relayUrls = relayUrls ?? Array.from(ndk.pool.relays.keys()); this.rpc = new NDKNostrRpc(ndk, this.signer, this.debug, this.relayUrls); this.permitCallback = permitCallback; } /** * This method starts the backend, which will start listening for incoming * requests. */ public async start() { this.localUser = await this.signer.user(); this.ndk.subscribe( { kinds: [24133 as number], "#p": [this.localUser.pubkey], }, { closeOnEose: false, onEvent: (e) => this.handleIncomingEvent(e), }, ); } public handlers: { [method: string]: IEventHandlingStrategy } = { connect: new ConnectEventHandlingStrategy(), sign_event: new SignEventHandlingStrategy(), nip04_encrypt: new Nip04EncryptHandlingStrategy(), nip04_decrypt: new Nip04DecryptHandlingStrategy(), nip44_encrypt: new Nip44EncryptHandlingStrategy(), nip44_decrypt: new Nip44DecryptHandlingStrategy(), get_public_key: new GetPublicKeyHandlingStrategy(), ping: new PingEventHandlingStrategy(), switch_relays: new SwitchRelaysEventHandlingStrategy(), }; /** * Enables the user to set a custom strategy for handling incoming events. * @param method - The method to set the strategy for * @param strategy - The strategy to set */ public setStrategy(method: string, strategy: IEventHandlingStrategy) { this.handlers[method] = strategy; } /** * Overload this method to apply tokens, which can * wrap permission sets to be applied to a pubkey. * @param pubkey public key to apply token to * @param token token to apply */ async applyToken(_pubkey: string, _token: string): Promise<void> { throw new Error("connection token not supported"); } protected async handleIncomingEvent(event: NDKEvent) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { id, method, params } = (await this.rpc.parseEvent(event)) as any; const remotePubkey = event.pubkey; let response: string | undefined; let errorHandled = false; this.debug("incoming event", { id, method, params }); // validate signature explicitly // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!event.verifySignature(false)) { this.debug("invalid signature", event.rawEvent()); return; } const strategy = this.handlers[method]; if (strategy) { try { response = await strategy.handle(this, id, remotePubkey, params); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { this.debug("error handling event", e, { id, method, params }); errorHandled = true; try { await this.rpc.sendResponse(id, remotePubkey, "error", undefined, e.message); } catch (sendError: any) { this.debug("failed to send error response", sendError); } } } else { this.debug("unsupported method", { method, params }); } // Only send response if we haven't already handled an error if (!errorHandled) { try { if (response) { this.debug(`sending response to ${remotePubkey}`, response); await this.rpc.sendResponse(id, remotePubkey, response); } else { await this.rpc.sendResponse(id, remotePubkey, "error", undefined, "Not authorized"); } } catch (sendError: any) { this.debug("failed to send response", sendError); } } // After sending switch_relays response, update the RPC to use the preferred relays // so future responses are published on the new relay set if (method === "switch_relays" && response) { this.rpc.updateRelays(this.relayUrls); } } /** * This method should be overriden by the user to allow or reject incoming * connections. */ public async pubkeyAllowed(params: Nip46PermitCallbackParams): Promise<boolean> { return this.permitCallback(params); } }