Merge pull request #1308 from PR0M3TH3AN/codex/create-async-signer-api-module-and-adapters

Add signer interface and Nostr signer adapters
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-18 08:53:11 -05:00
commit 46fed1eb10
4 files changed
+439

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
// js/nostr/adapters/nip07Adapter.js
import { normalizeActorKey } from "../watchHistory.js";
function resolveNip44Module(extension) {
const nip44 = extension?.nip44 && typeof extension.nip44 === "object" ? extension.nip44 : null;
if (!nip44) {
return null;
}
const v2 = nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
if (v2?.encrypt || v2?.decrypt) {
return v2;
}
return nip44;
}
export async function createNip07Adapter(extension) {
const resolvedExtension = extension || (typeof window !== "undefined" ? window?.nostr : null);
if (!resolvedExtension) {
return {
type: "nip07",
pubkey: "",
metadata: async () => null,
relays: async () => null,
signEvent: async () => {
throw new Error("NIP-07 extension unavailable.");
},
requestPermissions: async () => ({ ok: false, error: new Error("extension-unavailable") }),
destroy: async () => {},
canSign: () => false,
};
}
const getPublicKey =
typeof resolvedExtension.getPublicKey === "function"
? resolvedExtension.getPublicKey.bind(resolvedExtension)
: null;
const pubkey = getPublicKey ? normalizeActorKey(await getPublicKey()) : "";
const nip04 =
resolvedExtension.nip04 && typeof resolvedExtension.nip04 === "object"
? resolvedExtension.nip04
: null;
const nip44 = resolveNip44Module(resolvedExtension);
const nip04Encrypt =
nip04 && typeof nip04.encrypt === "function" ? nip04.encrypt.bind(nip04) : null;
const nip04Decrypt =
nip04 && typeof nip04.decrypt === "function" ? nip04.decrypt.bind(nip04) : null;
const nip44Encrypt =
nip44 && typeof nip44.encrypt === "function" ? nip44.encrypt.bind(nip44) : null;
const nip44Decrypt =
nip44 && typeof nip44.decrypt === "function" ? nip44.decrypt.bind(nip44) : null;
const requestPermissions = async (methods = []) => {
if (typeof resolvedExtension.requestPermissions === "function") {
return resolvedExtension.requestPermissions({
permissions: Array.isArray(methods) ? methods : [],
});
}
if (typeof resolvedExtension.enable === "function") {
if (Array.isArray(methods) && methods.length) {
return resolvedExtension.enable({ permissions: methods.map((method) => ({ method })) });
}
return resolvedExtension.enable();
}
return { ok: false, error: new Error("permission-request-unavailable") };
};
const signer = {
type: "nip07",
pubkey,
metadata: async () =>
typeof resolvedExtension.getMetadata === "function"
? resolvedExtension.getMetadata()
: null,
relays: async () =>
typeof resolvedExtension.getRelays === "function"
? resolvedExtension.getRelays()
: null,
signEvent: async (event) => {
if (typeof resolvedExtension.signEvent !== "function") {
throw new Error("NIP-07 extension missing signEvent.");
}
return resolvedExtension.signEvent(event);
},
requestPermissions,
destroy: async () => {},
canSign: () => typeof resolvedExtension.signEvent === "function",
};
if (nip04Encrypt) {
signer.nip04Encrypt = nip04Encrypt;
}
if (nip04Decrypt) {
signer.nip04Decrypt = nip04Decrypt;
}
if (nip44Encrypt) {
signer.nip44Encrypt = nip44Encrypt;
}
if (nip44Decrypt) {
signer.nip44Decrypt = nip44Decrypt;
}
return signer;
}
+66
View File
@@ -0,0 +1,66 @@
// js/nostr/adapters/nip46Adapter.js
import { Nip46RpcClient } from "../nip46Client.js";
import { normalizeActorKey } from "../watchHistory.js";
function resolveClient(client) {
if (client instanceof Nip46RpcClient) {
return client;
}
return client?.nip46Client instanceof Nip46RpcClient ? client.nip46Client : null;
}
export async function createNip46Adapter(client) {
const resolvedClient = resolveClient(client);
if (!resolvedClient) {
return {
type: "nip46",
pubkey: "",
metadata: async () => null,
relays: async () => null,
signEvent: async () => {
throw new Error("NIP-46 client unavailable.");
},
requestPermissions: async () => ({ ok: false, error: new Error("client-unavailable") }),
destroy: async () => {},
canSign: () => false,
};
}
let pubkey = normalizeActorKey(resolvedClient.userPubkey);
if (!pubkey) {
pubkey = normalizeActorKey(await resolvedClient.getUserPubkey());
}
const signer = {
type: "nip46",
pubkey,
metadata: async () =>
resolvedClient.metadata && typeof resolvedClient.metadata === "object"
? { ...resolvedClient.metadata }
: null,
relays: async () => (Array.isArray(resolvedClient.relays) ? [...resolvedClient.relays] : null),
signEvent: async (event) => resolvedClient.signEvent(event),
requestPermissions: async () => ({ ok: true }),
destroy: async () => {
resolvedClient.destroy();
},
canSign: () =>
!resolvedClient.destroyed && typeof resolvedClient.signEvent === "function",
};
if (typeof resolvedClient.nip04Encrypt === "function") {
signer.nip04Encrypt = resolvedClient.nip04Encrypt.bind(resolvedClient);
}
if (typeof resolvedClient.nip04Decrypt === "function") {
signer.nip04Decrypt = resolvedClient.nip04Decrypt.bind(resolvedClient);
}
if (typeof resolvedClient.nip44Encrypt === "function") {
signer.nip44Encrypt = resolvedClient.nip44Encrypt.bind(resolvedClient);
}
if (typeof resolvedClient.nip44Decrypt === "function") {
signer.nip44Decrypt = resolvedClient.nip44Decrypt.bind(resolvedClient);
}
return signer;
}
+212
View File
@@ -0,0 +1,212 @@
// js/nostr/adapters/nsecAdapter.js
import { signEventWithPrivateKey } from "../publishHelpers.js";
import { ensureNostrTools, getCachedNostrTools } from "../toolkit.js";
import { normalizeActorKey } from "../watchHistory.js";
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
function normalizePrivateKey(privateKey) {
if (typeof privateKey !== "string") {
return "";
}
const trimmed = privateKey.trim();
if (!trimmed || !HEX64_REGEX.test(trimmed)) {
return "";
}
return trimmed.toLowerCase();
}
async function resolvePublicKey(privateKey, pubkey) {
const normalizedPubkey = normalizeActorKey(pubkey);
if (normalizedPubkey && HEX64_REGEX.test(normalizedPubkey)) {
return normalizedPubkey;
}
if (!privateKey) {
return "";
}
const tools = (await ensureNostrTools()) || getCachedNostrTools();
if (tools?.getPublicKey && typeof tools.getPublicKey === "function") {
const derived = tools.getPublicKey(privateKey);
const normalized = normalizeActorKey(derived);
if (normalized && HEX64_REGEX.test(normalized)) {
return normalized;
}
}
return "";
}
async function createPrivateKeyCipherClosures(privateKey) {
const normalizedPrivateKey = normalizePrivateKey(privateKey);
if (!normalizedPrivateKey) {
return {};
}
const tools = (await ensureNostrTools()) || getCachedNostrTools();
if (!tools) {
return {};
}
const closures = {};
const normalizeTargetPubkey = (candidate) => {
const normalized = normalizeActorKey(candidate);
if (!normalized || !HEX64_REGEX.test(normalized)) {
throw new Error("A hex-encoded pubkey is required for encryption.");
}
return normalized;
};
const resolveHexToBytes = () => {
if (typeof tools?.utils?.hexToBytes === "function") {
return (value) => tools.utils.hexToBytes(value);
}
return (value) => {
if (typeof value !== "string") {
throw new Error("Invalid hex input.");
}
const trimmed = value.trim();
if (!trimmed || trimmed.length % 2 !== 0) {
throw new Error("Invalid hex input.");
}
const bytes = new Uint8Array(trimmed.length / 2);
for (let index = 0; index < trimmed.length; index += 2) {
const byte = Number.parseInt(trimmed.slice(index, index + 2), 16);
if (Number.isNaN(byte)) {
throw new Error("Invalid hex input.");
}
bytes[index / 2] = byte;
}
return bytes;
};
};
if (
tools?.nip04?.encrypt &&
typeof tools.nip04.encrypt === "function" &&
tools?.nip04?.decrypt &&
typeof tools.nip04.decrypt === "function"
) {
closures.nip04Encrypt = async (targetPubkey, plaintext) =>
tools.nip04.encrypt(
normalizedPrivateKey,
normalizeTargetPubkey(targetPubkey),
plaintext,
);
closures.nip04Decrypt = async (targetPubkey, ciphertext) =>
tools.nip04.decrypt(
normalizedPrivateKey,
normalizeTargetPubkey(targetPubkey),
ciphertext,
);
}
const nip44 = tools?.nip44 || null;
let nip44Encrypt = null;
let nip44Decrypt = null;
let nip44GetConversationKey = null;
if (nip44?.v2 && typeof nip44.v2 === "object") {
if (typeof nip44.v2.encrypt === "function") {
nip44Encrypt = nip44.v2.encrypt;
}
if (typeof nip44.v2.decrypt === "function") {
nip44Decrypt = nip44.v2.decrypt;
}
if (typeof nip44.v2?.utils?.getConversationKey === "function") {
nip44GetConversationKey = nip44.v2.utils.getConversationKey;
}
}
if ((!nip44Encrypt || !nip44Decrypt) && nip44 && typeof nip44 === "object") {
if (typeof nip44.encrypt === "function") {
nip44Encrypt = nip44.encrypt;
}
if (typeof nip44.decrypt === "function") {
nip44Decrypt = nip44.decrypt;
}
if (!nip44GetConversationKey) {
if (typeof nip44.getConversationKey === "function") {
nip44GetConversationKey = nip44.getConversationKey;
} else if (typeof nip44.utils?.getConversationKey === "function") {
nip44GetConversationKey = nip44.utils.getConversationKey;
}
}
}
if (nip44Encrypt && nip44Decrypt && nip44GetConversationKey) {
const hexToBytes = resolveHexToBytes();
let cachedPrivateKeyBytes = null;
const getPrivateKeyBytes = () => {
if (!cachedPrivateKeyBytes) {
cachedPrivateKeyBytes = hexToBytes(normalizedPrivateKey);
}
return cachedPrivateKeyBytes;
};
const conversationKeyCache = new Map();
const ensureConversationKey = (targetPubkey) => {
const normalizedTarget = normalizeTargetPubkey(targetPubkey);
const cached = conversationKeyCache.get(normalizedTarget);
if (cached) {
return cached;
}
const privateKeyBytes = getPrivateKeyBytes();
const derived = nip44GetConversationKey(privateKeyBytes, normalizedTarget);
conversationKeyCache.set(normalizedTarget, derived);
return derived;
};
closures.nip44Encrypt = async (targetPubkey, plaintext) =>
nip44Encrypt(plaintext, ensureConversationKey(targetPubkey));
closures.nip44Decrypt = async (targetPubkey, ciphertext) =>
nip44Decrypt(ciphertext, ensureConversationKey(targetPubkey));
}
return closures;
}
export async function createNsecAdapter({ privateKey, pubkey } = {}) {
const normalizedPrivateKey = normalizePrivateKey(privateKey);
const resolvedPubkey = await resolvePublicKey(normalizedPrivateKey, pubkey);
const cipherClosures = await createPrivateKeyCipherClosures(normalizedPrivateKey);
const signer = {
type: "nsec",
pubkey: resolvedPubkey,
metadata: async () => null,
relays: async () => null,
signEvent: async (event) => {
if (!normalizedPrivateKey) {
throw new Error("Missing private key for signing.");
}
return signEventWithPrivateKey(event, normalizedPrivateKey);
},
requestPermissions: async () => ({ ok: true }),
destroy: async () => {},
canSign: () => Boolean(normalizedPrivateKey),
};
if (typeof cipherClosures.nip04Encrypt === "function") {
signer.nip04Encrypt = cipherClosures.nip04Encrypt;
}
if (typeof cipherClosures.nip04Decrypt === "function") {
signer.nip04Decrypt = cipherClosures.nip04Decrypt;
}
if (typeof cipherClosures.nip44Encrypt === "function") {
signer.nip44Encrypt = cipherClosures.nip44Encrypt;
}
if (typeof cipherClosures.nip44Decrypt === "function") {
signer.nip44Decrypt = cipherClosures.nip44Decrypt;
}
return signer;
}
+50
View File
@@ -0,0 +1,50 @@
// js/nostr/signer-interface.js
/**
* Canonical async signer interface used by bitvid.
*
* @typedef {Object} NostrSignerInterface
* @property {string} type
* Identifier for the signer implementation ("nsec", "nip07", "nip46", etc.).
* @property {string} pubkey
* Hex-encoded public key for the signer (lowercase).
* @property {() => Promise<object|null>} metadata
* Returns signer metadata (name, url, image, etc.) when available.
* @property {() => Promise<object|null>} relays
* Returns relay hints or preferences when available.
* @property {(event: object) => Promise<object>} signEvent
* Signs a Nostr event payload and returns the signed event.
* @property {(targetPubkey: string, plaintext: string) => Promise<string>} [nip04Encrypt]
* Optional NIP-04 encryption helper.
* @property {(targetPubkey: string, ciphertext: string) => Promise<string>} [nip04Decrypt]
* Optional NIP-04 decryption helper.
* @property {(targetPubkey: string, plaintext: string) => Promise<string>} [nip44Encrypt]
* Optional NIP-44 encryption helper.
* @property {(targetPubkey: string, ciphertext: string) => Promise<string>} [nip44Decrypt]
* Optional NIP-44 decryption helper.
* @property {(methods?: string[]) => Promise<unknown>} requestPermissions
* Requests signer permissions for the provided methods, when supported.
* @property {() => Promise<void>} destroy
* Cleans up any resources associated with the signer.
* @property {() => boolean} canSign
* Returns true when signEvent is available and ready.
*/
export const SIGNER_INTERFACE_FIELDS = Object.freeze({
required: Object.freeze([
"type",
"pubkey",
"metadata",
"relays",
"signEvent",
"requestPermissions",
"destroy",
"canSign",
]),
optional: Object.freeze([
"nip04Encrypt",
"nip04Decrypt",
"nip44Encrypt",
"nip44Decrypt",
]),
});