164 lines
4.5 KiB
TypeScript
164 lines
4.5 KiB
TypeScript
import { type Event as NostrEvent } from "nostr-tools";
|
|
import NDK, {
|
|
NDKEvent,
|
|
NDKPrivateKeySigner,
|
|
type NDKTag,
|
|
type NDKFilter,
|
|
type NDKSigner,
|
|
type NDKUserProfile,
|
|
} from "@nostr-dev-kit/ndk";
|
|
import { getHashedKeyName } from "@/lib/nostr";
|
|
import { z } from "zod";
|
|
import { log } from "@/lib/utils";
|
|
|
|
const SignerSchema = z.object({
|
|
key: z.string(),
|
|
});
|
|
|
|
interface IFindEphemeralSignerLookups {
|
|
name?: string;
|
|
associatedEventNip19?: string;
|
|
}
|
|
|
|
/**
|
|
* Finds a named ephemeral signer from a self-DM.
|
|
*/
|
|
export async function findEphemeralSigner(
|
|
ndk: NDK,
|
|
mainSigner: NDKSigner,
|
|
opts: IFindEphemeralSignerLookups,
|
|
): Promise<NDKPrivateKeySigner | undefined> {
|
|
log("func", "findEphemeralSigner");
|
|
const filter: NDKFilter = { kinds: [2600 as number] };
|
|
|
|
if (opts.name) {
|
|
const hashedName = await getHashedKeyName(opts.name);
|
|
filter["#e"] = [hashedName];
|
|
} else if (opts.associatedEventNip19) {
|
|
const hashedEventReference = await getHashedKeyName(
|
|
opts.associatedEventNip19,
|
|
);
|
|
filter["#e"] = [hashedEventReference];
|
|
}
|
|
log("info", "filter", filter.toString());
|
|
const event = await ndk.fetchEvent(filter);
|
|
if (event) {
|
|
const decryptEventFunction = async (event: NDKEvent) => {
|
|
await event.decrypt(await mainSigner.user());
|
|
const content = SignerSchema.parse(JSON.parse(event.content));
|
|
return new NDKPrivateKeySigner(content.key as string);
|
|
};
|
|
|
|
const promise = new Promise<NDKPrivateKeySigner>((resolve, reject) => {
|
|
let decryptionAttempts = 0;
|
|
try {
|
|
decryptionAttempts++;
|
|
resolve(decryptEventFunction(event));
|
|
} catch (e) {
|
|
if (decryptionAttempts > 5) {
|
|
console.error(
|
|
`Failed to decrypt ephemeral signer event after ${decryptionAttempts} attempts.`,
|
|
);
|
|
reject(e);
|
|
return;
|
|
}
|
|
setTimeout(() => {
|
|
decryptEventFunction(event);
|
|
}, 1000 * Math.random());
|
|
}
|
|
});
|
|
|
|
return promise;
|
|
}
|
|
}
|
|
|
|
interface ISaveOpts {
|
|
associatedEvent?: NDKEvent;
|
|
name?: string;
|
|
metadata?: object;
|
|
keyProfile?: NDKUserProfile;
|
|
mainSigner?: NDKSigner;
|
|
}
|
|
type EphemeralKeyEventContent = {
|
|
key: string;
|
|
event?: string;
|
|
version: string;
|
|
metadata?: object;
|
|
};
|
|
function generateContent(
|
|
targetSigner: NDKPrivateKeySigner,
|
|
opts: ISaveOpts = {},
|
|
) {
|
|
const content: EphemeralKeyEventContent = {
|
|
key: targetSigner.privateKey!,
|
|
version: "v1",
|
|
...opts.metadata,
|
|
};
|
|
|
|
if (opts.associatedEvent) content.event = opts.associatedEvent.encode();
|
|
|
|
return JSON.stringify(content);
|
|
}
|
|
async function generateTags(mainSigner: NDKSigner, opts: ISaveOpts = {}) {
|
|
log("func", "generateTags", opts.toString());
|
|
const mainUser = await mainSigner.user();
|
|
const tags = [
|
|
["p", mainUser.pubkey],
|
|
["client", "flockstr"],
|
|
];
|
|
if (opts.associatedEvent) {
|
|
const encodedEvent = opts.associatedEvent.encode();
|
|
log("info", "encodedEvent", encodedEvent.toString());
|
|
// TODO: This is trivially reversable; better to encrypt it or hash it with the pubkey
|
|
const hashedEventReference = await getHashedKeyName(encodedEvent);
|
|
log("info", "hashedEventReference", hashedEventReference.toString());
|
|
tags.push(["e", hashedEventReference]);
|
|
}
|
|
|
|
if (opts.name) {
|
|
const hashedName = await getHashedKeyName(opts.name);
|
|
tags.push(["e", hashedName]);
|
|
}
|
|
|
|
return tags;
|
|
}
|
|
|
|
export async function saveEphemeralSigner(
|
|
ndk: NDK,
|
|
targetSigner: NDKPrivateKeySigner,
|
|
opts: ISaveOpts = {},
|
|
) {
|
|
log("func", "saveEphemeralSigner");
|
|
// Determine current user signer
|
|
const mainSigner = opts.mainSigner || ndk.signer;
|
|
|
|
if (!mainSigner) throw new Error("No main signer provided");
|
|
|
|
const mainUser = await mainSigner.user();
|
|
|
|
// Create 2600 kind which saves encrypted JSON of the ephemeral signer's private key, the associated list, and other metadata
|
|
const event = new NDKEvent(ndk, {
|
|
kind: 2600,
|
|
content: generateContent(targetSigner, opts),
|
|
tags: await generateTags(mainSigner, opts),
|
|
} as NostrEvent);
|
|
event.pubkey = mainUser.pubkey;
|
|
await event.encrypt(mainUser, mainSigner);
|
|
await event.publish();
|
|
|
|
// Update Ephemeral signers metadata
|
|
log("info", "Checking keyProfile", opts.keyProfile?.toString());
|
|
const user = await targetSigner.user();
|
|
if (opts.keyProfile) {
|
|
const event = new NDKEvent(ndk, {
|
|
kind: 0,
|
|
content: JSON.stringify(opts.keyProfile),
|
|
tags: [] as NDKTag[],
|
|
} as NostrEvent);
|
|
event.pubkey = user.pubkey;
|
|
await event.sign(targetSigner);
|
|
await event.publish();
|
|
}
|
|
return user;
|
|
}
|