mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 20:02:26 +00:00
Merge pull request #1379 from PR0M3TH3AN/codex/create-signingadapter-contract
Add SigningAdapter (NIP‑07 / dev / test) and wire DM composer to use it
This commit is contained in:
3 files changed
+207
-3
No files matched your search
@@ -0,0 +1,177 @@
|
||||
import { bytesToHex } from "../../vendor/crypto-helpers.bundle.min.js";
|
||||
import { signEventWithPrivateKey } from "../nostr/publishHelpers.js";
|
||||
import { ensureNostrTools, getCachedNostrTools } from "../nostr/toolkit.js";
|
||||
import { normalizeActorKey } from "../nostr/watchHistory.js";
|
||||
|
||||
const DEFAULT_TEST_PUBKEY = "f".repeat(64);
|
||||
const DEFAULT_DEV_DISPLAY_NAME = "Ephemeral dev signer";
|
||||
const DEFAULT_TEST_DISPLAY_NAME = "Test signer";
|
||||
|
||||
function normalizePubkey(value) {
|
||||
const normalized = typeof value === "string" ? normalizeActorKey(value) : "";
|
||||
return normalized || "";
|
||||
}
|
||||
|
||||
function getSigningTimestamp() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} SigningAdapter
|
||||
* @property {() => Promise<string>} getPubkey
|
||||
* @property {() => Promise<string>} getDisplayName
|
||||
* @property {(unsignedEvent: object) => Promise<object>} signEvent
|
||||
* @property {(message: string) => Promise<string>} signMessage
|
||||
*/
|
||||
|
||||
export function createNip07SigningAdapter({ extension } = {}) {
|
||||
const resolvedExtension =
|
||||
extension ||
|
||||
(typeof window !== "undefined" && window?.nostr ? window.nostr : null);
|
||||
|
||||
const getPubkey = async () => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.getPublicKey !== "function") {
|
||||
throw new Error("NIP-07 extension is unavailable.");
|
||||
}
|
||||
return normalizePubkey(await resolvedExtension.getPublicKey());
|
||||
};
|
||||
|
||||
const getDisplayName = async () => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.getMetadata !== "function") {
|
||||
return "";
|
||||
}
|
||||
const metadata = await resolvedExtension.getMetadata();
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return "";
|
||||
}
|
||||
const displayName =
|
||||
typeof metadata.display_name === "string" ? metadata.display_name.trim() : "";
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
return typeof metadata.name === "string" ? metadata.name.trim() : "";
|
||||
};
|
||||
|
||||
const signEvent = async (event) => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.signEvent !== "function") {
|
||||
throw new Error("NIP-07 extension is missing signEvent.");
|
||||
}
|
||||
return resolvedExtension.signEvent(event);
|
||||
};
|
||||
|
||||
const signMessage = async (message) => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.signMessage !== "function") {
|
||||
throw new Error("NIP-07 extension does not support signMessage.");
|
||||
}
|
||||
return resolvedExtension.signMessage(message);
|
||||
};
|
||||
|
||||
return {
|
||||
type: "nip07",
|
||||
getPubkey,
|
||||
getDisplayName,
|
||||
signEvent,
|
||||
signMessage,
|
||||
};
|
||||
}
|
||||
|
||||
export function createEphemeralDevSigningAdapter({
|
||||
displayName = DEFAULT_DEV_DISPLAY_NAME,
|
||||
privateKey,
|
||||
} = {}) {
|
||||
let cachedPrivateKey = typeof privateKey === "string" ? privateKey.trim() : "";
|
||||
let cachedPubkey = "";
|
||||
|
||||
const ensurePrivateKey = () => {
|
||||
if (cachedPrivateKey) {
|
||||
return cachedPrivateKey;
|
||||
}
|
||||
const random = new Uint8Array(32);
|
||||
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
||||
window.crypto.getRandomValues(random);
|
||||
} else if (typeof globalThis?.crypto?.getRandomValues === "function") {
|
||||
globalThis.crypto.getRandomValues(random);
|
||||
} else {
|
||||
for (let i = 0; i < random.length; i += 1) {
|
||||
random[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
}
|
||||
cachedPrivateKey = bytesToHex(random);
|
||||
return cachedPrivateKey;
|
||||
};
|
||||
|
||||
const ensurePubkey = async () => {
|
||||
if (cachedPubkey) {
|
||||
return cachedPubkey;
|
||||
}
|
||||
const tools = (await ensureNostrTools()) || getCachedNostrTools();
|
||||
if (!tools || typeof tools.getPublicKey !== "function") {
|
||||
throw new Error("Nostr tools are required to derive the dev pubkey.");
|
||||
}
|
||||
cachedPubkey = normalizePubkey(tools.getPublicKey(ensurePrivateKey()));
|
||||
if (!cachedPubkey) {
|
||||
throw new Error("Failed to derive dev pubkey.");
|
||||
}
|
||||
return cachedPubkey;
|
||||
};
|
||||
|
||||
const signEvent = async (event) =>
|
||||
signEventWithPrivateKey({ ...event, pubkey: await ensurePubkey() }, ensurePrivateKey());
|
||||
|
||||
const signMessage = async (message) => {
|
||||
const signed = signEventWithPrivateKey(
|
||||
{
|
||||
kind: 27235,
|
||||
created_at: getSigningTimestamp(),
|
||||
tags: [["u", "bitvid:signing-adapter"]],
|
||||
content: typeof message === "string" ? message : String(message ?? ""),
|
||||
pubkey: await ensurePubkey(),
|
||||
},
|
||||
ensurePrivateKey(),
|
||||
);
|
||||
if (!signed || typeof signed.sig !== "string") {
|
||||
throw new Error("Failed to sign message.");
|
||||
}
|
||||
return signed.sig;
|
||||
};
|
||||
|
||||
return {
|
||||
type: "dev",
|
||||
getPubkey: ensurePubkey,
|
||||
getDisplayName: async () => displayName,
|
||||
signEvent,
|
||||
signMessage,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestSigningAdapter({
|
||||
pubkey = DEFAULT_TEST_PUBKEY,
|
||||
displayName = DEFAULT_TEST_DISPLAY_NAME,
|
||||
signEvent,
|
||||
signMessage,
|
||||
} = {}) {
|
||||
const normalizedPubkey = normalizePubkey(pubkey) || DEFAULT_TEST_PUBKEY;
|
||||
|
||||
const signEventImpl =
|
||||
typeof signEvent === "function"
|
||||
? signEvent
|
||||
: async (event) => ({
|
||||
...event,
|
||||
pubkey: normalizedPubkey || event.pubkey,
|
||||
id: "test-event-id",
|
||||
sig: "test-event-sig",
|
||||
});
|
||||
|
||||
const signMessageImpl =
|
||||
typeof signMessage === "function"
|
||||
? signMessage
|
||||
: async (message) => `signed:${message}`;
|
||||
|
||||
return {
|
||||
type: "test",
|
||||
getPubkey: async () => normalizedPubkey,
|
||||
getDisplayName: async () => displayName,
|
||||
signEvent: signEventImpl,
|
||||
signMessage: signMessageImpl,
|
||||
};
|
||||
}
|
||||
@@ -77,6 +77,7 @@ export class AppShell {
|
||||
threadErrorType = "",
|
||||
composerState = "idle",
|
||||
notifications = [],
|
||||
signingAdapter = null,
|
||||
onSelectConversation,
|
||||
onSendMessage,
|
||||
} = {}) {
|
||||
@@ -125,6 +126,7 @@ export class AppShell {
|
||||
Composer({
|
||||
document: doc,
|
||||
state: composerState,
|
||||
signingAdapter,
|
||||
onSend: onSendMessage,
|
||||
}),
|
||||
);
|
||||
|
||||
+28
-3
@@ -14,6 +14,7 @@ export function Composer({
|
||||
placeholder = "Write a message…",
|
||||
state = "idle",
|
||||
privacyMode: initialPrivacyMode = "standard",
|
||||
signingAdapter = null,
|
||||
onSend,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
@@ -88,11 +89,35 @@ export function Composer({
|
||||
form.appendChild(actions);
|
||||
form.appendChild(status);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (typeof onSend !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { privacyMode, attachments: [] };
|
||||
if (signingAdapter) {
|
||||
try {
|
||||
if (typeof signingAdapter.getPubkey === "function") {
|
||||
payload.pubkey = await signingAdapter.getPubkey();
|
||||
}
|
||||
if (typeof signingAdapter.getDisplayName === "function") {
|
||||
payload.displayName = await signingAdapter.getDisplayName();
|
||||
}
|
||||
if (typeof signingAdapter.signMessage === "function") {
|
||||
payload.signature = await signingAdapter.signMessage(textarea.value);
|
||||
}
|
||||
} catch (error) {
|
||||
payload.signingError =
|
||||
error instanceof Error ? error.message : "Signing failed.";
|
||||
}
|
||||
}
|
||||
|
||||
onSend(textarea.value, payload);
|
||||
};
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
if (typeof onSend === "function") {
|
||||
onSend(textarea.value, { privacyMode, attachments: [] });
|
||||
}
|
||||
void handleSubmit();
|
||||
});
|
||||
|
||||
const updatePrivacyLabel = () => {
|
||||
|
||||
Reference in new issue
Block a user