103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
import {UnwrappedEvent, SignedEvent, HashedEvent, StampedEvent, WRAP, SEAL} from "@welshman/util"
|
|
import {own, hash, decrypt, ISigner} from "./util.js"
|
|
import {Nip01Signer} from "./signers/nip01.js"
|
|
|
|
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: StampedEvent) =>
|
|
hash(own(template, await signer.getPubkey()))
|
|
|
|
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 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: StampedEvent, tags: string[][] = []) =>
|
|
wrap(this.signer, this.wrapper || Nip01Signer.ephemeral(), pubkey, template, tags)
|
|
|
|
unwrap = (event: SignedEvent) => unwrap(this.signer, event)
|
|
}
|