Add some utils, add support for nip44 encryption to nip46 signer

This commit is contained in:
Jon Staab
2024-08-16 08:58:55 -07:00
parent e38422c9ba
commit bd65aca99d
5 changed files with 74 additions and 69 deletions
+2
View File
@@ -91,6 +91,8 @@ export const mergeRight = <T extends Record<string, any>>(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(/.*:\/\//, "")
+15 -14
View File
@@ -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)
}
+16 -20
View File
@@ -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 = <T>(store: Readable<T>) => {
return () => value
}
type Stop = () => void
type Sub<T> = (x: T) => void
type Start<T> = (set: Sub<T>) => Stop
type Start<T> = (set: Subscriber<T>) => Unsubscriber
export const custom = <T>(start: Start<T>, opts: {throttle?: number} = {}) => {
const subs: Sub<T>[] = []
const subs: Subscriber<T>[] = []
let value: T
let stop: () => void
@@ -36,7 +35,7 @@ export const custom = <T>(start: Start<T>, opts: {throttle?: number} = {}) => {
return {
set,
subscribe: (sub: Sub<T>) => {
subscribe: (sub: Subscriber<T>) => {
if (opts.throttle) {
sub = throttle(opts.throttle, sub)
}
@@ -71,8 +70,8 @@ export function withGetter<T>(store: Readable<T> | Writable<T>) {
export const throttled = <T>(delay: number, store: Readable<T>) =>
custom(set => store.subscribe(throttle(delay, set)))
export const createEventStore = (repository: Repository) => {
let subs: Sub<CustomEvent[]>[] = []
export const createEventStore = (repository: Repository): Writable<CustomEvent[]> => {
let subs: Subscriber<CustomEvent[]>[] = []
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<CustomEvent[]>) => {
update: (f: Updater<CustomEvent[]>) => repository.load(f(repository.dump())),
subscribe: (f: Subscriber<CustomEvent[]>) => {
f(repository.dump())
subs.push(f)
@@ -105,17 +104,15 @@ export const createEventStore = (repository: Repository) => {
}
}
export const deriveEventsMapped = <T>({
export const deriveEventsMapped = <T>(repository: Repository, {
filters,
repository,
eventToItem,
itemToEvent,
throttle = 300,
includeDeleted = false,
}: {
filters: Filter[]
repository: Repository,
eventToItem: (event: CustomEvent) => T | Promise<T>
eventToItem: (event: CustomEvent) => Maybe<T | Promise<T>>
itemToEvent: (item: T) => CustomEvent
throttle?: number
includeDeleted?: boolean
@@ -205,24 +202,23 @@ export const deriveEventsMapped = <T>({
return () => repository.off("update", onUpdate)
}, {throttle})
export const deriveEvents = (opts: {repository: Repository, filters: Filter[], includeDeleted?: boolean}) =>
deriveEventsMapped<CustomEvent>({
export const deriveEvents = (repository: Repository, opts: {filters: Filter[], includeDeleted?: boolean}) =>
deriveEventsMapped<CustomEvent>(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<boolean>(setter => {
setter(repository.isDeletedByAddress(event))
+3 -3
View File
@@ -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)
+38 -32
View File
@@ -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<Tag> as OmitStatics<typeof Fluent<Tag>, '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}
}