mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
243 lines
7.3 KiB
JavaScript
243 lines
7.3 KiB
JavaScript
// js/nostr/adapters/nip07Adapter.js
|
|
|
|
import { normalizeActorKey } from "../watchHistory.js";
|
|
import { runNip07WithRetry, NIP07_PRIORITY } from "../nip07Permissions.js";
|
|
|
|
async function retryNip07Call(operation, label, priority, options = {}) {
|
|
let lastError = null;
|
|
const attempts = 2; // Retry once on failure
|
|
const callOptions = { label, priority };
|
|
if (Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
|
|
callOptions.timeoutMs = options.timeoutMs;
|
|
}
|
|
if (Number.isFinite(options.retryMultiplier)) {
|
|
callOptions.retryMultiplier = options.retryMultiplier;
|
|
}
|
|
for (let i = 0; i < attempts; i++) {
|
|
try {
|
|
return await runNip07WithRetry(operation, callOptions);
|
|
} catch (error) {
|
|
lastError = error;
|
|
// Don't retry if user explicitly rejected
|
|
if (
|
|
error?.message?.toLowerCase().includes("denied") ||
|
|
error?.message?.toLowerCase().includes("rejected")
|
|
) {
|
|
throw error;
|
|
}
|
|
if (i < attempts - 1) {
|
|
const isChannelClosed = error?.message?.includes("message channel closed");
|
|
const delay = isChannelClosed ? 250 : 100;
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
}
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
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(initialExtension, { preloadedPubkey } = {}) {
|
|
const getExtension = () => {
|
|
if (typeof window !== "undefined" && window.nostr) {
|
|
return window.nostr;
|
|
}
|
|
return initialExtension || null;
|
|
};
|
|
|
|
// We use the initial extension state to determine which methods to expose.
|
|
// This matches standard behavior where capabilities are negotiated at login.
|
|
const bootstrapExtension = getExtension();
|
|
|
|
if (!bootstrapExtension) {
|
|
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,
|
|
capabilities: {
|
|
sign: false,
|
|
nip44: false,
|
|
nip04: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
const getPublicKey = async () => {
|
|
const ext = getExtension();
|
|
if (typeof ext?.getPublicKey !== "function") {
|
|
throw new Error("NIP-07 extension missing getPublicKey.");
|
|
}
|
|
return ext.getPublicKey();
|
|
};
|
|
|
|
const pubkey =
|
|
preloadedPubkey ||
|
|
(typeof bootstrapExtension.getPublicKey === "function"
|
|
? normalizeActorKey(await getPublicKey())
|
|
: "");
|
|
|
|
// Wrapper builders to ensure dynamic lookup at call time
|
|
const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => {
|
|
const ext = getExtension();
|
|
const nip04 = ext?.nip04;
|
|
if (!nip04 || typeof nip04.encrypt !== "function") {
|
|
throw new Error("NIP-04 encryption unavailable on active extension.");
|
|
}
|
|
return retryNip07Call(
|
|
() => nip04.encrypt(pubkey, plaintext),
|
|
"nip04.encrypt",
|
|
options?.priority,
|
|
);
|
|
};
|
|
|
|
const createNip04Decrypt = () => (pubkey, ciphertext, options = {}) => {
|
|
const ext = getExtension();
|
|
const nip04 = ext?.nip04;
|
|
if (!nip04 || typeof nip04.decrypt !== "function") {
|
|
throw new Error("NIP-04 decryption unavailable on active extension.");
|
|
}
|
|
return retryNip07Call(
|
|
() => nip04.decrypt(pubkey, ciphertext),
|
|
"nip04.decrypt",
|
|
options?.priority,
|
|
{ timeoutMs: options?.timeoutMs, retryMultiplier: options?.retryMultiplier },
|
|
);
|
|
};
|
|
|
|
const createNip44Encrypt = () => (pubkey, plaintext, options = {}) => {
|
|
const ext = getExtension();
|
|
const nip44 = resolveNip44Module(ext);
|
|
if (!nip44 || typeof nip44.encrypt !== "function") {
|
|
throw new Error("NIP-44 encryption unavailable on active extension.");
|
|
}
|
|
return retryNip07Call(
|
|
() => nip44.encrypt(pubkey, plaintext),
|
|
"nip44.encrypt",
|
|
options?.priority,
|
|
);
|
|
};
|
|
|
|
const createNip44Decrypt = () => (pubkey, ciphertext, options = {}) => {
|
|
const ext = getExtension();
|
|
const nip44 = resolveNip44Module(ext);
|
|
if (!nip44 || typeof nip44.decrypt !== "function") {
|
|
throw new Error("NIP-44 decryption unavailable on active extension.");
|
|
}
|
|
return retryNip07Call(
|
|
() => nip44.decrypt(pubkey, ciphertext),
|
|
"nip44.decrypt",
|
|
options?.priority,
|
|
{ timeoutMs: options?.timeoutMs, retryMultiplier: options?.retryMultiplier },
|
|
);
|
|
};
|
|
|
|
const requestPermissions = async (methods = []) => {
|
|
const ext = getExtension();
|
|
if (!ext) {
|
|
return { ok: false, error: new Error("extension-unavailable") };
|
|
}
|
|
|
|
if (typeof ext.requestPermissions === "function") {
|
|
return retryNip07Call(
|
|
() =>
|
|
ext.requestPermissions({
|
|
permissions: Array.isArray(methods) ? methods : [],
|
|
}),
|
|
"extension.requestPermissions",
|
|
);
|
|
}
|
|
|
|
if (typeof ext.enable === "function") {
|
|
if (Array.isArray(methods) && methods.length) {
|
|
return retryNip07Call(
|
|
() =>
|
|
ext.enable({
|
|
permissions: methods.map((method) => ({ method })),
|
|
}),
|
|
"extension.enable",
|
|
);
|
|
}
|
|
return retryNip07Call(() => ext.enable(), "extension.enable");
|
|
}
|
|
|
|
return { ok: false, error: new Error("permission-request-unavailable") };
|
|
};
|
|
|
|
const signer = {
|
|
type: "nip07",
|
|
pubkey,
|
|
metadata: async () => {
|
|
const ext = getExtension();
|
|
return typeof ext?.getMetadata === "function"
|
|
? retryNip07Call(() => ext.getMetadata(), "extension.getMetadata")
|
|
: null;
|
|
},
|
|
relays: async () => {
|
|
const ext = getExtension();
|
|
return typeof ext?.getRelays === "function"
|
|
? retryNip07Call(() => ext.getRelays(), "extension.getRelays")
|
|
: null;
|
|
},
|
|
signEvent: async (event) => {
|
|
const ext = getExtension();
|
|
if (typeof ext?.signEvent !== "function") {
|
|
throw new Error("NIP-07 extension missing signEvent.");
|
|
}
|
|
return retryNip07Call(
|
|
() => ext.signEvent(event),
|
|
"extension.signEvent",
|
|
NIP07_PRIORITY.HIGH,
|
|
);
|
|
},
|
|
requestPermissions,
|
|
destroy: async () => {},
|
|
canSign: () => typeof getExtension()?.signEvent === "function",
|
|
// Always expose these methods; they perform their own existence checks at runtime.
|
|
// This handles race conditions where NIP-04/44 APIs are injected lazily.
|
|
nip04Encrypt: createNip04Encrypt(),
|
|
nip04Decrypt: createNip04Decrypt(),
|
|
nip44Encrypt: createNip44Encrypt(),
|
|
nip44Decrypt: createNip44Decrypt(),
|
|
};
|
|
|
|
// Define capabilities as dynamic properties to reflect current extension state
|
|
Object.defineProperty(signer, "capabilities", {
|
|
get: () => {
|
|
const ext = getExtension();
|
|
const hasNip04 = !!(ext?.nip04 && typeof ext.nip04 === "object");
|
|
const hasNip44 = !!resolveNip44Module(ext);
|
|
return {
|
|
sign: typeof ext?.signEvent === "function",
|
|
nip44: hasNip44,
|
|
nip04: hasNip04,
|
|
};
|
|
},
|
|
enumerable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
return signer;
|
|
}
|