Skip to content

Commit d58bea9

Browse files
Fix: Remove blocking webRequest listeners (Manifest V3 incompatible)
- Remove blocking webRequest listeners that require webRequestBlocking permission - Implement JavaScript-level interception via fetch/XMLHttpRequest patching - Add CREATE_NIP98_AUTH_HEADER message handler - Intercept fetch and XMLHttpRequest in content script - Handle 401 responses with automatic retry Fixes Chrome extension error about webRequestBlocking permission
1 parent 460b393 commit d58bea9

2 files changed

Lines changed: 168 additions & 209 deletions

File tree

src/background.js

Lines changed: 59 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ async function handleMessage (message, sender) {
8383
case 'NIP04_DECRYPT':
8484
throw new Error('NIP-04 encryption not yet implemented');
8585

86+
case 'CREATE_NIP98_AUTH_HEADER':
87+
return await createNip98AuthHeader(
88+
message.url,
89+
message.method,
90+
message.body
91+
);
92+
8693
default:
8794
throw new Error(`Unknown message type: ${type}`);
8895
}
@@ -297,62 +304,6 @@ function formatEventForPrompt (event) {
297304
return lines.join('\n');
298305
}
299306

300-
/**
301-
* Create NIP-98 authentication event for an HTTP request
302-
* @param {object} requestDetails - Chrome webRequest details
303-
* @returns {Promise<object>} Unsigned NIP-98 event
304-
*/
305-
async function createNip98AuthEvent (requestDetails) {
306-
const event = {
307-
kind: 27235,
308-
content: '',
309-
created_at: Math.floor(Date.now() / 1000),
310-
tags: [
311-
['u', requestDetails.url], // Full URL including query params
312-
['method', requestDetails.method]
313-
]
314-
};
315-
316-
// If request has body, add payload tag with SHA-256 hash
317-
if (requestDetails.requestBody) {
318-
const bodyHash = await hashRequestBody(requestDetails.requestBody);
319-
event.tags.push(['payload', bodyHash]);
320-
}
321-
322-
return event;
323-
}
324-
325-
/**
326-
* Hash request body for NIP-98 payload tag
327-
* @param {object} requestBody - Chrome webRequest requestBody
328-
* @returns {Promise<string>} SHA-256 hash as hex string
329-
*/
330-
async function hashRequestBody (requestBody) {
331-
let bodyBytes;
332-
333-
if (requestBody.raw) {
334-
// ArrayBuffer[] format from Chrome
335-
const chunks = requestBody.raw;
336-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.bytes.byteLength, 0);
337-
const combined = new Uint8Array(totalLength);
338-
let offset = 0;
339-
for (const chunk of chunks) {
340-
combined.set(new Uint8Array(chunk.bytes), offset);
341-
offset += chunk.bytes.byteLength;
342-
}
343-
bodyBytes = combined;
344-
} else if (requestBody.formData) {
345-
// FormData - convert to string representation
346-
const formDataStr = JSON.stringify(requestBody.formData);
347-
bodyBytes = new TextEncoder().encode(formDataStr);
348-
} else {
349-
// Fallback: treat as empty
350-
bodyBytes = new Uint8Array(0);
351-
}
352-
353-
const hash = sha256(bodyBytes);
354-
return bytesToHex(hash);
355-
}
356307

357308
/**
358309
* Encode signed event to Authorization header value
@@ -365,185 +316,85 @@ function encodeNip98Header (signedEvent) {
365316
return btoa(eventJson);
366317
}
367318

368-
/**
369-
* Check if request should have NIP-98 auth added
370-
* @param {object} requestDetails - Chrome webRequest details
371-
* @returns {Promise<boolean>}
372-
*/
373-
async function shouldAddNip98Auth (requestDetails) {
374-
// Check if keypair exists
375-
const keyExists = await hasKeypair();
376-
if (!keyExists) {
377-
return false;
378-
}
379-
380-
// Check if origin is trusted
381-
const origin = new URL(requestDetails.url).origin;
382-
const trusted = await isTrustedOrigin(origin);
383-
if (!trusted) {
384-
return false;
385-
}
386-
387-
// Check if auto-sign is enabled
388-
const autoSign = await getAutoSign();
389-
if (!autoSign) {
390-
return false;
391-
}
392-
393-
// Don't add auth if request already has Authorization header (unless it's a retry)
394-
const hasAuth = requestDetails.requestHeaders?.some(
395-
h => h.name.toLowerCase() === 'authorization'
396-
);
397-
if (hasAuth && !retryState.has(requestDetails.requestId)) {
398-
return false;
399-
}
400-
401-
return true;
402-
}
403319

