diff --git a/package.json b/package.json
index 5abce00..600b91d 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"name": "plebeian-signer",
- "version": "v1.1.0",
+ "version": "v1.1.1",
"custom": {
"chrome": {
- "version": "v1.1.0"
+ "version": "v1.1.1"
},
"firefox": {
- "version": "v1.1.0"
+ "version": "v1.1.1"
}
},
"scripts": {
diff --git a/projects/chrome/public/manifest.json b/projects/chrome/public/manifest.json
index 541d415..925a430 100644
--- a/projects/chrome/public/manifest.json
+++ b/projects/chrome/public/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
- "version": "1.1.0",
+ "version": "1.1.1",
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
"options_page": "options.html",
"permissions": [
diff --git a/projects/chrome/public/prompt.html b/projects/chrome/public/prompt.html
index 24ee65f..fbbb0ab 100644
--- a/projects/chrome/public/prompt.html
+++ b/projects/chrome/public/prompt.html
@@ -278,6 +278,13 @@
+
+
All Queued
+
+
+
+
+
diff --git a/projects/chrome/src/background-common.ts b/projects/chrome/src/background-common.ts
index 0dbb4ef..104197c 100644
--- a/projects/chrome/src/background-common.ts
+++ b/projects/chrome/src/background-common.ts
@@ -48,8 +48,10 @@ export const debug = function (message: any) {
export type PromptResponse =
| 'reject'
| 'reject-once'
+ | 'reject-all' // P2: Reject all requests of this type from this host
| 'approve'
- | 'approve-once';
+ | 'approve-once'
+ | 'approve-all'; // P2: Approve all requests of this type from this host
export interface PromptResponseMessage {
id: string;
diff --git a/projects/chrome/src/background.ts b/projects/chrome/src/background.ts
index 104b1d9..2fbaba4 100644
--- a/projects/chrome/src/background.ts
+++ b/projects/chrome/src/background.ts
@@ -105,11 +105,24 @@ function parseInvoiceAmount(invoice: string): number | undefined {
type Relays = Record;
+// ==========================================
+// Permission Prompt Queue System (P0)
+// ==========================================
+
+// Timeout for permission prompts (30 seconds)
+const PROMPT_TIMEOUT_MS = 30000;
+
+// Maximum number of queued permission requests (prevent DoS)
+const MAX_PERMISSION_QUEUE_SIZE = 100;
+
+// Track open prompts with metadata for cleanup
const openPrompts = new Map<
string,
{
resolve: (response: PromptResponse) => void;
reject: (reason?: any) => void;
+ windowId?: number;
+ timeoutId?: ReturnType;
}
>();
@@ -123,6 +136,170 @@ const pendingRequests: {
reject: (error: any) => void;
}[] = [];
+// Queue for permission requests (only one prompt shown at a time)
+interface PermissionQueueItem {
+ id: string;
+ url: string;
+ width: number;
+ height: number;
+ resolve: (response: PromptResponse) => void;
+ reject: (reason?: any) => void;
+}
+
+const permissionQueue: PermissionQueueItem[] = [];
+let activePromptId: string | null = null;
+
+/**
+ * Show the next permission prompt from the queue
+ */
+async function showNextPermissionPrompt(): Promise {
+ if (activePromptId || permissionQueue.length === 0) {
+ return;
+ }
+
+ const next = permissionQueue[0];
+ activePromptId = next.id;
+
+ const { top, left } = await getPosition(next.width, next.height);
+
+ try {
+ const window = await browser.windows.create({
+ type: 'popup',
+ url: next.url,
+ height: next.height,
+ width: next.width,
+ top,
+ left,
+ });
+
+ const promptData = openPrompts.get(next.id);
+ if (promptData && window.id) {
+ promptData.windowId = window.id;
+ promptData.timeoutId = setTimeout(() => {
+ debug(`Prompt ${next.id} timed out after ${PROMPT_TIMEOUT_MS}ms`);
+ cleanupPrompt(next.id, 'timeout');
+ }, PROMPT_TIMEOUT_MS);
+ }
+ } catch (error) {
+ debug(`Failed to create prompt window: ${error}`);
+ cleanupPrompt(next.id, 'error');
+ }
+}
+
+/**
+ * Clean up a prompt and process the next one in queue
+ */
+function cleanupPrompt(promptId: string, reason: 'response' | 'timeout' | 'closed' | 'error'): void {
+ const promptData = openPrompts.get(promptId);
+
+ if (promptData) {
+ if (promptData.timeoutId) {
+ clearTimeout(promptData.timeoutId);
+ }
+ if (reason !== 'response') {
+ promptData.reject(new Error(`Permission prompt ${reason}`));
+ }
+ openPrompts.delete(promptId);
+ }
+
+ const queueIndex = permissionQueue.findIndex(item => item.id === promptId);
+ if (queueIndex !== -1) {
+ permissionQueue.splice(queueIndex, 1);
+ }
+
+ if (activePromptId === promptId) {
+ activePromptId = null;
+ }
+
+ showNextPermissionPrompt();
+}
+
+/**
+ * Queue a permission prompt request
+ */
+function queuePermissionPrompt(
+ urlWithoutId: string,
+ width: number,
+ height: number
+): Promise {
+ return new Promise((resolve, reject) => {
+ if (permissionQueue.length >= MAX_PERMISSION_QUEUE_SIZE) {
+ reject(new Error('Too many pending permission requests. Please try again later.'));
+ return;
+ }
+
+ const id = crypto.randomUUID();
+ const separator = urlWithoutId.includes('?') ? '&' : '?';
+ const url = `${urlWithoutId}${separator}id=${id}`;
+
+ openPrompts.set(id, { resolve, reject });
+ permissionQueue.push({ id, url, width, height, resolve, reject });
+
+ debug(`Queued permission prompt ${id}. Queue size: ${permissionQueue.length}`);
+ showNextPermissionPrompt();
+ });
+}
+
+// Listen for window close events to clean up orphaned prompts
+browser.windows.onRemoved.addListener((windowId: number) => {
+ for (const [promptId, promptData] of openPrompts.entries()) {
+ if (promptData.windowId === windowId) {
+ debug(`Prompt window ${windowId} closed without response`);
+ cleanupPrompt(promptId, 'closed');
+ break;
+ }
+ }
+});
+
+// ==========================================
+// Request Deduplication (P1)
+// ==========================================
+
+const pendingRequestPromises = new Map>();
+
+/**
+ * Generate a hash key for request deduplication
+ */
+function getRequestHash(host: string, method: string, params: any): string {
+ if (method === 'signEvent' && params?.kind !== undefined) {
+ return `${host}:${method}:kind${params.kind}`;
+ }
+ if ((method.includes('encrypt') || method.includes('decrypt')) && params?.peerPubkey) {
+ return `${host}:${method}:${params.peerPubkey}`;
+ }
+ return `${host}:${method}`;
+}
+
+/**
+ * Queue a permission prompt with deduplication
+ */
+function queuePermissionPromptDeduped(
+ host: string,
+ method: string,
+ params: any,
+ urlWithoutId: string,
+ width: number,
+ height: number
+): Promise {
+ const hash = getRequestHash(host, method, params);
+
+ const existingPromise = pendingRequestPromises.get(hash);
+ if (existingPromise) {
+ debug(`Deduplicating request: ${hash}`);
+ return existingPromise;
+ }
+
+ const promise = queuePermissionPrompt(urlWithoutId, width, height)
+ .finally(() => {
+ pendingRequestPromises.delete(hash);
+ });
+
+ pendingRequestPromises.set(hash, promise);
+ debug(`New permission request: ${hash}`);
+
+ return promise;
+}
+
browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
debug('Message received');
@@ -164,13 +341,12 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
const promptResponse = request as PromptResponseMessage;
const openPrompt = openPrompts.get(promptResponse.id);
if (!openPrompt) {
- throw new Error(
- 'Prompt response could not be matched to any previous request.'
- );
+ debug('Prompt response could not be matched (may have timed out)');
+ return;
}
openPrompt.resolve(promptResponse.response);
- openPrompts.delete(promptResponse.id);
+ cleanupPrompt(promptResponse.id, 'response');
return;
}
@@ -239,29 +415,23 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise
}
if (permissionState === undefined) {
- // Ask user for permission.
+ // Ask user for permission (queued + deduplicated)
const width = 375;
const height = 600;
- const { top, left } = await getPosition(width, height);
const base64Event = Buffer.from(
JSON.stringify(req.params ?? {}, undefined, 2)
).toString('base64');
- const response = await new Promise((resolve, reject) => {
- const id = crypto.randomUUID();
- openPrompts.set(id, { resolve, reject });
- browser.windows.create({
- type: 'popup',
- url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
- height,
- width,
- top,
- left,
- });
- });
+ // Include queue info for user awareness
+ const queueSize = permissionQueue.length;
+ const promptUrl = `prompt.html?method=${req.method}&host=${req.host}&nick=${encodeURIComponent(currentIdentity.nick)}&event=${base64Event}&queue=${queueSize}`;
+ const response = await queuePermissionPromptDeduped(req.host, req.method, req.params, promptUrl, width, height);
debug(response);
+
+ // Handle permission storage based on response type
if (response === 'approve' || response === 'reject') {
+ // Store permission for this specific kind (if signEvent) or method
const policy = response === 'approve' ? 'allow' : 'deny';
await storePermission(
browserSessionData,
@@ -271,15 +441,34 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise
policy,
req.params?.kind
);
- await backgroundLogPermissionStored(
+ await backgroundLogPermissionStored(req.host, req.method, policy, req.params?.kind);
+ } else if (response === 'approve-all') {
+ // P2: Store permission for ALL kinds/uses of this method from this host
+ await storePermission(
+ browserSessionData,
+ currentIdentity,
req.host,
req.method,
- policy,
- req.params?.kind
+ 'allow',
+ undefined // undefined kind = allow all kinds for signEvent
);
+ await backgroundLogPermissionStored(req.host, req.method, 'allow', undefined);
+ debug(`Stored approve-all permission for ${req.method} from ${req.host}`);
+ } else if (response === 'reject-all') {
+ // P2: Store deny permission for ALL uses of this method from this host
+ await storePermission(
+ browserSessionData,
+ currentIdentity,
+ req.host,
+ req.method,
+ 'deny',
+ undefined
+ );
+ await backgroundLogPermissionStored(req.host, req.method, 'deny', undefined);
+ debug(`Stored reject-all permission for ${req.method} from ${req.host}`);
}
- if (['reject', 'reject-once'].includes(response)) {
+ if (['reject', 'reject-once', 'reject-all'].includes(response)) {
await backgroundLogNip07Action(req.method, req.host, false, false, {
kind: req.params?.kind,
peerPubkey: req.params?.peerPubkey,
@@ -404,10 +593,9 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise
}
if (permissionState === undefined) {
- // Ask user for permission
+ // Ask user for permission (queued + deduplicated)
const width = 375;
const height = 600;
- const { top, left } = await getPosition(width, height);
// For sendPayment, include the invoice amount in the prompt data
let promptParams = req.params ?? {};
@@ -420,18 +608,10 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise
JSON.stringify(promptParams, undefined, 2)
).toString('base64');
- const response = await new Promise((resolve, reject) => {
- const id = crypto.randomUUID();
- openPrompts.set(id, { resolve, reject });
- browser.windows.create({
- type: 'popup',
- url: `prompt.html?method=${method}&host=${req.host}&id=${id}&nick=WebLN&event=${base64Event}`,
- height,
- width,
- top,
- left,
- });
- });
+ // Include queue info for user awareness
+ const queueSize = permissionQueue.length;
+ const promptUrl = `prompt.html?method=${method}&host=${req.host}&nick=WebLN&event=${base64Event}&queue=${queueSize}`;
+ const response = await queuePermissionPromptDeduped(req.host, method, req.params, promptUrl, width, height);
debug(response);
@@ -446,9 +626,20 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise
policy
);
await backgroundLogPermissionStored(req.host, method, policy);
+ } else if (response === 'approve-all' && method !== 'webln.sendPayment' && method !== 'webln.keysend') {
+ // P2: Store permission for all uses of this WebLN method
+ await storePermission(
+ browserSessionData,
+ null,
+ req.host,
+ method,
+ 'allow'
+ );
+ await backgroundLogPermissionStored(req.host, method, 'allow');
+ debug(`Stored approve-all permission for ${method} from ${req.host}`);
}
- if (['reject', 'reject-once'].includes(response)) {
+ if (['reject', 'reject-once', 'reject-all'].includes(response)) {
throw new Error('Permission denied');
}
}
diff --git a/projects/chrome/src/prompt.ts b/projects/chrome/src/prompt.ts
index 99ace62..94a0072 100644
--- a/projects/chrome/src/prompt.ts
+++ b/projects/chrome/src/prompt.ts
@@ -302,4 +302,21 @@ document.addEventListener('DOMContentLoaded', function () {
approveAlwaysButton?.addEventListener('click', () => {
deliver('approve');
});
+
+ const rejectAllButton = document.getElementById('rejectAllButton');
+ rejectAllButton?.addEventListener('click', () => {
+ deliver('reject-all');
+ });
+
+ const approveAllButton = document.getElementById('approveAllButton');
+ approveAllButton?.addEventListener('click', () => {
+ deliver('approve-all');
+ });
+
+ // Show/hide "All Queued" row based on queue size
+ const queueSize = parseInt(params.get('queueSize') || '0', 10);
+ const allQueuedRow = document.getElementById('allQueuedRow');
+ if (allQueuedRow && queueSize <= 1) {
+ allQueuedRow.style.display = 'none';
+ }
});
diff --git a/projects/firefox/public/manifest.json b/projects/firefox/public/manifest.json
index 0059cb2..8d4a494 100644
--- a/projects/firefox/public/manifest.json
+++ b/projects/firefox/public/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Plebeian Signer",
"description": "Nostr Identity Manager & Signer",
- "version": "1.1.0",
+ "version": "1.1.1",
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
"options_page": "options.html",
"permissions": [
diff --git a/projects/firefox/public/prompt.html b/projects/firefox/public/prompt.html
index 24ee65f..fbbb0ab 100644
--- a/projects/firefox/public/prompt.html
+++ b/projects/firefox/public/prompt.html
@@ -278,6 +278,13 @@
+
+
All Queued
+
+
+
+
+
diff --git a/projects/firefox/src/background-common.ts b/projects/firefox/src/background-common.ts
index f2138c9..68d89a2 100644
--- a/projects/firefox/src/background-common.ts
+++ b/projects/firefox/src/background-common.ts
@@ -49,8 +49,10 @@ export const debug = function (message: any) {
export type PromptResponse =
| 'reject'
| 'reject-once'
+ | 'reject-all' // P2: Reject all requests of this type from this host
| 'approve'
- | 'approve-once';
+ | 'approve-once'
+ | 'approve-all'; // P2: Approve all requests of this type from this host
export interface PromptResponseMessage {
id: string;
diff --git a/projects/firefox/src/background.ts b/projects/firefox/src/background.ts
index 104b1d9..2fbaba4 100644
--- a/projects/firefox/src/background.ts
+++ b/projects/firefox/src/background.ts
@@ -105,11 +105,24 @@ function parseInvoiceAmount(invoice: string): number | undefined {
type Relays = Record;
+// ==========================================
+// Permission Prompt Queue System (P0)
+// ==========================================
+
+// Timeout for permission prompts (30 seconds)
+const PROMPT_TIMEOUT_MS = 30000;
+
+// Maximum number of queued permission requests (prevent DoS)
+const MAX_PERMISSION_QUEUE_SIZE = 100;
+
+// Track open prompts with metadata for cleanup
const openPrompts = new Map<
string,
{
resolve: (response: PromptResponse) => void;
reject: (reason?: any) => void;
+ windowId?: number;
+ timeoutId?: ReturnType;
}
>();
@@ -123,6 +136,170 @@ const pendingRequests: {
reject: (error: any) => void;
}[] = [];
+// Queue for permission requests (only one prompt shown at a time)
+interface PermissionQueueItem {
+ id: string;
+ url: string;
+ width: number;
+ height: number;
+ resolve: (response: PromptResponse) => void;
+ reject: (reason?: any) => void;
+}
+
+const permissionQueue: PermissionQueueItem[] = [];
+let activePromptId: string | null = null;
+
+/**
+ * Show the next permission prompt from the queue
+ */
+async function showNextPermissionPrompt(): Promise {
+ if (activePromptId || permissionQueue.length === 0) {
+ return;
+ }
+
+ const next = permissionQueue[0];
+ activePromptId = next.id;
+
+ const { top, left } = await getPosition(next.width, next.height);
+
+ try {
+ const window = await browser.windows.create({
+ type: 'popup',
+ url: next.url,
+ height: next.height,
+ width: next.width,
+ top,
+ left,
+ });
+
+ const promptData = openPrompts.get(next.id);
+ if (promptData && window.id) {
+ promptData.windowId = window.id;
+ promptData.timeoutId = setTimeout(() => {
+ debug(`Prompt ${next.id} timed out after ${PROMPT_TIMEOUT_MS}ms`);
+ cleanupPrompt(next.id, 'timeout');
+ }, PROMPT_TIMEOUT_MS);
+ }
+ } catch (error) {
+ debug(`Failed to create prompt window: ${error}`);
+ cleanupPrompt(next.id, 'error');
+ }
+}
+
+/**
+ * Clean up a prompt and process the next one in queue
+ */
+function cleanupPrompt(promptId: string, reason: 'response' | 'timeout' | 'closed' | 'error'): void {
+ const promptData = openPrompts.get(promptId);
+
+ if (promptData) {
+ if (promptData.timeoutId) {
+ clearTimeout(promptData.timeoutId);
+ }
+ if (reason !== 'response') {
+ promptData.reject(new Error(`Permission prompt ${reason}`));
+ }
+ openPrompts.delete(promptId);
+ }
+
+ const queueIndex = permissionQueue.findIndex(item => item.id === promptId);
+ if (queueIndex !== -1) {
+ permissionQueue.splice(queueIndex, 1);
+ }
+
+ if (activePromptId === promptId) {
+ activePromptId = null;
+ }
+
+ showNextPermissionPrompt();
+}
+
+/**
+ * Queue a permission prompt request
+ */
+function queuePermissionPrompt(
+ urlWithoutId: string,
+ width: number,
+ height: number
+): Promise {
+ return new Promise((resolve, reject) => {
+ if (permissionQueue.length >= MAX_PERMISSION_QUEUE_SIZE) {
+ reject(new Error('Too many pending permission requests. Please try again later.'));
+ return;
+ }
+
+ const id = crypto.randomUUID();
+ const separator = urlWithoutId.includes('?') ? '&' : '?';
+ const url = `${urlWithoutId}${separator}id=${id}`;
+
+ openPrompts.set(id, { resolve, reject });
+ permissionQueue.push({ id, url, width, height, resolve, reject });
+
+ debug(`Queued permission prompt ${id}. Queue size: ${permissionQueue.length}`);
+ showNextPermissionPrompt();
+ });
+}
+
+// Listen for window close events to clean up orphaned prompts
+browser.windows.onRemoved.addListener((windowId: number) => {
+ for (const [promptId, promptData] of openPrompts.entries()) {
+ if (promptData.windowId === windowId) {
+ debug(`Prompt window ${windowId} closed without response`);
+ cleanupPrompt(promptId, 'closed');
+ break;
+ }
+ }
+});
+
+// ==========================================
+// Request Deduplication (P1)
+// ==========================================
+
+const pendingRequestPromises = new Map>();
+
+/**
+ * Generate a hash key for request deduplication
+ */
+function getRequestHash(host: string, method: string, params: any): string {
+ if (method === 'signEvent' && params?.kind !== undefined) {
+ return `${host}:${method}:kind${params.kind}`;
+ }
+ if ((method.includes('encrypt') || method.includes('decrypt')) && params?.peerPubkey) {
+ return `${host}:${method}:${params.peerPubkey}`;
+ }
+ return `${host}:${method}`;
+}
+
+/**
+ * Queue a permission prompt with deduplication
+ */
+function queuePermissionPromptDeduped(
+ host: string,
+ method: string,
+ params: any,
+ urlWithoutId: string,
+ width: number,
+ height: number
+): Promise {
+ const hash = getRequestHash(host, method, params);
+
+ const existingPromise = pendingRequestPromises.get(hash);
+ if (existingPromise) {
+ debug(`Deduplicating request: ${hash}`);
+ return existingPromise;
+ }
+
+ const promise = queuePermissionPrompt(urlWithoutId, width, height)
+ .finally(() => {
+ pendingRequestPromises.delete(hash);
+ });
+
+ pendingRequestPromises.set(hash, promise);
+ debug(`New permission request: ${hash}`);
+
+ return promise;
+}
+
browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
debug('Message received');
@@ -164,13 +341,12 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
const promptResponse = request as PromptResponseMessage;
const openPrompt = openPrompts.get(promptResponse.id);
if (!openPrompt) {
- throw new Error(
- 'Prompt response could not be matched to any previous request.'
- );
+ debug('Prompt response could not be matched (may have timed out)');
+ return;
}
openPrompt.resolve(promptResponse.response);
- openPrompts.delete(promptResponse.id);
+ cleanupPrompt(promptResponse.id, 'response');
return;
}
@@ -239,29 +415,23 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise
}
if (permissionState === undefined) {
- // Ask user for permission.
+ // Ask user for permission (queued + deduplicated)
const width = 375;
const height = 600;
- const { top, left } = await getPosition(width, height);
const base64Event = Buffer.from(
JSON.stringify(req.params ?? {}, undefined, 2)
).toString('base64');
- const response = await new Promise((resolve, reject) => {
- const id = crypto.randomUUID();
- openPrompts.set(id, { resolve, reject });
- browser.windows.create({
- type: 'popup',
- url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
- height,
- width,
- top,
- left,
- });
- });
+ // Include queue info for user awareness
+ const queueSize = permissionQueue.length;
+ const promptUrl = `prompt.html?method=${req.method}&host=${req.host}&nick=${encodeURIComponent(currentIdentity.nick)}&event=${base64Event}&queue=${queueSize}`;
+ const response = await queuePermissionPromptDeduped(req.host, req.method, req.params, promptUrl, width, height);
debug(response);
+
+ // Handle permission storage based on response type
if (response === 'approve' || response === 'reject') {
+ // Store permission for this specific kind (if signEvent) or method
const policy = response === 'approve' ? 'allow' : 'deny';
await storePermission(
browserSessionData,
@@ -271,15 +441,34 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise
policy,
req.params?.kind
);
- await backgroundLogPermissionStored(
+ await backgroundLogPermissionStored(req.host, req.method, policy, req.params?.kind);
+ } else if (response === 'approve-all') {
+ // P2: Store permission for ALL kinds/uses of this method from this host
+ await storePermission(
+ browserSessionData,
+ currentIdentity,
req.host,
req.method,
- policy,
- req.params?.kind
+ 'allow',
+ undefined // undefined kind = allow all kinds for signEvent
);
+ await backgroundLogPermissionStored(req.host, req.method, 'allow', undefined);
+ debug(`Stored approve-all permission for ${req.method} from ${req.host}`);
+ } else if (response === 'reject-all') {
+ // P2: Store deny permission for ALL uses of this method from this host
+ await storePermission(
+ browserSessionData,
+ currentIdentity,
+ req.host,
+ req.method,
+ 'deny',
+ undefined
+ );
+ await backgroundLogPermissionStored(req.host, req.method, 'deny', undefined);
+ debug(`Stored reject-all permission for ${req.method} from ${req.host}`);
}
- if (['reject', 'reject-once'].includes(response)) {
+ if (['reject', 'reject-once', 'reject-all'].includes(response)) {
await backgroundLogNip07Action(req.method, req.host, false, false, {
kind: req.params?.kind,
peerPubkey: req.params?.peerPubkey,
@@ -404,10 +593,9 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise
}
if (permissionState === undefined) {
- // Ask user for permission
+ // Ask user for permission (queued + deduplicated)
const width = 375;
const height = 600;
- const { top, left } = await getPosition(width, height);
// For sendPayment, include the invoice amount in the prompt data
let promptParams = req.params ?? {};
@@ -420,18 +608,10 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise
JSON.stringify(promptParams, undefined, 2)
).toString('base64');
- const response = await new Promise((resolve, reject) => {
- const id = crypto.randomUUID();
- openPrompts.set(id, { resolve, reject });
- browser.windows.create({
- type: 'popup',
- url: `prompt.html?method=${method}&host=${req.host}&id=${id}&nick=WebLN&event=${base64Event}`,
- height,
- width,
- top,
- left,
- });
- });
+ // Include queue info for user awareness
+ const queueSize = permissionQueue.length;
+ const promptUrl = `prompt.html?method=${method}&host=${req.host}&nick=WebLN&event=${base64Event}&queue=${queueSize}`;
+ const response = await queuePermissionPromptDeduped(req.host, method, req.params, promptUrl, width, height);
debug(response);
@@ -446,9 +626,20 @@ async function processWeblnRequest(req: BackgroundRequestMessage): Promise
policy
);
await backgroundLogPermissionStored(req.host, method, policy);
+ } else if (response === 'approve-all' && method !== 'webln.sendPayment' && method !== 'webln.keysend') {
+ // P2: Store permission for all uses of this WebLN method
+ await storePermission(
+ browserSessionData,
+ null,
+ req.host,
+ method,
+ 'allow'
+ );
+ await backgroundLogPermissionStored(req.host, method, 'allow');
+ debug(`Stored approve-all permission for ${method} from ${req.host}`);
}
- if (['reject', 'reject-once'].includes(response)) {
+ if (['reject', 'reject-once', 'reject-all'].includes(response)) {
throw new Error('Permission denied');
}
}
diff --git a/projects/firefox/src/prompt.ts b/projects/firefox/src/prompt.ts
index 33f148f..3745e6f 100644
--- a/projects/firefox/src/prompt.ts
+++ b/projects/firefox/src/prompt.ts
@@ -303,4 +303,21 @@ document.addEventListener('DOMContentLoaded', function () {
approveAlwaysButton?.addEventListener('click', () => {
deliver('approve');
});
+
+ const rejectAllButton = document.getElementById('rejectAllButton');
+ rejectAllButton?.addEventListener('click', () => {
+ deliver('reject-all');
+ });
+
+ const approveAllButton = document.getElementById('approveAllButton');
+ approveAllButton?.addEventListener('click', () => {
+ deliver('approve-all');
+ });
+
+ // Show/hide "All Queued" row based on queue size
+ const queueSize = parseInt(params.get('queueSize') || '0', 10);
+ const allQueuedRow = document.getElementById('allQueuedRow');
+ if (allQueuedRow && queueSize <= 1) {
+ allQueuedRow.style.display = 'none';
+ }
});
diff --git a/releases/plebeian-signer-chrome-v1.1.0.zip b/releases/plebeian-signer-chrome-v1.1.1.zip
similarity index 86%
rename from releases/plebeian-signer-chrome-v1.1.0.zip
rename to releases/plebeian-signer-chrome-v1.1.1.zip
index fb33c22..8730ec8 100644
Binary files a/releases/plebeian-signer-chrome-v1.1.0.zip and b/releases/plebeian-signer-chrome-v1.1.1.zip differ
diff --git a/releases/plebeian-signer-firefox-v1.1.0.zip b/releases/plebeian-signer-firefox-v1.1.1.zip
similarity index 84%
rename from releases/plebeian-signer-firefox-v1.1.0.zip
rename to releases/plebeian-signer-firefox-v1.1.1.zip
index 5d80eeb..4688139 100644
Binary files a/releases/plebeian-signer-firefox-v1.1.0.zip and b/releases/plebeian-signer-firefox-v1.1.1.zip differ