mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 19:35:28 +00:00
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:
4 files changed
+439
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
]),
|
||||
});
|
||||
Reference in new issue
Block a user