@@ -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