2023-07-05 10:41:47 +00:00
|
|
|
import { unwrap, bech32ToHex } from "@snort/shared";
|
|
|
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
|
|
import { v4 as uuid } from "uuid";
|
|
|
|
import debug from "debug";
|
|
|
|
|
|
|
|
import { Connection } from "../connection";
|
2023-08-17 18:54:14 +00:00
|
|
|
import { EventSigner, PrivateKeySigner } from "../signer";
|
2023-07-05 10:41:47 +00:00
|
|
|
import { NostrEvent } from "../nostr";
|
|
|
|
import { EventBuilder } from "../event-builder";
|
|
|
|
import EventKind from "../event-kind";
|
2024-03-04 12:22:48 +00:00
|
|
|
import { EventEmitter } from "eventemitter3";
|
2023-07-05 10:41:47 +00:00
|
|
|
|
|
|
|
const NIP46_KIND = 24_133;
|
2024-03-08 05:49:36 +00:00
|
|
|
// FIXME add all kinds that Snort signs
|
2024-04-04 10:21:38 +00:00
|
|
|
const PERMS =
|
|
|
|
"nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:3,sign_event:4,sign_event:6,sign_event:7,sign_event:30078";
|
2023-07-05 10:41:47 +00:00
|
|
|
|
|
|
|
interface Nip46Metadata {
|
2023-07-22 18:37:46 +00:00
|
|
|
name: string;
|
|
|
|
url?: string;
|
|
|
|
description?: string;
|
|
|
|
icons?: Array<string>;
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Nip46Request {
|
2023-07-22 18:37:46 +00:00
|
|
|
id: string;
|
|
|
|
method: string;
|
|
|
|
params: Array<any>;
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Nip46Response {
|
2023-07-22 18:37:46 +00:00
|
|
|
id: string;
|
|
|
|
result: any;
|
|
|
|
error: string;
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface QueueObj {
|
2024-02-15 11:28:09 +00:00
|
|
|
resolve: (o: Nip46Response) => void;
|
2023-07-22 18:37:46 +00:00
|
|
|
reject: (e: Error) => void;
|
2024-03-08 05:49:36 +00:00
|
|
|
authed?: boolean;
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
|
|
|
|
2024-02-15 11:28:09 +00:00
|
|
|
interface Nip46Events {
|
|
|
|
oauth: (url: string) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Nip46Signer extends EventEmitter<Nip46Events> implements EventSigner {
|
2023-07-22 18:37:46 +00:00
|
|
|
#conn?: Connection;
|
|
|
|
#relay: string;
|
|
|
|
#localPubkey: string;
|
|
|
|
#remotePubkey?: string;
|
|
|
|
#token?: string;
|
|
|
|
#insideSigner: EventSigner;
|
|
|
|
#commandQueue: Map<string, QueueObj> = new Map();
|
|
|
|
#log = debug("NIP-46");
|
|
|
|
#proto: string;
|
|
|
|
#didInit: boolean = false;
|
|
|
|
|
2024-02-15 11:28:09 +00:00
|
|
|
/**
|
|
|
|
* Start NIP-46 connection
|
|
|
|
* @param config bunker/nostrconnect://{npub/hex-pubkey}?relay={websocket-url}#{token-hex}
|
2024-02-15 16:52:37 +00:00
|
|
|
* @param insideSigner
|
2024-02-15 11:28:09 +00:00
|
|
|
*/
|
2023-07-22 18:37:46 +00:00
|
|
|
constructor(config: string, insideSigner?: EventSigner) {
|
2024-02-15 11:28:09 +00:00
|
|
|
super();
|
2023-07-22 18:37:46 +00:00
|
|
|
const u = new URL(config);
|
|
|
|
this.#proto = u.protocol;
|
2024-02-15 11:28:09 +00:00
|
|
|
this.#localPubkey = u.hostname || u.pathname.substring(2);
|
2023-07-22 18:37:46 +00:00
|
|
|
|
|
|
|
if (u.hash.length > 1) {
|
|
|
|
this.#token = u.hash.substring(1);
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
if (this.#localPubkey.startsWith("npub")) {
|
|
|
|
this.#localPubkey = bech32ToHex(this.#localPubkey);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#relay = unwrap(u.searchParams.get("relay"));
|
|
|
|
this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey());
|
|
|
|
}
|
|
|
|
|
2023-09-05 13:57:50 +00:00
|
|
|
get supports(): string[] {
|
2023-09-05 14:16:50 +00:00
|
|
|
return ["nip04"];
|
2023-09-05 13:57:50 +00:00
|
|
|
}
|
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
get relays() {
|
|
|
|
return [this.#relay];
|
|
|
|
}
|
2023-07-05 10:41:47 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
get privateKey() {
|
|
|
|
if (this.#insideSigner instanceof PrivateKeySigner) {
|
|
|
|
return this.#insideSigner.privateKey;
|
2023-07-10 14:40:22 +00:00
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-10 14:40:22 +00:00
|
|
|
|
2024-02-15 15:35:24 +00:00
|
|
|
/**
|
|
|
|
* Connect to the bunker relay
|
|
|
|
* @param autoConnect Start connect flow for pubkey
|
2024-02-15 16:52:37 +00:00
|
|
|
* @returns
|
2024-02-15 15:35:24 +00:00
|
|
|
*/
|
|
|
|
async init(autoConnect = true) {
|
2023-07-22 18:37:46 +00:00
|
|
|
const isBunker = this.#proto === "bunker:";
|
|
|
|
if (isBunker) {
|
|
|
|
this.#remotePubkey = this.#localPubkey;
|
|
|
|
this.#localPubkey = await this.#insideSigner.getPubKey();
|
2023-07-12 10:32:44 +00:00
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
return await new Promise<void>((resolve, reject) => {
|
|
|
|
this.#conn = new Connection(this.#relay, { read: true, write: true });
|
2023-11-07 13:28:01 +00:00
|
|
|
this.#conn.on("event", async (sub, e) => {
|
2023-07-22 18:37:46 +00:00
|
|
|
await this.#onReply(e);
|
2023-11-07 13:28:01 +00:00
|
|
|
});
|
|
|
|
this.#conn.on("connected", async () => {
|
2024-01-25 15:21:42 +00:00
|
|
|
this.#conn!.queueReq(
|
2023-07-22 18:37:46 +00:00
|
|
|
[
|
|
|
|
"REQ",
|
|
|
|
"reply",
|
|
|
|
{
|
|
|
|
kinds: [NIP46_KIND],
|
|
|
|
"#p": [this.#localPubkey],
|
2024-03-08 05:49:36 +00:00
|
|
|
// strfry doesn't always delete ephemeral events
|
|
|
|
since: Math.floor(Date.now() / 1000 - 10),
|
2023-07-22 18:37:46 +00:00
|
|
|
},
|
|
|
|
],
|
2024-02-15 16:52:37 +00:00
|
|
|
() => {},
|
2023-07-22 18:37:46 +00:00
|
|
|
);
|
2023-07-12 10:32:44 +00:00
|
|
|
|
2024-02-15 15:35:24 +00:00
|
|
|
if (autoConnect) {
|
|
|
|
if (isBunker) {
|
|
|
|
const rsp = await this.#connect(unwrap(this.#remotePubkey));
|
|
|
|
if (rsp.result === "ack") {
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject(rsp.error);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.#commandQueue.set("connect", {
|
2024-02-15 11:28:09 +00:00
|
|
|
reject,
|
2024-02-15 15:35:24 +00:00
|
|
|
resolve: () => {
|
|
|
|
resolve();
|
|
|
|
},
|
2024-02-15 11:28:09 +00:00
|
|
|
});
|
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
} else {
|
2024-02-15 15:35:24 +00:00
|
|
|
resolve();
|
2023-07-10 14:40:22 +00:00
|
|
|
}
|
2023-11-07 13:28:01 +00:00
|
|
|
});
|
2024-01-25 15:21:42 +00:00
|
|
|
this.#conn.connect();
|
2023-07-22 18:37:46 +00:00
|
|
|
this.#didInit = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async close() {
|
|
|
|
if (this.#conn) {
|
|
|
|
await this.#disconnect();
|
2024-01-25 15:21:42 +00:00
|
|
|
this.#conn.closeReq("reply");
|
|
|
|
this.#conn.close();
|
2023-07-22 18:37:46 +00:00
|
|
|
this.#conn = undefined;
|
|
|
|
this.#didInit = false;
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-05 10:41:47 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async describe() {
|
2024-02-15 11:28:09 +00:00
|
|
|
const rsp = await this.#rpc("describe", []);
|
|
|
|
return rsp.result as Array<string>;
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-10 14:40:22 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async getPubKey() {
|
2024-02-15 15:35:24 +00:00
|
|
|
//const rsp = await this.#rpc("get_public_key", []);
|
|
|
|
//return rsp.result as string;
|
|
|
|
return this.#remotePubkey!;
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-10 14:40:22 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async nip4Encrypt(content: string, otherKey: string) {
|
2024-02-15 11:28:09 +00:00
|
|
|
const rsp = await this.#rpc("nip04_encrypt", [otherKey, content]);
|
|
|
|
return rsp.result as string;
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-05 10:41:47 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async nip4Decrypt(content: string, otherKey: string) {
|
2024-02-15 11:28:09 +00:00
|
|
|
const rsp = await this.#rpc("nip04_decrypt", [otherKey, content]);
|
2024-02-15 15:35:24 +00:00
|
|
|
return rsp.result as string;
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-05 10:41:47 +00:00
|
|
|
|
2023-08-17 18:54:14 +00:00
|
|
|
nip44Encrypt(content: string, key: string): Promise<string> {
|
|
|
|
throw new Error("Method not implemented.");
|
|
|
|
}
|
|
|
|
|
|
|
|
nip44Decrypt(content: string, otherKey: string): Promise<string> {
|
|
|
|
throw new Error("Method not implemented.");
|
|
|
|
}
|
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async sign(ev: NostrEvent) {
|
2024-02-15 11:28:09 +00:00
|
|
|
const rsp = await this.#rpc("sign_event", [JSON.stringify(ev)]);
|
|
|
|
return JSON.parse(rsp.result as string);
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-05 10:41:47 +00:00
|
|
|
|
2024-02-15 11:40:05 +00:00
|
|
|
/**
|
|
|
|
* NIP-46 oAuth bunker signup
|
|
|
|
* @param name Desired name
|
|
|
|
* @param domain Desired domain
|
|
|
|
* @param email Backup email address
|
2024-02-15 16:52:37 +00:00
|
|
|
* @returns
|
2024-02-15 11:40:05 +00:00
|
|
|
*/
|
|
|
|
async createAccount(name: string, domain: string, email?: string) {
|
2024-02-15 15:35:24 +00:00
|
|
|
await this.init(false);
|
2024-03-08 05:49:36 +00:00
|
|
|
const rsp = await this.#rpc("create_account", [name, domain, email ?? "", PERMS]);
|
2024-02-15 15:35:24 +00:00
|
|
|
if (!rsp.error) {
|
|
|
|
this.#remotePubkey = rsp.result as string;
|
2024-02-15 11:40:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async #disconnect() {
|
|
|
|
return await this.#rpc("disconnect", []);
|
|
|
|
}
|
2023-07-10 14:40:22 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async #connect(pk: string) {
|
2024-04-04 10:21:38 +00:00
|
|
|
const connectParams = [pk, this.#token ?? "", PERMS];
|
2024-02-15 11:28:09 +00:00
|
|
|
return await this.#rpc("connect", connectParams);
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-05 10:41:47 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
async #onReply(e: NostrEvent) {
|
|
|
|
if (e.kind !== NIP46_KIND) {
|
|
|
|
throw new Error("Unknown event kind");
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, e.pubkey);
|
|
|
|
const reply = JSON.parse(decryptedContent) as Nip46Request | Nip46Response;
|
|
|
|
|
|
|
|
let id = reply.id;
|
|
|
|
this.#log("Recv: %O", reply);
|
|
|
|
if ("method" in reply && reply.method === "connect") {
|
|
|
|
this.#remotePubkey = reply.params[0];
|
|
|
|
await this.#sendCommand(
|
|
|
|
{
|
|
|
|
id: reply.id,
|
|
|
|
result: "ack",
|
|
|
|
error: "",
|
|
|
|
},
|
2023-07-24 14:30:21 +00:00
|
|
|
unwrap(this.#remotePubkey),
|
2023-07-22 18:37:46 +00:00
|
|
|
);
|
|
|
|
id = "connect";
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
const pending = this.#commandQueue.get(id);
|
|
|
|
if (!pending) {
|
|
|
|
throw new Error("No pending command found");
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|
2023-07-10 14:40:22 +00:00
|
|
|
|
2024-02-15 15:35:24 +00:00
|
|
|
if ("result" in reply && reply.result === "auth_url") {
|
2024-03-08 05:49:36 +00:00
|
|
|
if (!pending.authed) this.emit("oauth", reply.error);
|
|
|
|
pending.authed = true;
|
2024-02-15 15:35:24 +00:00
|
|
|
} else {
|
|
|
|
const rx = reply as Nip46Response;
|
|
|
|
if (rx.error) {
|
|
|
|
pending.reject(new Error(rx.error));
|
|
|
|
} else {
|
|
|
|
pending.resolve(rx);
|
|
|
|
}
|
|
|
|
this.#commandQueue.delete(reply.id);
|
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-10 14:40:22 +00:00
|
|
|
|
2024-02-15 11:28:09 +00:00
|
|
|
async #rpc(method: string, params: Array<any>) {
|
2023-07-22 18:37:46 +00:00
|
|
|
if (!this.#didInit) {
|
|
|
|
await this.init();
|
2023-07-10 14:40:22 +00:00
|
|
|
}
|
2023-07-22 18:37:46 +00:00
|
|
|
if (!this.#conn) throw new Error("Connection error");
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
id: uuid(),
|
|
|
|
method,
|
|
|
|
params,
|
|
|
|
} as Nip46Request;
|
|
|
|
|
|
|
|
this.#sendCommand(payload, unwrap(this.#remotePubkey));
|
2024-02-15 11:28:09 +00:00
|
|
|
return await new Promise<Nip46Response>((resolve, reject) => {
|
2023-07-22 18:37:46 +00:00
|
|
|
this.#commandQueue.set(payload.id, {
|
|
|
|
resolve: async (o: Nip46Response) => {
|
2024-02-15 11:28:09 +00:00
|
|
|
resolve(o);
|
2023-07-22 18:37:46 +00:00
|
|
|
},
|
|
|
|
reject,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async #sendCommand(payload: Nip46Request | Nip46Response, target: string) {
|
|
|
|
if (!this.#conn) return;
|
|
|
|
|
|
|
|
const eb = new EventBuilder();
|
|
|
|
eb.kind(NIP46_KIND as EventKind)
|
|
|
|
.content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), target))
|
|
|
|
.tag(["p", target]);
|
|
|
|
|
|
|
|
this.#log("Send: %O", payload);
|
|
|
|
const evCommand = await eb.buildAndSign(this.#insideSigner);
|
2024-01-25 15:21:42 +00:00
|
|
|
await this.#conn.sendEventAsync(evCommand);
|
2023-07-22 18:37:46 +00:00
|
|
|
}
|
2023-07-05 10:41:47 +00:00
|
|
|
}
|