Merge pull request #778 from PR0M3TH3AN/codex/update-userblocklistmanager-for-new-signer

Handle direct signer decryptors for user blocks
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-21 21:34:50 -04:00
commit 47b4be94d5
2 files changed
+226 -32

No files matched your search

+62 -31
View File
@@ -339,33 +339,53 @@ class UserBlockListManager {
});
};
const permissionResult = await requestDefaultExtensionPermissions();
if (!permissionResult.ok) {
userLogger.warn(
"[UserBlockList] Unable to load block list without extension permissions.",
permissionResult.error,
);
this.reset();
this.loaded = true;
const error =
permissionResult.error instanceof Error
? permissionResult.error
: new Error("Extension permissions required to load block list.");
emitStatus({ status: "error", error });
emitStatus({ status: "settled" });
return;
const activeSigner = getActiveSigner();
const signerDecryptor =
activeSigner && typeof activeSigner.nip04Decrypt === "function"
? activeSigner.nip04Decrypt
: null;
let extensionDecryptor =
typeof window?.nostr?.nip04?.decrypt === "function"
? (pubkey, payload) => window.nostr.nip04.decrypt(pubkey, payload)
: null;
if (!signerDecryptor && !extensionDecryptor) {
const permissionResult = await requestDefaultExtensionPermissions();
if (!permissionResult.ok) {
userLogger.warn(
"[UserBlockList] Unable to load block list via extension decryptor; permissions denied.",
permissionResult.error,
);
this.reset();
this.loaded = true;
const error =
permissionResult.error instanceof Error
? permissionResult.error
: new Error(
"Extension permissions are required to use the browser decryptor.",
);
emitStatus({ status: "error", error, decryptor: "extension" });
emitStatus({ status: "settled" });
return;
}
extensionDecryptor =
typeof window?.nostr?.nip04?.decrypt === "function"
? (pubkey, payload) => window.nostr.nip04.decrypt(pubkey, payload)
: null;
}
if (!window?.nostr?.nip04?.decrypt) {
if (!signerDecryptor && !extensionDecryptor) {
userLogger.warn(
"[UserBlockList] nip04.decrypt is unavailable; treating block list as empty."
"[UserBlockList] No nip04 decryptor available; treating block list as empty.",
);
this.reset();
this.loaded = true;
emitStatus({
status: "error",
error: new Error("nip04.decrypt is unavailable"),
});
const error = new Error(
"No NIP-04 decryptor is available to load the block list.",
);
emitStatus({ status: "error", error, decryptor: "unavailable" });
emitStatus({ status: "settled" });
return;
}
@@ -549,28 +569,39 @@ class UserBlockListManager {
return;
}
const activeSigner = getActiveSigner();
const decryptWithActiveSigner =
activeSigner && typeof activeSigner.nip04Decrypt === "function";
let decrypted = "";
const decryptPath = signerDecryptor
? "active-signer"
: extensionDecryptor
? "extension"
: "unavailable";
try {
if (decryptWithActiveSigner) {
decrypted = await activeSigner.nip04Decrypt(normalized, newest.content);
} else if (window?.nostr?.nip04?.decrypt) {
decrypted = await window.nostr.nip04.decrypt(normalized, newest.content);
if (signerDecryptor) {
decrypted = await signerDecryptor(normalized, newest.content);
} else if (extensionDecryptor) {
decrypted = await extensionDecryptor(normalized, newest.content);
} else {
throw new Error("nip04-unavailable");
}
} catch (err) {
userLogger.error("[UserBlockList] Failed to decrypt block list:", err);
userLogger.error(
`[UserBlockList] Failed to decrypt block list via ${decryptPath}:`,
err,
);
applyBlockedPubkeys([], {
source,
reason: "decrypt-error",
event: newest,
error: err,
decryptor: decryptPath,
});
emitStatus({
status: "error",
event: newest,
error: err,
source,
decryptor: decryptPath,
});
emitStatus({ status: "error", event: newest, error: err, source });
return;
}
+164 -1
View File
@@ -2,7 +2,12 @@ import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
const { userBlocks, USER_BLOCK_EVENTS } = await import("../js/userBlocks.js");
const { nostrClient } = await import("../js/nostr.js");
const {
nostrClient,
setActiveSigner,
clearActiveSigner,
getActiveSigner,
} = await import("../js/nostr.js");
if (typeof globalThis.window === "undefined") {
globalThis.window = {};
@@ -436,3 +441,161 @@ await (async () => {
localStorage.clear();
}
})();
await (async () => {
const actor = "4".repeat(64);
const target = "5".repeat(64);
const originalBlocked = new Set(userBlocks.blockedPubkeys);
const originalBlockEventId = userBlocks.blockEventId;
const originalBlockEventCreatedAt = userBlocks.blockEventCreatedAt;
const originalLastPublishedCreatedAt = userBlocks.lastPublishedCreatedAt;
const originalLoaded = userBlocks.loaded;
const originalPool = nostrClient.pool;
const originalRelays = Array.isArray(nostrClient.relays)
? [...nostrClient.relays]
: nostrClient.relays;
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
? [...nostrClient.writeRelays]
: nostrClient.writeRelays;
const originalEnsurePermissions = nostrClient.ensureExtensionPermissions;
const originalSignerState = getActiveSigner();
clearActiveSigner();
const originalNostr = window.nostr;
let permissionRequests = 0;
nostrClient.ensureExtensionPermissions = async () => {
permissionRequests += 1;
return { ok: true };
};
let eventCounter = 0;
let latestCiphertext = "cipher:[]";
let latestEventId = "event-initial";
let latestCreatedAt = 1_000;
const decryptPayloads = new Map([
[latestCiphertext, JSON.stringify({ blockedPubkeys: [] })],
]);
nostrClient.relays = ["wss://direct-signer.example"];
nostrClient.writeRelays = nostrClient.relays;
nostrClient.pool = {
publish: (_targets, event) => {
latestCiphertext = event.content;
latestEventId = event.id || `event-${Date.now()}`;
latestCreatedAt = event.created_at || Math.floor(Date.now() / 1000);
return {
on(eventName, handler) {
if (eventName === "ok") {
setImmediate(handler);
return true;
}
return false;
},
};
},
list: async () => [
{
id: latestEventId,
created_at: latestCreatedAt,
pubkey: actor,
content: latestCiphertext,
},
],
};
setActiveSigner({
type: "private-key",
pubkey: actor,
nip04Decrypt: async (pubkey, ciphertext) => {
assert.equal(pubkey, actor, "decrypt should target the actor pubkey");
if (!decryptPayloads.has(ciphertext)) {
throw new Error(`unexpected ciphertext ${ciphertext}`);
}
return decryptPayloads.get(ciphertext);
},
nip04Encrypt: async (pubkey, plaintext) => {
assert.equal(pubkey, actor, "encrypt should target the actor pubkey");
let parsed;
try {
parsed = JSON.parse(plaintext);
} catch (error) {
throw new Error(`unexpected payload ${plaintext}`);
}
const blocked = Array.isArray(parsed?.blockedPubkeys)
? [...parsed.blockedPubkeys].sort()
: [];
const cipher = `cipher:${JSON.stringify(blocked)}`;
decryptPayloads.set(cipher, plaintext);
return cipher;
},
signEvent: async (event) => {
eventCounter += 1;
return {
...event,
id: `event-${eventCounter}`,
created_at: event.created_at ?? Math.floor(Date.now() / 1000),
};
},
});
userBlocks.blockedPubkeys = new Set();
userBlocks.blockEventId = null;
userBlocks.blockEventCreatedAt = null;
userBlocks.lastPublishedCreatedAt = null;
userBlocks.loaded = false;
try {
window.nostr = undefined;
await userBlocks.loadBlocks(actor);
assert.deepEqual(
userBlocks.getBlockedPubkeys(),
[],
"direct signer should load block list without extension decryptor",
);
assert.equal(
permissionRequests,
0,
"loadBlocks should not request extension permissions when signer decryptor exists",
);
const blockResult = await userBlocks.addBlock(target, actor);
assert.equal(blockResult?.ok, true, "addBlock should succeed with direct signer");
assert(userBlocks.blockedPubkeys.has(target), "target should be in the block list");
const unblockResult = await userBlocks.removeBlock(target, actor);
assert.equal(
unblockResult?.ok,
true,
"removeBlock should succeed with direct signer",
);
assert.equal(
userBlocks.blockedPubkeys.has(target),
false,
"target should be removed from the block list",
);
assert.equal(
permissionRequests,
0,
"direct signer flows should never trigger extension permission prompts",
);
} finally {
userBlocks.blockedPubkeys = originalBlocked;
userBlocks.blockEventId = originalBlockEventId;
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
userBlocks.lastPublishedCreatedAt = originalLastPublishedCreatedAt;
userBlocks.loaded = originalLoaded;
nostrClient.pool = originalPool;
nostrClient.relays = originalRelays;
nostrClient.writeRelays = originalWriteRelays;
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
window.nostr = originalNostr;
clearActiveSigner();
if (originalSignerState) {
setActiveSigner(originalSignerState);
}
}
})();