Files
welshman/packages/signer/src/nip59.ts
T
2025-12-02 13:15:24 -08:00

111 lines
3.0 KiB
TypeScript

import {
isHashedEvent,
SignedEvent,
HashedEvent,
StampedEvent,
WRAP,
SEAL,
prep,
hash,
} from "@welshman/util"
import {decrypt, ISigner} from "./util.js"
import {Nip01Signer} from "./signers/nip01.js"
export const seen = new Map<string, HashedEvent | Error>()
export const now = (drift = 0) =>
Math.round(Date.now() / 1000 - Math.random() * Math.pow(10, drift))
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: StampedEvent,
tags: string[][] = [],
) => {
const author = await signer.getPubkey()
const rumor = await prep(template, author)
const seal = await getSeal(signer, pubkey, rumor)
const wrap = await getWrap(wrapper, pubkey, seal, tags)
return wrap
}
export const unwrap = async (signer: ISigner, wrap: SignedEvent): Promise<HashedEvent> => {
// 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")
if (!isHashedEvent(rumor)) throw new Error("Unwrapped object was not a hashed event")
seen.set(wrap.id, rumor)
return rumor
} 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: StampedEvent, tags: string[][] = []) =>
wrap(this.signer, this.wrapper || Nip01Signer.ephemeral(), pubkey, template, tags)
unwrap = (event: SignedEvent) => unwrap(this.signer, event)
}