rename client, update docs/skills
tests / tests (push) Failing after 5m4s

This commit is contained in:
2026-06-18 19:31:14 +00:00
parent dfeb7a747b
commit fe5c11b00f
92 changed files with 1811 additions and 5268 deletions
+55 -311
View File
@@ -1,338 +1,82 @@
import {Client, ClientOptions, PomadeSigner} from "@pomade/core"
import {derived, writable} from "svelte/store"
import {cached, randomId, append, omit, equals, assoc, TaskQueue} from "@welshman/lib"
import {withGetter} from "@welshman/store"
import {
Wallet,
WRAP,
getPubkeyTagValues,
StampedEvent,
SignedEvent,
getPubkey,
} from "@welshman/util"
import {
Nip59,
WrappedSigner,
Nip46Broker,
Nip46Signer,
Nip07Signer,
Nip01Signer,
Nip55Signer,
ISigner,
} from "@welshman/signer"
import {WrapManager} from "@welshman/net"
import {tracker, repository} from "./core.js"
import {Client as PomadeClient, PomadeSigner} from "@pomade/core"
import type {ClientOptions as PomadeClientOptions} from "@pomade/core"
import type {MaybeAsync} from "@welshman/lib"
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer, Nip55Signer} from "@welshman/signer"
import type {ISigner} from "@welshman/signer"
export enum SessionMethod {
Nip01 = "nip01",
Nip07 = "nip07",
Nip46 = "nip46",
Nip55 = "nip55",
Pomade = "pomade",
Pubkey = "pubkey",
Anonymous = "anonymous",
// ── Sessions: serializable {method, data} descriptors ──
export type Session<M extends string = string, D = unknown> = {
method: M
data: D
}
export type SessionNip01 = {
method: SessionMethod.Nip01
pubkey: string
secret: string
// ── Session handlers: a method string, its data shape, and how to build a signer ──
export type SessionHandler<M extends string, D> = {
method: M
getSigner: (data: D) => MaybeAsync<ISigner>
}
export type SessionNip07 = {
method: SessionMethod.Nip07
pubkey: string
}
/**
* Define a session handler. `M` and `D` are inferred from the arguments, so
* `getSigner` is type-checked against the data shape — and the same handler is
* used to build typed sessions (`toSession`) and to reconstruct signers.
*/
export const defineSessionHandler = <M extends string, D>(handler: SessionHandler<M, D>) => handler
export type SessionNip46 = {
method: SessionMethod.Nip46
pubkey: string
secret: string
handler: {
pubkey: string
relays: string[]
}
}
/** Build a typed, serializable session from a handler and its data. */
export const toSession = <M extends string, D>(
handler: SessionHandler<M, D>,
data: D,
): Session<M, D> => ({method: handler.method, data})
export type SessionNip55 = {
method: SessionMethod.Nip55
pubkey: string
signer: string
}
// ── Built-in handlers ──
export type SessionPomade = {
method: SessionMethod.Pomade
pubkey: string
clientOptions: ClientOptions
email: string
}
export type SessionPubkey = {
method: SessionMethod.Pubkey
pubkey: string
}
export type SessionAnonymous = {
method: SessionMethod.Anonymous
}
export type SessionAnyMethod =
| SessionNip01
| SessionNip07
| SessionNip46
| SessionNip55
| SessionPomade
| SessionPubkey
| SessionAnonymous
export type Session = SessionAnyMethod & {wallet?: Wallet} & Record<string, any>
export const pubkey = withGetter(writable<string | undefined>(undefined))
export const sessions = withGetter(writable<Record<string, Session>>({}))
export const session = withGetter(
derived([pubkey, sessions], ([$pubkey, $sessions]) => ($pubkey ? $sessions[$pubkey] : undefined)),
)
export const getSession = (pubkey: string) => sessions.get()[pubkey]
export const addSession = (session: Session) => {
sessions.update(assoc(session.pubkey, session))
pubkey.set(session.pubkey)
}
export const putSession = (session: Session) => {
if (!equals(getSession(session.pubkey), session)) {
sessions.update(assoc(session.pubkey, session))
}
}
export const updateSession = (pubkey: string, f: (session: Session) => Session) =>
putSession(f(getSession(pubkey)))
export const dropSession = (_pubkey: string) => {
getSigner.pop(getSession(_pubkey))?.cleanup?.()
pubkey.update($pubkey => ($pubkey === _pubkey ? undefined : $pubkey))
sessions.update($sessions => omit([_pubkey], $sessions))
}
export const clearSessions = () => {
for (const pubkey of Object.keys(sessions.get())) {
dropSession(pubkey)
}
}
// Session factories
export const makeNip01Session = (secret: string): SessionNip01 => ({
method: SessionMethod.Nip01,
secret,
pubkey: getPubkey(secret),
export const nip01 = defineSessionHandler({
method: "nip01",
getSigner: (data: {secret: string}) => new Nip01Signer(data.secret),
})
export const makeNip07Session = (pubkey: string): SessionNip07 => ({
method: SessionMethod.Nip07,
pubkey,
export const nip07 = defineSessionHandler({
method: "nip07",
getSigner: (_data: Record<string, never>) => new Nip07Signer(),
})
export const makeNip46Session = (
pubkey: string,
clientSecret: string,
signerPubkey: string,
relays: string[],
): SessionNip46 => ({
method: SessionMethod.Nip46,
pubkey,
secret: clientSecret,
handler: {pubkey: signerPubkey, relays},
export const nip46 = defineSessionHandler({
method: "nip46",
getSigner: (data: {clientSecret: string; signerPubkey: string; relays: string[]}) =>
new Nip46Signer(new Nip46Broker(data)),
})
export const makeNip55Session = (pubkey: string, signer: string): SessionNip55 => ({
method: SessionMethod.Nip55,
pubkey,
signer,
export const nip55 = defineSessionHandler({
method: "nip55",
getSigner: (data: {pubkey: string; signer: string}) => new Nip55Signer(data.signer, data.pubkey),
})
export const makePomadeSession = (
pubkey: string,
email: string,
clientOptions: ClientOptions,
): SessionPomade => ({
method: SessionMethod.Pomade,
pubkey,
clientOptions,
email,
export const pomade = defineSessionHandler({
method: "pomade",
getSigner: (data: {clientOptions: PomadeClientOptions; email: string}) =>
new PomadeSigner(new PomadeClient(data.clientOptions)),
})
export const makePubkeySession = (pubkey: string): SessionPubkey => ({
method: SessionMethod.Pubkey,
pubkey,
})
// ── Registry: deserialize a stored session back into a signer ──
// Type guards
export const sessionHandlers = new Map<string, SessionHandler<string, any>>()
export const isNip01Session = (session?: Session): session is SessionNip01 =>
session?.method === SessionMethod.Nip01
export const isNip07Session = (session?: Session): session is SessionNip07 =>
session?.method === SessionMethod.Nip07
export const isNip46Session = (session?: Session): session is SessionNip46 =>
session?.method === SessionMethod.Nip46
export const isNip55Session = (session?: Session): session is SessionNip55 =>
session?.method === SessionMethod.Nip55
export const isPomadeSession = (session?: Session): session is SessionPomade =>
session?.method === SessionMethod.Pomade
export const isPubkeySession = (session?: Session): session is SessionPubkey =>
session?.method === SessionMethod.Pubkey
// Login utilities
export const loginWithNip01 = (secret: string) => addSession(makeNip01Session(secret))
export const loginWithNip07 = (pubkey: string) => addSession(makeNip07Session(pubkey))
export const loginWithNip46 = (
pubkey: string,
clientSecret: string,
signerPubkey: string,
relays: string[],
) => addSession(makeNip46Session(pubkey, clientSecret, signerPubkey, relays))
export const loginWithNip55 = (pubkey: string, signer: string) =>
addSession(makeNip55Session(pubkey, signer))
export const loginWithPomade = (pubkey: string, email: string, clientOptions: ClientOptions) =>
addSession(makePomadeSession(pubkey, email, clientOptions))
export const loginWithPubkey = (pubkey: string) => addSession(makePubkeySession(pubkey))
// Other stuff
export const nip46Perms = "sign_event:22242,nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt"
export type SignerLogEntry = {
id: string
method: string
started_at: number
finished_at?: number
ok?: boolean
export const registerSessionHandler = (handler: SessionHandler<string, any>) => {
sessionHandlers.set(handler.method, handler)
}
export const signerLog = withGetter(writable<SignerLogEntry[]>([]))
export const wrapSigner = (signer: ISigner) =>
new WrappedSigner(signer, async <T>(method: string, thunk: () => Promise<T>) => {
const id = randomId()
signerLog.update(log => append({id, method, started_at: Date.now()}, log))
try {
const result = await thunk()
signerLog.update(log =>
log.map(x => (x.id === id ? {...x, finished_at: Date.now(), ok: true} : x)),
)
return result
} catch (error: any) {
signerLog.update(log =>
log.map(x => (x.id === id ? {...x, finished_at: Date.now(), ok: false} : 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 wrapSigner(new Nip07Signer())
if (isNip01Session(session)) return wrapSigner(new Nip01Signer(session.secret))
if (isNip55Session(session)) return wrapSigner(new Nip55Signer(session.signer, session.pubkey))
if (isPomadeSession(session))
return wrapSigner(new PomadeSigner(new Client(session.clientOptions)))
if (isNip46Session(session)) {
const {
secret: clientSecret,
handler: {relays, pubkey: signerPubkey},
} = session
const broker = new Nip46Broker({clientSecret, signerPubkey, relays})
const signer = new Nip46Signer(broker)
return wrapSigner(signer)
}
},
})
export const getSignerFromPubkey = (pubkey: string) => {
const session = getSession(pubkey)
if (session) {
return getSigner(session)
}
export const unregisterSessionHandler = (handler: SessionHandler<string, any>) => {
sessionHandlers.delete(handler.method)
}
export const signer = withGetter(derived(session, getSigner))
export const getSignerFromSession = (session: Session): MaybeAsync<ISigner> | undefined =>
sessionHandlers.get(session.method)?.getSigner(session.data)
export const sign = (event: StampedEvent) => signer.get()?.sign(event)
// ── Initialize default session handlers ──
export const nip44EncryptToSelf = (payload: string) => {
const $pubkey = pubkey.get()
const $signer = signer.get()
if (!$signer) {
throw new Error("Unable to encrypt to self without valid signer")
}
return $signer.nip44.encrypt($pubkey!, payload)
}
// Gift wrap utilities
export const wrapManager = new WrapManager({repository, tracker})
export const shouldUnwrap = withGetter(writable(false))
export const failedUnwraps = new Set<string>()
export const wrapQueue = new TaskQueue({
batchSize: 5,
batchDelay: 30,
processItem: async (wrap: SignedEvent) => {
for (const recipient of getPubkeyTagValues(wrap.tags)) {
const signer = getSignerFromPubkey(recipient)
if (signer) {
try {
const rumor = await Nip59.fromSigner(signer).unwrap(wrap)
wrapManager.add({wrap, rumor, recipient})
return rumor
} catch (e) {
failedUnwraps.add(wrap.id)
}
}
}
},
})
export const unwrapAndStore = async (wrap: SignedEvent) => {
if (wrap.kind !== WRAP) {
throw new Error("Tried to unwrap an invalid event")
}
if (!shouldUnwrap.get()) {
throw new Error("Discarding wrapped event because `shouldUnwrap` is not enabled")
}
if (!failedUnwraps.has(wrap.id) && !wrapManager.getRumor(wrap.id)) {
wrapQueue.push(wrap)
}
for (const sessionHandler of [nip01, nip07, nip46, nip55, pomade]) {
registerSessionHandler(sessionHandler)
}