From bd65aca99dbe9bb83764b0d2a0c463147cd7b304 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 16 Aug 2024 08:58:55 -0700 Subject: [PATCH] Add some utils, add support for nip44 encryption to nip46 signer --- packages/lib/src/Tools.ts | 2 + packages/signer/src/signers/nip46.ts | 29 ++++++------ packages/store/src/index.ts | 36 +++++++------- packages/util/src/Address.ts | 6 +-- packages/util/src/Tags.ts | 70 +++++++++++++++------------- 5 files changed, 74 insertions(+), 69 deletions(-) diff --git a/packages/lib/src/Tools.ts b/packages/lib/src/Tools.ts index 6d2e0bc..91ccd72 100644 --- a/packages/lib/src/Tools.ts +++ b/packages/lib/src/Tools.ts @@ -91,6 +91,8 @@ export const mergeRight = >(a: T, b: T) => ({...a, export const between = (low: number, high: number, n: number) => n > low && n < high +export const randomInt = (min = 0, max = 9) => min + Math.round(Math.random()) * (max - min) + export const randomId = (): string => Math.random().toString().slice(2) export const stripProtocol = (url: string) => url.replace(/.*:\/\//, "") diff --git a/packages/signer/src/signers/nip46.ts b/packages/signer/src/signers/nip46.ts index 953cb06..273b4f4 100644 --- a/packages/signer/src/signers/nip46.ts +++ b/packages/signer/src/signers/nip46.ts @@ -1,11 +1,13 @@ -import {nip04, finalizeEvent, getPublicKey} from "nostr-tools" +import {finalizeEvent, getPublicKey} from "nostr-tools" import {hexToBytes} from '@noble/hashes/utils' import {Emitter, tryCatch, randomId, sleep, equals, now} from "@welshman/lib" import {createEvent, TrustedEvent, EventTemplate, NOSTR_CONNECT} from "@welshman/util" import {subscribe, publish, Subscription} from "@welshman/net" -import {ISigner, decrypt, hash, own} from '../util' +import {nip04, nip44, ISigner, decrypt, hash, own} from '../util' import {Nip01Signer} from './nip01' +export type Algorithm = "nip04" | "nip44" + export type Nip46Handler = { relays: string[] pubkey?: string @@ -20,10 +22,6 @@ export type Nip46Response = { let singleton: Nip46Broker -// FIXME set the full list of requested perms -const Perms = - "nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:4,sign_event:6,sign_event:7" - export class Nip46Broker extends Emitter { #sub: Subscription #signer: ISigner @@ -31,14 +29,15 @@ export class Nip46Broker extends Emitter { #closed = false #connectResult: any - static get(pubkey: string, secret: string, handler: Nip46Handler) { + static get(pubkey: string, secret: string, handler: Nip46Handler, algorithm: Algorithm = "nip04") { if ( singleton?.pubkey !== pubkey || singleton?.secret !== secret || - !equals(singleton?.handler, handler) + !equals(singleton?.handler, handler) || + singleton?.algorithm !== algorithm ) { singleton?.teardown() - singleton = new Nip46Broker(pubkey, secret, handler) + singleton = new Nip46Broker(pubkey, secret, handler, algorithm) } return singleton @@ -48,6 +47,7 @@ export class Nip46Broker extends Emitter { readonly pubkey: string, readonly secret: string, readonly handler: Nip46Handler, + readonly algorithm: Algorithm ) { super() @@ -98,7 +98,8 @@ export class Nip46Broker extends Emitter { const id = randomId() const pubkey = admin ? this.handler.pubkey! : this.pubkey const payload = JSON.stringify({id, method, params}) - const content = await nip04.encrypt(this.secret, pubkey, payload) + const crypt = this.algorithm === "nip04" ? nip04 : nip44 + const content = await crypt.encrypt(pubkey, this.secret, payload) const template = createEvent(NOSTR_CONNECT, {content, tags: [["p", pubkey]]}) const event = finalizeEvent(template, this.secret as any) @@ -119,17 +120,17 @@ export class Nip46Broker extends Emitter { }) } - createAccount = (username: string) => { + createAccount = (username: string, perms = "") => { if (!this.handler.domain) { throw new Error("Unable to create an account without a handler domain") } - return this.request("create_account", [username, this.handler.domain, "", Perms], true) + return this.request("create_account", [username, this.handler.domain, "", perms], true) } - connect = async (token = "") => { + connect = async (token = "", perms = "") => { if (!this.#connectResult) { - const params = [this.pubkey, token, Perms] + const params = [this.pubkey, token, perms] this.#connectResult = await this.request("connect", params) } diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index bfdff35..a29064b 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1,7 +1,8 @@ import {throttle} from "throttle-debounce" import {derived} from "svelte/store" -import type {Readable, Writable} from "svelte/store" +import type {Readable, Updater, Writable, Subscriber, Unsubscriber} from "svelte/store" import {identity, batch, partition, first} from "@welshman/lib" +import type {Maybe} from "@welshman/lib" import type {Repository} from "@welshman/util" import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util" import type {Filter, CustomEvent} from "@welshman/util" @@ -16,12 +17,10 @@ export const getter = (store: Readable) => { return () => value } -type Stop = () => void -type Sub = (x: T) => void -type Start = (set: Sub) => Stop +type Start = (set: Subscriber) => Unsubscriber export const custom = (start: Start, opts: {throttle?: number} = {}) => { - const subs: Sub[] = [] + const subs: Subscriber[] = [] let value: T let stop: () => void @@ -36,7 +35,7 @@ export const custom = (start: Start, opts: {throttle?: number} = {}) => { return { set, - subscribe: (sub: Sub) => { + subscribe: (sub: Subscriber) => { if (opts.throttle) { sub = throttle(opts.throttle, sub) } @@ -71,8 +70,8 @@ export function withGetter(store: Readable | Writable) { export const throttled = (delay: number, store: Readable) => custom(set => store.subscribe(throttle(delay, set))) -export const createEventStore = (repository: Repository) => { - let subs: Sub[] = [] +export const createEventStore = (repository: Repository): Writable => { + let subs: Subscriber[] = [] const onUpdate = throttle(300, () => { const $events = repository.dump() @@ -83,9 +82,9 @@ export const createEventStore = (repository: Repository) => { }) return { - get: () => repository.dump(), set: (events: CustomEvent[]) => repository.load(events), - subscribe: (f: Sub) => { + update: (f: Updater) => repository.load(f(repository.dump())), + subscribe: (f: Subscriber) => { f(repository.dump()) subs.push(f) @@ -105,17 +104,15 @@ export const createEventStore = (repository: Repository) => { } } -export const deriveEventsMapped = ({ +export const deriveEventsMapped = (repository: Repository, { filters, - repository, eventToItem, itemToEvent, throttle = 300, includeDeleted = false, }: { filters: Filter[] - repository: Repository, - eventToItem: (event: CustomEvent) => T | Promise + eventToItem: (event: CustomEvent) => Maybe> itemToEvent: (item: T) => CustomEvent throttle?: number includeDeleted?: boolean @@ -205,24 +202,23 @@ export const deriveEventsMapped = ({ return () => repository.off("update", onUpdate) }, {throttle}) -export const deriveEvents = (opts: {repository: Repository, filters: Filter[], includeDeleted?: boolean}) => - deriveEventsMapped({ +export const deriveEvents = (repository: Repository, opts: {filters: Filter[], includeDeleted?: boolean}) => + deriveEventsMapped(repository, { ...opts, eventToItem: identity, itemToEvent: identity, }) -export const deriveEvent = ({repository, idOrAddress}: {repository: Repository, idOrAddress: string}) => +export const deriveEvent = (repository: Repository, idOrAddress: string) => derived( - deriveEvents({ - repository, + deriveEvents(repository, { filters: getIdFilters([idOrAddress]), includeDeleted: true, }), first ) -export const deriveIsDeletedByAddress = ({repository, event}: {repository: Repository, event: CustomEvent}) => +export const deriveIsDeletedByAddress = (repository: Repository, event: CustomEvent) => custom(setter => { setter(repository.isDeletedByAddress(event)) diff --git a/packages/util/src/Address.ts b/packages/util/src/Address.ts index d1d7206..f5be6be 100644 --- a/packages/util/src/Address.ts +++ b/packages/util/src/Address.ts @@ -60,8 +60,8 @@ export class Address { export const getAddress = (e: AddressableEvent) => Address.fromEvent(e).toString() -export const isGroupAddress = (a: string, ...args: unknown[]) => Address.from(a).kind === GROUP +export const isGroupAddress = (a: string, ...args: unknown[]) => Address.isAddress(a) && Address.from(a).kind === GROUP -export const isCommunityAddress = (a: string, ...args: unknown[]) => Address.from(a).kind === COMMUNITY +export const isCommunityAddress = (a: string, ...args: unknown[]) => Address.isAddress(a) && Address.from(a).kind === COMMUNITY -export const isContextAddress = (a: string, ...args: unknown[]) => [GROUP, COMMUNITY].includes(Address.from(a).kind) +export const isContextAddress = (a: string, ...args: unknown[]) => Address.isAddress(a) && [GROUP, COMMUNITY].includes(Address.from(a).kind) diff --git a/packages/util/src/Tags.ts b/packages/util/src/Tags.ts index d8f25af..9e592a7 100644 --- a/packages/util/src/Tags.ts +++ b/packages/util/src/Tags.ts @@ -1,6 +1,6 @@ import {EventTemplate} from 'nostr-tools' import type {OmitStatics} from '@welshman/lib' -import {Fluent, nth, ensurePlural} from '@welshman/lib' +import {Fluent, nth, nthEq, ensurePlural} from '@welshman/lib' import {isRelayUrl, normalizeRelayUrl} from './Relay' import {Address, isContextAddress} from './Address' import {GROUP, COMMUNITY} from './Kinds' @@ -76,37 +76,7 @@ export class Tags extends (Fluent as OmitStatics, 'from' topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, "")) ancestors = (x?: boolean) => { - const tags = this.filterByKey(["a", "e", "q"]).reject(t => t.isContext()) - const mentionTags = tags.whereKey("q") - const roots: string[][] = [] - const replies: string[][] = [] - const mentions: string[][] = [] - - const dispatchTags = (thisTags: Tags) => - thisTags.forEach((t: Tag, i: number) => { - if (t.nth(3) === 'root') { - if (tags.filter(t => t.nth(3) === "reply").count() === 0) { - replies.push(t.valueOf()) - } else { - roots.push(t.valueOf()) - } - } else if (t.nth(3) === 'reply') { - replies.push(t.valueOf()) - } else if (t.nth(3) === 'mention') { - mentions.push(t.valueOf()) - } else if (i === thisTags.count() - 1) { - replies.push(t.valueOf()) - } else if (i === 0) { - roots.push(t.valueOf()) - } else { - mentions.push(t.valueOf()) - } - }) - - // Add different types separately so positional logic works - dispatchTags(tags.whereKey("e")) - dispatchTags(tags.whereKey("a").filter(t => Boolean(t.nth(3)))) - mentionTags.forEach((t: Tag) => mentions.push(t.valueOf())) + const {roots, replies, mentions} = getAncestorTags(this.unwrap()) return { roots: Tags.wrap(roots), @@ -240,3 +210,39 @@ export const getKindTags = (tags: string[][]) => tags.filter(t => ["k"].includes(t[0]) && t[1].match(/^\d+$/)) export const getKindTagValues = (tags: string[][]) => getKindTags(tags).map(t => parseInt(t[1])) + +export const getAncestorTags = (tags: string[][]) => { + const validTags = tags.filter(t => ["a", "e", "q"].includes(t[0]) && !isContextAddress(t[1])) + const mentionTags = validTags.filter(nthEq(0, "q")) + const roots: string[][] = [] + const replies: string[][] = [] + const mentions: string[][] = [] + + const dispatchTags = (thisTags: string[][]) => + thisTags.forEach((t: string[], i: number) => { + if (t[3] === 'root') { + if (validTags.filter(nthEq(3, "reply")).length === 0) { + replies.push(t) + } else { + roots.push(t) + } + } else if (t[3] === 'reply') { + replies.push(t) + } else if (t[3] === 'mention') { + mentions.push(t) + } else if (i === thisTags.length - 1) { + replies.push(t) + } else if (i === 0) { + roots.push(t) + } else { + mentions.push(t) + } + }) + + // Add different types separately so positional logic works + dispatchTags(validTags.filter(nthEq(0, "e"))) + dispatchTags(validTags.filter(nthEq(0, "a")).filter(t => Boolean(t[3]))) + mentionTags.forEach((t: string[]) => mentions.push(t)) + + return {roots, replies, mentions} +}