From 1f62afacb1a725c4d449686813f18457441804b0 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Sun, 22 Jan 2023 16:45:44 -0500 Subject: [PATCH] nip42 initial --- src/Feed/EventPublisher.ts | 29 +++++++++++++++ src/Nostr/Auth.ts | 19 ++++++++++ src/Nostr/Connection.ts | 74 ++++++++++++++++++++++++++++++++------ src/Nostr/EventKind.ts | 3 +- 4 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 src/Nostr/Auth.ts diff --git a/src/Feed/EventPublisher.ts b/src/Feed/EventPublisher.ts index 3c8cbd8..2847ae5 100644 --- a/src/Feed/EventPublisher.ts +++ b/src/Feed/EventPublisher.ts @@ -7,6 +7,8 @@ import { RootState } from "State/Store"; import { HexKey, RawEvent, u256, UserMetadata } from "Nostr"; import { bech32ToHex } from "Util" import { DefaultRelays, HashtagRegex } from "Const"; +import { useEffect } from "react"; +import { NIP42AuthChallenge, NIP42AuthResponse } from "Nostr/Auth"; declare global { interface Window { @@ -76,6 +78,33 @@ export default function useEventPublisher() { ev.Content = content; } + const nip42Auth = async (challenge: string, relay:string): Promise => { + if(pubKey) { + const ev = NEvent.ForPubKey(pubKey); + ev.Kind = EventKind.Auth; + ev.Content = ""; + ev.Tags.push(new Tag(["relay", relay], 0)); + ev.Tags.push(new Tag(["challenge", challenge], 1)); + return await signEvent(ev); + } + return Promise.reject(); + } + + useEffect(() =>{ + const nip42AuthEvent = async (event: NIP42AuthChallenge) => { + if(event.challenge && event.relay) { + const signedEvent = await nip42Auth(event.challenge, event.relay); + const response = new NIP42AuthResponse(event.challenge, signedEvent); + window.dispatchEvent(response); + } + } + window.addEventListener("nip42auth", nip42AuthEvent) + + return () => { + window.removeEventListener("nip42auth", nip42AuthEvent) + } + }, []) + return { broadcast: (ev: NEvent | undefined) => { if (ev) { diff --git a/src/Nostr/Auth.ts b/src/Nostr/Auth.ts new file mode 100644 index 0000000..c9a62c9 --- /dev/null +++ b/src/Nostr/Auth.ts @@ -0,0 +1,19 @@ +import {default as NEvent} from "./Event" + +export class NIP42AuthChallenge extends Event { + challenge?:string + relay?:string + constructor(challenge:string, relay:string) { + super("nip42auth"); + this.challenge = challenge; + this.relay = relay; + } +} + +export class NIP42AuthResponse extends Event { + event?: NEvent + constructor(challenge: string, event: NEvent) { + super(`nip42response:${challenge}`); + this.event = event; + } +} \ No newline at end of file diff --git a/src/Nostr/Connection.ts b/src/Nostr/Connection.ts index bd92816..a67949e 100644 --- a/src/Nostr/Connection.ts +++ b/src/Nostr/Connection.ts @@ -51,6 +51,8 @@ export default class Connection { IsClosed: boolean; ReconnectTimer: ReturnType | null; EventsCallback: Map void>; + Authed: boolean; + AwaitingAuth: boolean; constructor(addr: string, options: RelaySettings) { this.Id = uuid(); @@ -76,6 +78,8 @@ export default class Connection { this.IsClosed = false; this.ReconnectTimer = null; this.EventsCallback = new Map(); + this.Authed = false; + this.AwaitingAuth = false; this.Connect(); } @@ -123,17 +127,12 @@ export default class Connection { OnOpen(e: Event) { this.ConnectTimeout = DefaultConnectTimeout; console.log(`[${this.Address}] Open!`); - - // send pending - for (let p of this.Pending) { - this._SendJson(p); - } - this.Pending = []; - - for (let [_, s] of this.Subscriptions) { - this._SendSubscription(s); - } - this._UpdateState(); + setTimeout(() => { + if(this.AwaitingAuth) { + return + } + this._InitSubscriptions(); + }, 500) } OnClose(e: CloseEvent) { @@ -156,6 +155,12 @@ export default class Connection { let msg = JSON.parse(e.data); let tag = msg[0]; switch (tag) { + case "AUTH": { + this._OnAuth(msg[1]) + this.Stats.EventsReceived++; + this._UpdateState(); + break; + } case "EVENT": { this._OnEvent(msg[1], msg[2]); this.Stats.EventsReceived++; @@ -314,6 +319,19 @@ export default class Connection { } } + _InitSubscriptions() { + // send pending + for (let p of this.Pending) { + this._SendJson(p); + } + this.Pending = []; + + for (let [_, s] of this.Subscriptions) { + this._SendSubscription(s); + } + this._UpdateState(); + } + _SendSubscription(sub: Subscriptions) { let req = ["REQ", sub.Id, sub.ToObject()]; if (sub.OrSubs.length > 0) { @@ -349,6 +367,40 @@ export default class Connection { } } + async _OnAuth(challenge: string, timeout: number = 5000):Promise { + const challengeEvent = new NIP42AuthChallenge(challenge, this.Address) + + const authCallback = (e:NIP42AuthResponse):Promise => { + return new Promise((resolve,reject) => { + if(!e.event) { + return Promise.reject('no event'); + } + + let t = setTimeout(() => { + window.removeEventListener(`nip42response:${challenge}`, authCallback); + this.AwaitingAuth = false; + reject('timeout'); + }, timeout); + + this.EventsCallback.set(e.event.Id, () => { + clearTimeout(t); + this.AwaitingAuth = false; + window.removeEventListener(`nip42response:${challenge}`, authCallback) + resolve(); + }); + + let req = ["AUTH", e.event.ToObject()]; + this._SendJson(req); + this.Stats.EventsSent++; + this._UpdateState(); + }) + } + + this.AwaitingAuth = true; + window.addEventListener(`nip42response:${challenge}`, authCallback) + window.dispatchEvent(challengeEvent) + } + _OnEnd(subId: string) { let sub = this.Subscriptions.get(subId); if (sub) { diff --git a/src/Nostr/EventKind.ts b/src/Nostr/EventKind.ts index d12012b..5b4cfbd 100644 --- a/src/Nostr/EventKind.ts +++ b/src/Nostr/EventKind.ts @@ -7,7 +7,8 @@ const enum EventKind { DirectMessage = 4, // NIP-04 Deletion = 5, // NIP-09 Repost = 6, // NIP-18 - Reaction = 7 // NIP-25 + Reaction = 7, // NIP-25 + Auth = 22242 // NIP-42 }; export default EventKind; \ No newline at end of file