Merge pull request #1897 from PR0M3TH3AN/codex/remove-nip04-and-nip44-from-default-permissions

Limit default NIP-07 login perms; request NIP-04/NIP-44 on-demand with decrypt UI
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-28 14:47:20 -05:00
commit 7583b7791f
8 files changed
+437 -12

No files matched your search

+115 -1
View File
@@ -201,6 +201,7 @@ import {
clearActiveSigner as clearActiveSignerInRegistry,
logoutSigner as logoutSignerFromRegistry,
resolveActiveSigner as resolveActiveSignerFromRegistry,
requestDefaultExtensionPermissions,
} from "../nostrClientRegistry.js";
import { createNip07Adapter } from "./adapters/nip07Adapter.js";
import { createNsecAdapter } from "./adapters/nsecAdapter.js";
@@ -1016,6 +1017,63 @@ function buildDmFilters(actorPubkey, { since, until, limit } = {}) {
return filters;
}
function normalizeDmEncryptionHint(value) {
if (typeof value !== "string") {
return "";
}
return value.trim().toLowerCase().replace(/[^a-z0-9_.-]/g, "");
}
function resolveDmDecryptPermissionMethods(event) {
if (!event || typeof event !== "object") {
return [];
}
const kind = Number.isFinite(event.kind) ? event.kind : null;
if (kind === 1059) {
return ["nip44.decrypt", "nip44.v2.decrypt"];
}
const tags = Array.isArray(event.tags) ? event.tags : [];
let requiresNip44 = false;
let requiresNip04 = false;
for (const tag of tags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
const label = typeof tag[0] === "string" ? tag[0].trim().toLowerCase() : "";
if (label !== "encrypted" && label !== "encryption") {
continue;
}
for (let index = 1; index < tag.length; index += 1) {
const normalized = normalizeDmEncryptionHint(tag[index]);
if (!normalized) {
continue;
}
if (normalized.includes("nip44")) {
requiresNip44 = true;
}
if (normalized.includes("nip04")) {
requiresNip04 = true;
}
}
}
if (!requiresNip04 && !requiresNip44 && kind === 4) {
requiresNip04 = true;
}
const methods = [];
if (requiresNip04) {
methods.push("nip04.decrypt");
}
if (requiresNip44) {
methods.push("nip44.decrypt", "nip44.v2.decrypt");
}
return methods;
}
const EXTENSION_MIME_MAP = Object.freeze(
Object.fromEntries(
@@ -4650,7 +4708,10 @@ export class NostrClient {
return { actorPubkey: normalizedActor, decryptors };
}
async decryptDirectMessageEvent(event, { actorPubkey } = {}) {
async decryptDirectMessageEvent(
event,
{ actorPubkey, onPermissionRequest } = {},
) {
const eventId = typeof event?.id === "string" ? event.id : "";
if (eventId) {
const cached = this.dmDecryptCache.get(eventId);
@@ -4661,6 +4722,50 @@ export class NostrClient {
const decryptDM = await this.ensureDmDecryptor();
const context = await this.buildDmDecryptContext(actorPubkey);
const permissionMethods = resolveDmDecryptPermissionMethods(event);
const needsExtensionPermission =
permissionMethods.length &&
typeof window !== "undefined" &&
window?.nostr &&
!this.hasRequiredExtensionPermissions(permissionMethods) &&
context.decryptors.some(
(candidate) => candidate?.source === "extension",
);
if (needsExtensionPermission) {
if (typeof onPermissionRequest === "function") {
onPermissionRequest({
status: "decrypting",
methods: permissionMethods,
eventId,
});
}
const permissionResult =
await requestDefaultExtensionPermissions(permissionMethods);
if (!permissionResult?.ok) {
const error = new Error("extension-permission-denied");
error.code = "extension-permission-denied";
error.cause = permissionResult?.error || null;
if (typeof onPermissionRequest === "function") {
onPermissionRequest({
status: "error",
methods: permissionMethods,
eventId,
error,
});
}
return { ok: false, error };
}
if (typeof onPermissionRequest === "function") {
onPermissionRequest({
status: "ready",
methods: permissionMethods,
eventId,
});
}
}
let result;
try {
@@ -4694,6 +4799,10 @@ export class NostrClient {
Number.isFinite(options?.decryptLimit) && options.decryptLimit > 0
? Math.floor(options.decryptLimit)
: null;
const onPermissionRequest =
typeof options?.onPermissionRequest === "function"
? options.onPermissionRequest
: null;
const relayCandidates = Array.isArray(options.relays)
? options.relays
@@ -4746,6 +4855,7 @@ export class NostrClient {
try {
const decrypted = await this.decryptDirectMessageEvent(event, {
actorPubkey: context.actorPubkey || actorPubkeyInput,
onPermissionRequest,
});
if (decrypted?.ok) {
messages.push(decrypted);
@@ -4830,6 +4940,10 @@ export class NostrClient {
const result = await this.decryptDirectMessageEvent(event, {
actorPubkey: context.actorPubkey || actorPubkeyInput,
onPermissionRequest:
typeof options?.onPermissionRequest === "function"
? options.onPermissionRequest
: null,
});
if (result?.ok) {
+1 -2
View File
@@ -22,12 +22,11 @@ export const DEFAULT_NIP07_ENCRYPTION_METHODS = Object.freeze([
]);
export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
// Core auth + relay metadata
// Core auth + relay metadata (encryption permissions requested on demand)
"get_public_key",
"sign_event",
"read_relays",
"write_relays",
...DEFAULT_NIP07_ENCRYPTION_METHODS,
]);
export const NIP07_PRIORITY = Object.freeze({
+17 -2
View File
@@ -717,7 +717,14 @@ class HashtagPreferencesService {
(!signerHasNip04 && requiresNip04)
) {
try {
await requestDefaultExtensionPermissions();
const permissionMethods = [];
if (requiresNip04) {
permissionMethods.push("nip04.decrypt");
}
if (requiresNip44) {
permissionMethods.push("nip44.decrypt", "nip44.v2.decrypt");
}
await requestDefaultExtensionPermissions(permissionMethods);
} catch (error) {
userLogger.warn(
`${LOG_PREFIX} Extension permissions request failed while loading preferences`,
@@ -920,7 +927,15 @@ class HashtagPreferencesService {
}
if (signer.type === "extension") {
const permissionResult = await requestDefaultExtensionPermissions();
const permissionMethods = ["sign_event"];
if (typeof signer.nip04Encrypt === "function") {
permissionMethods.push("nip04.encrypt");
}
if (typeof signer.nip44Encrypt === "function") {
permissionMethods.push("nip44.encrypt", "nip44.v2.encrypt");
}
const permissionResult =
await requestDefaultExtensionPermissions(permissionMethods);
if (!permissionResult?.ok) {
const error = new Error(
"The active signer must allow encryption and signing before publishing preferences.",
+36
View File
@@ -958,6 +958,24 @@ export class NostrService {
...options,
limit: resolvedLimit,
decryptLimit: resolvedDecryptLimit,
onPermissionRequest: (detail = {}) => {
const status = detail?.status;
if (status === "decrypting") {
this.emit("directMessages:status", {
status,
message: "Decrypting direct messages…",
methods: detail?.methods || [],
eventId: detail?.eventId || null,
});
} else if (status === "ready") {
this.emit("directMessages:status", { status: "ready" });
} else if (status === "error") {
this.emit("directMessages:status", {
status: "error",
error: detail?.error || null,
});
}
},
onMessage: (message) => {
this.applyDirectMessage(message, { reason: "load-incremental" });
},
@@ -1077,6 +1095,24 @@ export class NostrService {
{
relays: resolvedRelays,
onEvent: handlers.onEvent,
onPermissionRequest: (detail = {}) => {
const status = detail?.status;
if (status === "decrypting") {
this.emit("directMessages:status", {
status,
message: "Decrypting direct messages…",
methods: detail?.methods || [],
eventId: detail?.eventId || null,
});
} else if (status === "ready") {
this.emit("directMessages:status", { status: "ready" });
} else if (status === "error") {
this.emit("directMessages:status", {
status: "error",
error: detail?.error || null,
});
}
},
onMessage: (message, context = {}) => {
this.applyDirectMessage(message, {
reason: "subscription",
+49 -4
View File
@@ -357,6 +357,28 @@ class SubscriptionsManager {
});
}
emitStatus(detail = {}) {
this.emitter.emit("status", detail);
if (detail?.status !== "loading") {
return;
}
const message =
typeof detail?.message === "string" && detail.message.trim()
? detail.message.trim()
: "Decrypting subscriptions…";
const container =
this.lastContainerId &&
typeof document !== "undefined" &&
document.getElementById
? document.getElementById(this.lastContainerId)
: null;
if (container) {
container.innerHTML = getSidebarLoadingMarkup(message);
}
}
/**
* Decrypt the subscription list from kind=30000 (d="subscriptions").
*/
@@ -751,9 +773,21 @@ class SubscriptionsManager {
(!signerHasNip04 && !windowHasNip04 && requiresNip04)
) {
try {
const hasCachedPermissions = hasDefaultExtensionPermissions();
const permissionMethods = [];
if (requiresNip04) {
permissionMethods.push("nip04.decrypt");
}
if (requiresNip44) {
permissionMethods.push("nip44.decrypt", "nip44.v2.decrypt");
}
const hasCachedPermissions = hasDefaultExtensionPermissions(permissionMethods);
if (!hasCachedPermissions) {
const permissionResult = await requestDefaultExtensionPermissions();
this.emitStatus({
status: "loading",
message: "Decrypting subscriptions…",
});
const permissionResult =
await requestDefaultExtensionPermissions(permissionMethods);
if (!permissionResult?.ok) {
const error =
permissionResult?.error instanceof Error
@@ -769,6 +803,8 @@ class SubscriptionsManager {
const wrapped = error instanceof Error ? error : new Error(String(error));
wrapped.code = "nostr-permission-error";
return { ok: false, error: wrapped };
} finally {
this.emitStatus({ status: "idle" });
}
}
@@ -983,9 +1019,18 @@ class SubscriptionsManager {
}
if (signer.type === "extension") {
const hasCachedPermissions = hasDefaultExtensionPermissions();
const permissionMethods = ["sign_event"];
if (typeof signer.nip04Encrypt === "function") {
permissionMethods.push("nip04.encrypt");
}
if (typeof signer.nip44Encrypt === "function") {
permissionMethods.push("nip44.encrypt", "nip44.v2.encrypt");
}
const hasCachedPermissions =
hasDefaultExtensionPermissions(permissionMethods);
if (!hasCachedPermissions) {
const permissionResult = await requestDefaultExtensionPermissions();
const permissionResult =
await requestDefaultExtensionPermissions(permissionMethods);
if (!permissionResult.ok) {
userLogger.warn(
"[SubscriptionsManager] Signer permissions denied while updating subscriptions.",
+2 -1
View File
@@ -940,7 +940,8 @@ export class UploadModal {
try {
if (signer?.type === "extension") {
const permissionResult = await requestDefaultExtensionPermissions();
const permissionResult =
await requestDefaultExtensionPermissions(["nip04.decrypt"]);
if (!permissionResult?.ok) {
alert("Extension permissions are required to unlock storage.");
return;
+192
View File
@@ -1270,6 +1270,8 @@ export class ProfileModalController {
this.blockListEmpty = null;
this.blockListStatus = null;
this.blockListLoadingState = "idle";
this.subscriptionsStatus = null;
this.subscriptionsLoadingState = "idle";
this.blockInput = null;
this.addBlockedButton = null;
this.profileBlockedList = null;
@@ -1415,6 +1417,9 @@ export class ProfileModalController {
this.subscriptionsService.on("change", () => {
void this.populateSubscriptionsList();
});
this.subscriptionsService.on("status", (detail) => {
this.handleSubscriptionsStatus(detail);
});
}
if (
@@ -1424,6 +1429,9 @@ export class ProfileModalController {
this.services.userBlocks.on("change", () => {
this.populateBlockedList();
});
this.services.userBlocks.on("status", (detail) => {
this.handleBlockListStatus(detail);
});
}
}
@@ -1953,6 +1961,9 @@ export class ProfileModalController {
subscribe("directMessages:updated", (detail) => {
this.handleDirectMessagesUpdated(detail);
});
subscribe("directMessages:status", (detail) => {
this.handleDirectMessagesStatus(detail);
});
subscribe("directMessages:cleared", () => {
this.handleDirectMessagesCleared();
});
@@ -5018,6 +5029,38 @@ export class ProfileModalController {
});
}
handleDirectMessagesStatus(detail = {}) {
const status = typeof detail?.status === "string" ? detail.status : "";
if (status === "decrypting") {
this.setMessagesLoadingState("loading", {
message:
typeof detail?.message === "string" && detail.message.trim()
? detail.message.trim()
: "Decrypting direct messages…",
});
return;
}
if (status === "error") {
this.setMessagesLoadingState("error", {
message:
"Unable to decrypt direct messages. Check your extension permissions.",
});
return;
}
if (status === "ready" && this.messagesLoadingState === "loading") {
const actor = this.resolveActiveDmActor();
if (!actor) {
this.setMessagesLoadingState("unauthenticated");
} else if (!this.directMessagesCache.length) {
this.setMessagesLoadingState("empty");
} else {
this.setMessagesLoadingState("ready");
}
}
}
handleDirectMessagesCleared() {
if (this.activeMessagesRequest) {
return;
@@ -7393,6 +7436,144 @@ export class ProfileModalController {
}
}
handleBlockListStatus(detail = {}) {
const status = typeof detail?.status === "string" ? detail.status : "";
const message =
typeof detail?.message === "string" && detail.message.trim()
? detail.message.trim()
: "";
if (status === "loading") {
this.setBlockListLoadingState("loading", { message });
return;
}
if (status === "error") {
this.setBlockListLoadingState("error", { message });
return;
}
if (status) {
this.setBlockListLoadingState("idle");
}
}
ensureSubscriptionsStatusElement() {
if (this.subscriptionsStatus instanceof HTMLElement) {
return this.subscriptionsStatus;
}
const anchor =
this.subscriptionList instanceof HTMLElement
? this.subscriptionList
: this.subscriptionListEmpty instanceof HTMLElement
? this.subscriptionListEmpty
: null;
if (!anchor || !(anchor.parentElement instanceof HTMLElement)) {
return null;
}
const existing = anchor.parentElement.querySelector(
'[data-role="subscriptions-status"]',
);
if (existing instanceof HTMLElement) {
this.subscriptionsStatus = existing;
return existing;
}
const status = document.createElement("div");
status.dataset.role = "subscriptions-status";
status.className = "mt-4 flex items-center gap-3 text-sm text-muted hidden";
status.setAttribute("role", "status");
status.setAttribute("aria-live", "polite");
if (this.subscriptionList instanceof HTMLElement) {
anchor.parentElement.insertBefore(status, this.subscriptionList);
} else {
anchor.parentElement.appendChild(status);
}
this.subscriptionsStatus = status;
return status;
}
setSubscriptionsLoadingState(state = "idle", options = {}) {
const statusEl = this.ensureSubscriptionsStatusElement();
if (!statusEl) {
this.subscriptionsLoadingState = state;
return;
}
const message =
typeof options.message === "string" && options.message.trim()
? options.message.trim()
: "";
statusEl.innerHTML = "";
statusEl.classList.remove("text-status-warning");
statusEl.classList.add("text-muted");
statusEl.classList.add("hidden");
this.subscriptionsLoadingState = state;
if (state === "loading") {
if (this.subscriptionListEmpty instanceof HTMLElement) {
this.subscriptionListEmpty.classList.add("hidden");
}
const spinner = document.createElement("span");
spinner.className = "status-spinner status-spinner--inline";
spinner.setAttribute("aria-hidden", "true");
const text = document.createElement("span");
text.textContent = message || "Loading subscriptions…";
statusEl.appendChild(spinner);
statusEl.appendChild(text);
statusEl.classList.remove("hidden");
return;
}
if (state === "error") {
statusEl.classList.remove("text-muted");
statusEl.classList.add("text-status-warning");
if (this.subscriptionListEmpty instanceof HTMLElement) {
this.subscriptionListEmpty.classList.add("hidden");
}
const text = document.createElement("span");
text.textContent =
message || "Subscriptions may be out of date. Try again later.";
statusEl.appendChild(text);
statusEl.classList.remove("hidden");
}
}
handleSubscriptionsStatus(detail = {}) {
const status = typeof detail?.status === "string" ? detail.status : "";
const message =
typeof detail?.message === "string" && detail.message.trim()
? detail.message.trim()
: "";
if (status === "loading") {
this.setSubscriptionsLoadingState("loading", { message });
return;
}
if (status === "error") {
this.setSubscriptionsLoadingState("error", { message });
return;
}
if (status) {
this.setSubscriptionsLoadingState("idle");
}
}
normalizeHashtagTag(value) {
return normalizeHashtag(value);
}
@@ -8141,6 +8322,9 @@ export class ProfileModalController {
if (!deduped.length) {
this.subscriptionListEmpty.classList.remove("hidden");
this.subscriptionList.classList.add("hidden");
if (this.subscriptionsLoadingState === "loading") {
this.setSubscriptionsLoadingState("idle");
}
return;
}
@@ -8224,6 +8408,10 @@ export class ProfileModalController {
this.subscriptionList.appendChild(item);
});
if (this.subscriptionsLoadingState === "loading") {
this.setSubscriptionsLoadingState("idle");
}
if (
entriesNeedingFetch.size &&
typeof this.services.batchFetchProfiles === "function"
@@ -8251,6 +8439,10 @@ export class ProfileModalController {
if (this.subscriptionListEmpty instanceof HTMLElement) {
this.subscriptionListEmpty.classList.remove("hidden");
}
if (this.subscriptionsLoadingState === "loading") {
this.setSubscriptionsLoadingState("idle");
}
}
async handleUnsubscribeFromCreator(candidate) {
+25 -2
View File
@@ -825,7 +825,20 @@ class UserBlockListManager {
let permissionError = null;
if ((!signerHasNip44 && requiresNip44) || (!signerHasNip04 && requiresNip04)) {
try {
const permissionResult = await requestDefaultExtensionPermissions();
const permissionMethods = [];
if (requiresNip04) {
permissionMethods.push("nip04.decrypt");
}
if (requiresNip44) {
permissionMethods.push("nip44.decrypt", "nip44.v2.decrypt");
}
emitStatus({
status: "loading",
message: "Decrypting blocked creators…",
reason: "permissions",
});
const permissionResult =
await requestDefaultExtensionPermissions(permissionMethods);
if (!permissionResult?.ok) {
permissionError =
permissionResult?.error instanceof Error
@@ -836,6 +849,8 @@ class UserBlockListManager {
}
} catch (error) {
permissionError = error instanceof Error ? error : new Error(String(error));
} finally {
emitStatus({ status: "settled", reason: "permissions" });
}
}
@@ -1789,7 +1804,15 @@ class UserBlockListManager {
}
if (signer.type === "extension") {
const permissionResult = await requestDefaultExtensionPermissions();
const permissionMethods = ["sign_event"];
if (typeof signer.nip04Encrypt === "function") {
permissionMethods.push("nip04.encrypt");
}
if (typeof signer.nip44Encrypt === "function") {
permissionMethods.push("nip44.encrypt", "nip44.v2.encrypt");
}
const permissionResult =
await requestDefaultExtensionPermissions(permissionMethods);
if (!permissionResult.ok) {
userLogger.warn(
"[UserBlockList] Signer permissions denied while updating the block list.",