Defer NIP-07 permission prompts

This commit is contained in:
thePR0M3TH3AN committed 2026-01-30 16:28:00 -05:00
1 parent a5c7d3d496
commit e77f5f3636
2 files changed
+161 -12

No files matched your search

+138 -12
View File
@@ -428,6 +428,54 @@ function shouldRequestExtensionPermissions(signer) {
return signer.type === "extension";
}
const PERMISSION_STATUS_AUTO_HIDE_MS = 12_000;
const ENCRYPTION_METHOD_PREFIXES = ["nip04.", "nip44."];
function hasEncryptionPermissionMethods(methods) {
if (!Array.isArray(methods)) {
return false;
}
return methods.some((method) =>
ENCRYPTION_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix)),
);
}
function hasSigningPermissionMethods(methods) {
if (!Array.isArray(methods)) {
return false;
}
return methods.some(
(method) => method === "sign_event" || method === "get_public_key",
);
}
function resolvePermissionStatusMessage(methods, context) {
const normalizedContext =
typeof context === "string" ? context.trim().toLowerCase() : "";
if (normalizedContext === "dm") {
return "Approve the extension prompt to enable encrypted direct messages.";
}
if (normalizedContext === "lists") {
return "Approve the extension prompt to access your encrypted lists.";
}
const includesEncryption = hasEncryptionPermissionMethods(methods);
const includesSigning = hasSigningPermissionMethods(methods);
if (includesEncryption && includesSigning) {
return "Approve the extension prompt to enable signing and encrypted features (DMs, subscriptions, block lists).";
}
if (includesEncryption) {
return "Approve the extension prompt to enable encrypted features like DMs and private lists.";
}
if (includesSigning) {
return "Approve the extension prompt to enable signing.";
}
return "Approve the extension prompt to continue.";
}
const RELAY_CONNECT_TIMEOUT_MS = 5000;
const RELAY_RECONNECT_BASE_DELAY_MS = 2000;
const RELAY_RECONNECT_MAX_DELAY_MS = 60000;
@@ -1367,6 +1415,7 @@ export class NostrClient {
this.extensionPermissionsGranted = this.hasCachedExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
this.extensionPermissionStatusHandler = null;
}
getStoredNip46Metadata() {
@@ -2031,6 +2080,7 @@ export class NostrClient {
const permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_CORE_METHODS,
{ context: "login", logMetrics: true, showStatus: false },
);
if (!permissionResult.ok) {
const denialMessage =
@@ -2116,17 +2166,6 @@ export class NostrClient {
adapter.pubkey = pubkey;
setActiveSigner(adapter);
if (!this.extensionPermissionsGranted) {
try {
await this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
} catch (err) {
userLogger.warn(
"[nostr] Extension permissions were not fully granted after login:",
err,
);
}
}
return { pubkey, signer: adapter };
}
@@ -2916,6 +2955,26 @@ export class NostrClient {
return normalized.every((method) => this.extensionPermissionCache.has(method));
}
setExtensionPermissionStatusHandler(handler) {
this.extensionPermissionStatusHandler =
typeof handler === "function" ? handler : null;
}
emitExtensionPermissionStatus(status) {
if (typeof this.extensionPermissionStatusHandler !== "function") {
return;
}
try {
this.extensionPermissionStatusHandler(status);
} catch (error) {
devLogger.warn(
"[nostr] Extension permission status handler threw:",
error,
);
}
}
warmupExtension() {
const extension =
typeof window !== "undefined" && window && window.nostr ? window.nostr : null;
@@ -2929,11 +2988,16 @@ export class NostrClient {
};
}
async ensureExtensionPermissions(methods = DEFAULT_NIP07_PERMISSION_METHODS) {
async ensureExtensionPermissions(
methods = DEFAULT_NIP07_PERMISSION_METHODS,
options = {},
) {
if (!Array.isArray(methods)) {
methods = [];
}
const config = options && typeof options === "object" ? options : {};
const normalized = Array.from(
new Set(
methods
@@ -2978,10 +3042,70 @@ export class NostrClient {
}
this.extensionReady = true;
const statusHandler =
typeof config.onStatus === "function"
? config.onStatus
: this.extensionPermissionStatusHandler;
const shouldShowStatus = config.showStatus !== false;
const statusMessage =
typeof config.statusMessage === "string" && config.statusMessage.trim()
? config.statusMessage.trim()
: resolvePermissionStatusMessage(normalized, config.context);
const statusAutoHideMs = Number.isFinite(config.autoHideMs)
? config.autoHideMs
: PERMISSION_STATUS_AUTO_HIDE_MS;
const shouldShowSpinner = config.showSpinner !== false;
if (statusHandler && shouldShowStatus && statusMessage) {
try {
statusHandler({
phase: "permission",
state: "prompt",
context:
typeof config.context === "string" ? config.context.trim() : "",
message: statusMessage,
methods: outstanding.slice(),
autoHideMs: statusAutoHideMs,
showSpinner: shouldShowSpinner,
});
} catch (error) {
devLogger.warn(
"[nostr] Failed to emit extension permission status:",
error,
);
}
}
const shouldLogMetrics = config.logMetrics === true;
const now =
typeof performance !== "undefined" &&
typeof performance.now === "function"
? () => performance.now()
: () => Date.now();
const metricsStart = shouldLogMetrics ? now() : null;
const enableResult = await requestEnablePermissions(extension, outstanding, {
isDevMode,
});
if (shouldLogMetrics && metricsStart !== null) {
const metricsContext =
typeof config.metricsLabel === "string" && config.metricsLabel.trim()
? config.metricsLabel.trim()
: typeof config.context === "string" && config.context.trim()
? config.context.trim()
: "permissions";
const durationMs = Math.max(0, Math.round(now() - metricsStart));
userLogger.info(
`[nostr] extension.enable duration (${metricsContext})`,
{
durationMs,
requestedCount: outstanding.length,
ok: Boolean(enableResult?.ok),
},
);
}
if (enableResult?.ok) {
this.markExtensionPermissions(outstanding);
this.extensionPermissionsGranted = this.hasCachedExtensionPermissions(
@@ -4943,6 +5067,7 @@ export class NostrClient {
) {
extensionPermissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_ENCRYPTION_METHODS,
{ context: "dm" },
);
if (!extensionPermissionResult?.ok) {
devLogger.warn(
@@ -5400,6 +5525,7 @@ export class NostrClient {
if (shouldRequestExtensionPermissions(signer)) {
const permissionResult = await this.ensureExtensionPermissions(
DEFAULT_NIP07_ENCRYPTION_METHODS,
{ context: "dm" },
);
if (!permissionResult.ok) {
userLogger.warn(
+23
View File
@@ -717,6 +717,29 @@ export default class ApplicationBootstrap {
app.lastExperimentalWarningKey = null;
app.lastExperimentalWarningAt = 0;
if (
nostrClient &&
typeof nostrClient.setExtensionPermissionStatusHandler === "function"
) {
nostrClient.setExtensionPermissionStatusHandler((status) => {
if (!status || typeof status !== "object") {
return;
}
const message =
typeof status.message === "string" ? status.message.trim() : "";
if (!message) {
return;
}
const autoHideMs = Number.isFinite(status.autoHideMs)
? status.autoHideMs
: 12000;
const showSpinner = status.showSpinner !== false;
if (typeof app.showStatus === "function") {
app.showStatus(message, { autoHideMs, showSpinner });
}
});
}
app.pubkey = null;
app.currentMagnetUri = null;
app.currentVideo = null;