Add wrapped signer and signer log; fix adapter unsubscribing in app storage; add more details to thunk signing error, handle signing errors in auth

This commit is contained in:
Jon Staab
2025-07-29 11:11:40 -07:00
parent dc9a873d92
commit bd67f2763d
5 changed files with 101 additions and 15 deletions
+56 -5
View File
@@ -1,13 +1,15 @@
import {derived, writable} from "svelte/store"
import {cached, omit, equals, assoc} from "@welshman/lib"
import {cached, randomId, append, omit, equals, assoc} from "@welshman/lib"
import {withGetter} from "@welshman/store"
import {
WrappedSigner,
Nip46Broker,
Nip46Signer,
Nip07Signer,
Nip01Signer,
Nip55Signer,
getPubkey,
ISigner,
} from "@welshman/signer"
export enum SessionMethod {
@@ -175,13 +177,62 @@ export const loginWithPubkey = (pubkey: string) => addSession(makePubkeySession(
export const nip46Perms = "sign_event:22242,nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt"
export enum SignerLogEntryStatus {
Pending = "pending",
Success = "success",
Failure = "failure",
}
export type SignerLogEntry = {
id: string
method: string
status: SignerLogEntryStatus
duration: number
}
export const signerLog = withGetter(writable<SignerLogEntry[]>([]))
export const wrapSigner = (signer: ISigner) =>
new WrappedSigner(signer, async <T>(method: string, thunk: () => Promise<T>) => {
const id = randomId()
const now = Date.now()
signerLog.update(log =>
append({id, method, status: SignerLogEntryStatus.Pending, duration: 0}, log),
)
try {
const result = await thunk()
signerLog.update(log =>
log.map(x =>
x.id === id
? {...x, status: SignerLogEntryStatus.Success, duration: Date.now() - now}
: x,
),
)
return result
} catch (error: any) {
signerLog.update(log =>
log.map(x =>
x.id === id
? {...x, status: SignerLogEntryStatus.Failure, duration: Date.now() - now}
: x,
),
)
throw error
}
})
export const getSigner = cached({
maxSize: 100,
getKey: ([session]: [Session | undefined]) => `${session?.method}:${session?.pubkey}`,
getValue: ([session]: [Session | undefined]) => {
if (isNip07Session(session)) return new Nip07Signer()
if (isNip01Session(session)) return new Nip01Signer(session.secret)
if (isNip55Session(session)) return new Nip55Signer(session.signer)
if (isNip07Session(session)) return wrapSigner(new Nip07Signer())
if (isNip01Session(session)) return wrapSigner(new Nip01Signer(session.secret))
if (isNip55Session(session)) return wrapSigner(new Nip55Signer(session.signer))
if (isNip46Session(session)) {
const {
secret: clientSecret,
@@ -190,7 +241,7 @@ export const getSigner = cached({
const broker = new Nip46Broker({clientSecret, signerPubkey, relays})
const signer = new Nip46Signer(broker)
return signer
return wrapSigner(signer)
}
},
})
+8 -6
View File
@@ -22,7 +22,7 @@ export const ready = defer<void>()
export const dead = withGetter(writable(false))
export const subs: Unsubscriber[] = []
export const unsubscribers: Unsubscriber[] = []
export const getAll = async (name: string) => {
await ready
@@ -100,16 +100,18 @@ export const initStorage = async (
ready.resolve()
await Promise.all(Object.values(adapters).map(adapter => adapter.init()))
await Promise.all(
Object.values(adapters).map(async adapter => {
await adapter.init()
const unsubscribers = Object.values(adapters).map(adapter => adapter.sync())
return () => unsubscribers.forEach(call)
unsubscribers.push(adapter.sync())
}),
)
}
export const closeStorage = async () => {
dead.set(true)
subs.forEach(unsub => unsub())
unsubscribers.forEach(call)
await db?.close()
}
+1 -1
View File
@@ -108,7 +108,7 @@ export class Thunk {
try {
event = await signer.sign(event)
} catch (e: any) {
return this._fail(String(e.error || e))
return this._fail(`Failed to sign event: ${String(e.error || e)}`)
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import EventEmitter from "events"
import {on, poll, call} from "@welshman/lib"
import {on, poll, call, tryCatch} from "@welshman/lib"
import {SignedEvent, StampedEvent} from "@welshman/util"
import {makeRelayAuth} from "@welshman/util"
import {isRelayAuth, isClientAuth, isRelayOk, RelayMessage} from "./message.js"
@@ -101,7 +101,7 @@ export class AuthState extends EventEmitter {
this.setStatus(AuthStatus.PendingSignature)
const template = makeRelayAuth(this.socket.url, this.challenge)
const event = await sign(template)
const event = await tryCatch(() => sign(template))
if (event) {
this.request = event.id
+34 -1
View File
@@ -3,7 +3,7 @@ import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
import * as nt04 from "nostr-tools/nip04"
import * as nt44 from "nostr-tools/nip44"
import {generateSecretKey, getPublicKey, getEventHash} from "nostr-tools/pure"
import {cached, now} from "@welshman/lib"
import {Emitter, cached, now} from "@welshman/lib"
import {SignedEvent, HashedEvent, EventTemplate, StampedEvent, OwnedEvent} from "@welshman/util"
export const makeSecret = () => bytesToHex(generateSecretKey())
@@ -64,3 +64,36 @@ export const decrypt = async (signer: ISigner, pubkey: string, message: string)
nip04.detect(message)
? signer.nip04.decrypt(pubkey, message)
: signer.nip44.decrypt(pubkey, message)
export type SignerMethodWrapper = <T>(method: string, thunk: () => Promise<T>) => Promise<T>
export class WrappedSigner extends Emitter implements ISigner {
constructor(
private signer: ISigner,
private wrapMethod: SignerMethodWrapper,
) {
super()
}
sign(event: StampedEvent) {
return this.wrapMethod("sign", () => this.signer.sign(event))
}
getPubkey() {
return this.wrapMethod("getPubkey", () => this.signer.getPubkey())
}
nip04 = {
encrypt: async (pubkey: string, message: string) =>
this.wrapMethod("nip04.encrypt", () => this.signer.nip04.encrypt(pubkey, message)),
decrypt: async (pubkey: string, message: string) =>
this.wrapMethod("nip04.decrypt", () => this.signer.nip04.decrypt(pubkey, message)),
}
nip44 = {
encrypt: async (pubkey: string, message: string) =>
this.wrapMethod("nip44.encrypt", () => this.signer.nip44.encrypt(pubkey, message)),
decrypt: async (pubkey: string, message: string) =>
this.wrapMethod("nip44.decrypt", () => this.signer.nip44.decrypt(pubkey, message)),
}
}