404320
/**
405-
* Intercept requests and add NIP-98 auth if needed
406-
* @param {object} details - Chrome webRequest details
407-
* @returns {object|undefined} Modified request headers or undefined
321+
* Create NIP-98 auth header for a request (called from content script)
322+
* @param {string} url - Request URL
323+
* @param {string} method - HTTP method
324+
* @param {string|ArrayBuffer|Blob|null} body - Request body
325+
* @returns {Promise<string>} Authorization header value
408326
*/
409-
async function interceptRequest (details) {
327+
async function createNip98AuthHeader (url, method, body = null) {
410328
try {
411-
// Only process XMLHttpRequest and fetch requests
412-
if (!['xmlhttprequest', 'main_frame', 'sub_frame'].includes(details.type)) {
413-
return;
329+
// Check if we should add auth
330+
const origin = new URL(url).origin;
331+
const trusted = await isTrustedOrigin(origin);
332+
const autoSign = await getAutoSign();
333+
const keyExists = await hasKeypair();
334+
335+
if (!keyExists || !trusted || !autoSign) {
336+
return null;
414337
}
415338

416-
const shouldAuth = await shouldAddNip98Auth(details);
417-
if (!shouldAuth) {
418-
return;
339+
// Hash body if present
340+
let bodyHash = '';
341+
if (body) {
342+
if (typeof body === 'string') {
343+
bodyHash = bytesToHex(sha256(new TextEncoder().encode(body)));
344+
} else if (body instanceof ArrayBuffer) {
345+
bodyHash = bytesToHex(sha256(new Uint8Array(body)));
346+
} else if (body instanceof Blob) {
347+
const arrayBuffer = await body.arrayBuffer();
348+
bodyHash = bytesToHex(sha256(new Uint8Array(arrayBuffer)));
349+
}
419350
}
420351

421-
// Check cache first
422-
const bodyHash = details.requestBody ? await hashRequestBody(details.requestBody) : '';
423-
const cacheKey = `${details.url}:${details.method}:${bodyHash}`;
352+
// Check cache
353+
const cacheKey = `${url}:${method}:${bodyHash}`;
424354
const cached = nip98Cache.get(cacheKey);
425355

426356
let signedEvent;
427357
if (cached && cached.expires > Date.now()) {
428-
// Use cached event
429358
signedEvent = cached.event;
430359
console.log('[Podkey] Using cached NIP-98 auth event');
431360
} else {
432-
// Create and sign new event
433-
const event = await createNip98AuthEvent(details);
361+
// Create and sign event
362+
const event = {
363+
kind: 27235,
364+
content: '',
365+
created_at: Math.floor(Date.now() / 1000),
366+
tags: [
367+
['u', url],
368+
['method', method]
369+
]
370+
};
371+
372+
if (bodyHash) {
373+
event.tags.push(['payload', bodyHash]);
374+
}
375+
434376
const keypair = await getKeypair();
435377
signedEvent = await signEvent(event, keypair.privateKey);
436378

437-
// Cache the signed event
379+
// Cache
438380
nip98Cache.set(cacheKey, {
439381
event: signedEvent,
440382
expires: Date.now() + CACHE_TTL
441383
});
442384

443-
console.log('[Podkey] Created and signed NIP-98 auth event for', details.url);
385+
console.log('[Podkey] Created and signed NIP-98 auth event for', url);
444386
}
445387

446-
// Encode to Authorization header
447-
const authHeader = encodeNip98Header(signedEvent);
448-
449-
// Add or replace Authorization header
450-
const headers = details.requestHeaders || [];
451-
const authIndex = headers.findIndex(h => h.name.toLowerCase() === 'authorization');
452-
453-
if (authIndex >= 0) {
454-
headers[authIndex].value = `Nostr ${authHeader}`;
455-
} else {
456-
headers.push({
457-
name: 'Authorization',
458-
value: `Nostr ${authHeader}`
459-
});
460-
}
461-
462-
return { requestHeaders: headers };
388+
return `Nostr ${encodeNip98Header(signedEvent)}`;
463389
} catch (error) {
464-
console.error('[Podkey] Error intercepting request:', error);
465-
// Don't block the request if auth fails
466-
return;
390+
console.error('[Podkey] Error creating NIP-98 auth header:', error);
391+
return null;
467392
}
468393
}
469394

470-
/**
471-
* Handle 401 response - create auth and retry
472-
* @param {object} details - Chrome webRequest details
473-
* @returns {object|undefined} Modified response or undefined
474-
*/
475-
async function handle401Response (details) {
476-
// Only retry on 401 Unauthorized
477-
if (details.statusCode !== 401) {
478-
return;
479-
}
480-
481-
// Prevent infinite retry loops
482-
if (retryState.has(details.requestId)) {
483-
console.log('[Podkey] Already retried this request, skipping');
484-
return;
485-
}
486-
487-
try {
488-
// Check if we should auto-auth this origin
489-
const origin = new URL(details.url).origin;
490-
const trusted = await isTrustedOrigin(origin);
491-
const autoSign = await getAutoSign();
492-
493-
if (!trusted || !autoSign) {
494-
console.log('[Podkey] Origin not trusted or auto-sign disabled, not retrying');
495-
return;
496-
}
497-
498-
// Mark as retrying
499-
retryState.set(details.requestId, true);
500-
501-
// Create NIP-98 auth event
502-
const event = await createNip98AuthEvent({
503-
url: details.url,
504-
method: details.method,
505-
requestBody: details.requestBody
506-
});
507-
508-
const keypair = await getKeypair();
509-
const signedEvent = await signEvent(event, keypair.privateKey);
510-
const authHeader = encodeNip98Header(signedEvent);
511-
512-
console.log('[Podkey] 401 detected, created NIP-98 auth for retry:', details.url);
513-
514-
// Retry the request with auth header
515-
// Note: Chrome webRequest API doesn't support retrying directly
516-
// The page/script needs to retry, but we can log the auth header
517-
// For now, we'll rely on the onBeforeSendHeaders interceptor for the retry
518-
// This is a limitation - we'd need to use fetch() API to actually retry
519-
520-
// Clean up retry state after a delay
521-
setTimeout(() => {
522-
retryState.delete(details.requestId);
523-
}, 5000);
524-
} catch (error) {
525-
console.error('[Podkey] Error handling 401 response:', error);
526-
retryState.delete(details.requestId);
527-
}
528-
}
395+
// Note: Blocking webRequest listeners require webRequestBlocking permission,
396+
// which is deprecated in Manifest V3 and only available for enterprise extensions.
397+
// Instead, we use JavaScript-level interception via content scripts.
398+
// See src/injected.js for fetch/XMLHttpRequest interception.
529399

530-
// Set up webRequest listeners for NIP-98 auto-auth
531-
chrome.webRequest.onBeforeSendHeaders.addListener(
532-
interceptRequest,
533-
{
534-
urls: ['<all_urls>'],
535-
types: ['xmlhttprequest', 'main_frame', 'sub_frame']
536-
},
537-
['requestHeaders', 'blocking']
538-
);
539-
540-
chrome.webRequest.onHeadersReceived.addListener(
541-
handle401Response,
542-
{
543-
urls: ['<all_urls>'],
544-
types: ['xmlhttprequest', 'main_frame', 'sub_frame']
545-
},
546-
['responseHeaders']
547-
);
548-
549-
console.log('[Podkey] NIP-98 auto-auth listeners registered');
400+
console.log('[Podkey] NIP-98 auto-auth: Using JavaScript-level interception (see injected.js)');

0 commit comments

Comments
 (0)