@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
text/typescript
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);
}
}