Name signers after nips
This commit is contained in:
@@ -121,9 +121,15 @@ export const toggle = <T>(x: T, xs: T[]) => xs.includes(x) ? remove(x, xs) : app
|
||||
|
||||
export const clamp = ([min, max]: [number, number], n: number) => Math.min(max, Math.max(min, n))
|
||||
|
||||
export const tryCatch = async <T>(f: () => Promise<T | void> | T | void, onError?: (e: Error) => void): Promise<T | void> => {
|
||||
export const tryCatch = <T>(f: () => T, onError?: (e: Error) => void): T | undefined => {
|
||||
try {
|
||||
return await f()
|
||||
const r = f()
|
||||
|
||||
if (r instanceof Promise) {
|
||||
r.catch(e => onError?.(e as Error))
|
||||
}
|
||||
|
||||
return r
|
||||
} catch (e) {
|
||||
onError?.(e as Error)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './util'
|
||||
export * from './wrapper'
|
||||
export * from './signers/secret'
|
||||
export * from './signers/connect'
|
||||
export * from './signers/extension'
|
||||
export * from './nip59'
|
||||
export * from './signers/nip01'
|
||||
export * from './signers/nip07'
|
||||
export * from './signers/nip46'
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {UnwrappedEvent, SignedEvent, HashedEvent, EventTemplate, WRAP, SEAL} from '@welshman/util'
|
||||
import {own, hash, decrypt, ISigner} from './util'
|
||||
import {Nip01Signer} from './signers/nip01'
|
||||
|
||||
export const seen = new Map<string, UnwrappedEvent | Error>()
|
||||
|
||||
export const now = (drift = 0) =>
|
||||
Math.round(Date.now() / 1000 - Math.random() * Math.pow(10, drift))
|
||||
|
||||
export const getRumor = async (signer: ISigner, template: EventTemplate) =>
|
||||
hash(own(await signer.getPubkey(), template))
|
||||
|
||||
export const getSeal = async (signer: ISigner, pubkey: string, rumor: HashedEvent) =>
|
||||
signer.sign(hash({
|
||||
kind: SEAL,
|
||||
pubkey: await signer.getPubkey(),
|
||||
content: await signer.nip44.encrypt(pubkey, JSON.stringify(rumor)),
|
||||
created_at: now(5),
|
||||
tags: [],
|
||||
}))
|
||||
|
||||
export const getWrap = async (wrapper: ISigner, pubkey: string, seal: SignedEvent, tags: string[][]) =>
|
||||
wrapper.sign(hash({
|
||||
kind: WRAP,
|
||||
pubkey: await wrapper.getPubkey(),
|
||||
content: await wrapper.nip44.encrypt(pubkey, JSON.stringify(seal)),
|
||||
created_at: now(5),
|
||||
tags: [...tags, ["p", pubkey]],
|
||||
}))
|
||||
|
||||
export const wrap = async (signer: ISigner, wrapper: ISigner, pubkey: string, template: EventTemplate, tags: string[][] = []) => {
|
||||
const rumor = await getRumor(signer, template)
|
||||
const seal = await getSeal(signer, pubkey, rumor)
|
||||
const wrap = await getWrap(wrapper, pubkey, seal, tags)
|
||||
|
||||
return Object.assign(rumor, {wrap}) as UnwrappedEvent
|
||||
}
|
||||
|
||||
export const unwrap = async (signer: ISigner, wrap: SignedEvent) => {
|
||||
// Avoid decrypting the same event multiple times
|
||||
if (seen.has(wrap.id)) {
|
||||
const rumorOrError = seen.get(wrap.id)
|
||||
|
||||
if (rumorOrError instanceof Error) {
|
||||
throw rumorOrError
|
||||
} else {
|
||||
return rumorOrError
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const seal = JSON.parse(await decrypt(signer, wrap.pubkey, wrap.content))
|
||||
const rumor = JSON.parse(await decrypt(signer, seal.pubkey, seal.content))
|
||||
|
||||
if (seal.pubkey !== rumor.pubkey) throw new Error("Seal pubkey does not match rumor pubkey")
|
||||
|
||||
seen.set(wrap.id, rumor)
|
||||
|
||||
return Object.assign(rumor, {wrap}) as UnwrappedEvent
|
||||
} catch (error) {
|
||||
seen.set(wrap.id, error as Error)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// This is a utility that makes it harder to re-use wrapper signers, since that can result in
|
||||
// leaked metadata. It simultaneously makes it easier to wrap stuff, because it allows for
|
||||
// wrapping a single user signer and omit the wrapper signer argument to wrap, while still
|
||||
// making it possible to pass a wrapper signer if desired.
|
||||
export class Nip59 {
|
||||
constructor(private signer: ISigner, private wrapper?: ISigner) {}
|
||||
|
||||
static fromSigner = (signer: ISigner) => new Nip59(signer)
|
||||
|
||||
static fromSecret = (secret: string) => new Nip59(new Nip01Signer(secret))
|
||||
|
||||
withWrapper = (wrapper: ISigner) => new Nip59(this.signer, wrapper)
|
||||
|
||||
wrap = (pubkey: string, template: EventTemplate, tags: string[][] = []) =>
|
||||
wrap(this.signer, this.wrapper || Nip01Signer.ephemeral(), pubkey, template, tags)
|
||||
|
||||
unwrap = (event: SignedEvent) =>
|
||||
unwrap(this.signer, event)
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import {EventTemplate} from '@welshman/util'
|
||||
import {nip04, nip44, own, hash, sign, getPubkey, ISigner} from "../util"
|
||||
import {nip04, nip44, own, hash, sign, getPubkey, ISigner, makeSecret} from "../util"
|
||||
|
||||
export class SecretSigner implements ISigner {
|
||||
export class Nip01Signer implements ISigner {
|
||||
private pubkey: string
|
||||
|
||||
constructor(private secret: string) {
|
||||
this.pubkey = getPubkey(this.secret)
|
||||
}
|
||||
|
||||
isEnabled = () => true
|
||||
static fromSecret = (secret: string) => new Nip01Signer(secret)
|
||||
|
||||
static ephemeral = () => new Nip01Signer(makeSecret())
|
||||
|
||||
getPubkey = async () => this.pubkey
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import {EventTemplate} from '@welshman/util'
|
||||
import {hash, own, Sign, ISigner, EncryptionImplementation} from '../util'
|
||||
|
||||
export type Extension = {
|
||||
sign: Sign
|
||||
export type Nip07 = {
|
||||
signEvent: Sign
|
||||
nip04: EncryptionImplementation
|
||||
nip44: EncryptionImplementation
|
||||
getPublicKey: () => string | undefined
|
||||
}
|
||||
|
||||
export class ExtensionSigner implements ISigner {
|
||||
export const getNip07 = () => (window as {nostr?: Nip07}).nostr
|
||||
|
||||
export class Nip07Signer implements ISigner {
|
||||
#lock = Promise.resolve()
|
||||
|
||||
#ext = () => (window as {nostr?: Extension}).nostr
|
||||
|
||||
#then = async <T>(f: (ext: Extension) => T | Promise<T>) => {
|
||||
#then = async <T>(f: (ext: Nip07) => T | Promise<T>) => {
|
||||
const promise = this.#lock.then(() => {
|
||||
const ext = this.#ext()
|
||||
const ext = getNip07()
|
||||
|
||||
if (!ext) throw new Error("Extension is not enabled")
|
||||
if (!ext) throw new Error("Nip07 is not enabled")
|
||||
|
||||
return f(ext)
|
||||
})
|
||||
@@ -28,19 +28,13 @@ export class ExtensionSigner implements ISigner {
|
||||
return promise
|
||||
}
|
||||
|
||||
isEnabled = () => Boolean(this.#ext())
|
||||
getPubkey = async () => getNip07()!.getPublicKey()!
|
||||
|
||||
getPubkey = () =>
|
||||
this.#then(ext => {
|
||||
const pubkey = ext.getPublicKey()
|
||||
sign = async (template: EventTemplate) => {
|
||||
const event = hash(own(await this.getPubkey(), template))
|
||||
|
||||
if (!pubkey) throw new Error("Failed to retrieve pubkey")
|
||||
|
||||
return pubkey as string
|
||||
})
|
||||
|
||||
sign = (event: EventTemplate) =>
|
||||
this.#then(ext => ext.sign(hash(own(ext.getPublicKey() as string, event))))
|
||||
return this.#then(ext => ext.signEvent(event))
|
||||
}
|
||||
|
||||
nip04 = {
|
||||
encrypt: (pubkey: string, message: string) =>
|
||||
@@ -4,41 +4,41 @@ 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} from '../util'
|
||||
import {SecretSigner} from './secret'
|
||||
import {Nip01Signer} from './nip01'
|
||||
|
||||
export type NostrConnectHandler = {
|
||||
pubkey: string
|
||||
export type Nip46Handler = {
|
||||
relays: string[]
|
||||
pubkey?: string
|
||||
domain?: string
|
||||
}
|
||||
|
||||
export type ConnectResponse = {
|
||||
export type Nip46Response = {
|
||||
id: string
|
||||
error?: string
|
||||
result?: string
|
||||
}
|
||||
|
||||
let singleton: NostrConnectBroker
|
||||
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 NostrConnectBroker extends Emitter {
|
||||
export class Nip46Broker extends Emitter {
|
||||
#sub: Subscription
|
||||
#signer: ISigner
|
||||
#ready = sleep(500)
|
||||
#closed = false
|
||||
#connectResult: any
|
||||
|
||||
static get(pubkey: string, secret: string, handler: NostrConnectHandler) {
|
||||
static get(pubkey: string, secret: string, handler: Nip46Handler) {
|
||||
if (
|
||||
singleton?.pubkey !== pubkey ||
|
||||
singleton?.secret !== secret ||
|
||||
!equals(singleton?.handler, handler)
|
||||
) {
|
||||
singleton?.teardown()
|
||||
singleton = new NostrConnectBroker(pubkey, secret, handler)
|
||||
singleton = new Nip46Broker(pubkey, secret, handler)
|
||||
}
|
||||
|
||||
return singleton
|
||||
@@ -47,11 +47,11 @@ export class NostrConnectBroker extends Emitter {
|
||||
constructor(
|
||||
readonly pubkey: string,
|
||||
readonly secret: string,
|
||||
readonly handler: NostrConnectHandler,
|
||||
readonly handler: Nip46Handler,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.#signer = new SecretSigner(secret)
|
||||
this.#signer = new Nip01Signer(secret)
|
||||
this.#sub = this.subscribe()
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class NostrConnectBroker extends Emitter {
|
||||
await this.#ready
|
||||
|
||||
const id = randomId()
|
||||
const pubkey = admin ? this.handler.pubkey : this.pubkey
|
||||
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 template = createEvent(NOSTR_CONNECT, {content, tags: [["p", pubkey]]})
|
||||
@@ -109,7 +109,7 @@ export class NostrConnectBroker extends Emitter {
|
||||
})
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.once(`res-${id}`, ({result, error}: ConnectResponse) => {
|
||||
this.once(`res-${id}`, ({result, error}: Nip46Response) => {
|
||||
if (error) {
|
||||
reject(error as string)
|
||||
} else {
|
||||
@@ -163,10 +163,8 @@ export class NostrConnectBroker extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectSigner implements ISigner {
|
||||
constructor(private broker: NostrConnectBroker) {}
|
||||
|
||||
isEnabled = () => true
|
||||
export class Nip46Signer implements ISigner {
|
||||
constructor(private broker: Nip46Broker) {}
|
||||
|
||||
getPubkey = async () => this.broker.pubkey
|
||||
|
||||
@@ -4,7 +4,7 @@ import {nip04 as nt04, nip44 as nt44, generateSecretKey, getPublicKey, getEventH
|
||||
import {cached} from '@welshman/lib'
|
||||
import {SignedEvent, HashedEvent, EventTemplate, OwnedEvent} from '@welshman/util'
|
||||
|
||||
export const getSecret = () => bytesToHex(generateSecretKey())
|
||||
export const makeSecret = () => bytesToHex(generateSecretKey())
|
||||
|
||||
export const getPubkey = (secret: string) => getPublicKey(hexToBytes(secret))
|
||||
|
||||
@@ -47,11 +47,10 @@ export type EncryptionImplementation = {
|
||||
}
|
||||
|
||||
export interface ISigner {
|
||||
isEnabled: () => boolean
|
||||
getPubkey: () => Promise<string>
|
||||
sign: Sign
|
||||
nip04: EncryptionImplementation
|
||||
nip44: EncryptionImplementation
|
||||
getPubkey: () => Promise<string>
|
||||
}
|
||||
|
||||
export const decrypt = async (signer: ISigner, pubkey: string, message: string) =>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import {UnwrappedEvent, SignedEvent, HashedEvent, EventTemplate, WRAP, SEAL} from '@welshman/util'
|
||||
import {own, hash, decrypt, ISigner} from './util'
|
||||
|
||||
// Wrapper
|
||||
|
||||
export type WrapperParams = {
|
||||
author?: string
|
||||
wrap?: {
|
||||
author: string
|
||||
recipient: string
|
||||
tags?: string[][]
|
||||
}
|
||||
}
|
||||
|
||||
export class Wrapper {
|
||||
seen = new Map<string, UnwrappedEvent | Error>()
|
||||
|
||||
constructor(readonly userSigner: ISigner, readonly wrapSigner: ISigner) {}
|
||||
|
||||
now = (drift = 0) =>
|
||||
Math.round(Date.now() / 1000 - Math.random() * Math.pow(10, drift))
|
||||
|
||||
getSeal = async (pk: string, rumor: HashedEvent) =>
|
||||
this.userSigner.sign(hash({
|
||||
kind: SEAL,
|
||||
pubkey: await this.userSigner.getPubkey(),
|
||||
content: await this.userSigner.nip44.encrypt(pk, JSON.stringify(rumor)),
|
||||
created_at: this.now(5),
|
||||
tags: [],
|
||||
}))
|
||||
|
||||
getWrap = async (pk: string, seal: SignedEvent) =>
|
||||
this.wrapSigner.sign(hash({
|
||||
kind: WRAP,
|
||||
pubkey: await this.wrapSigner.getPubkey(),
|
||||
content: await this.wrapSigner.nip44.encrypt(pk, JSON.stringify(seal)),
|
||||
created_at: this.now(5),
|
||||
tags: [["p", pk]],
|
||||
}))
|
||||
|
||||
wrap = async (pk: string, template: EventTemplate) => {
|
||||
const pubkey = await this.userSigner.getPubkey()
|
||||
const rumor = hash(own(pubkey, template))
|
||||
const seal = await this.getSeal(pk, rumor)
|
||||
const wrap = await this.getWrap(pk, seal)
|
||||
|
||||
return wrap
|
||||
}
|
||||
|
||||
unwrap = async (wrap: SignedEvent) => {
|
||||
// Avoid decrypting the same event multiple times
|
||||
if (this.seen.has(wrap.id)) {
|
||||
const rumorOrError = this.seen.get(wrap.id)
|
||||
|
||||
if (rumorOrError instanceof Error) {
|
||||
throw rumorOrError
|
||||
} else {
|
||||
return rumorOrError
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const seal = JSON.parse(await decrypt(this.wrapSigner, wrap.pubkey, wrap.content))
|
||||
const rumor = JSON.parse(await decrypt(this.wrapSigner, seal.pubkey, seal.content))
|
||||
|
||||
if (seal.pubkey !== rumor.pubkey) throw new Error("Seal pubkey does not match rumor pubkey")
|
||||
|
||||
this.seen.set(wrap.id, rumor)
|
||||
|
||||
return rumor
|
||||
} catch (error) {
|
||||
this.seen.set(wrap.id, error as Error)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
|
||||
Reference in New Issue
Block a user