@@ -12,9 +12,18 @@ import {
1212 addTrustedOrigin ,
1313 getAutoSign
1414} from './storage.js' ;
15+ import { sha256 } from '@noble/hashes/sha256' ;
16+ import { bytesToHex } from '@noble/hashes/utils' ;
1517
1618console . log ( '[Podkey] Background service worker started' ) ;
1719
20+ // NIP-98 auth event cache: key = `${url}:${method}:${bodyHash}`, value = { event, expires }
21+ const nip98Cache = new Map ( ) ;
22+ const CACHE_TTL = 60000 ; // 60 seconds
23+
24+ // Track retry state to prevent infinite loops: key = requestId, value = true
25+ const retryState = new Map ( ) ;
26+
1827// Initialize extension
1928chrome . runtime . onInstalled . addListener ( async ( ) => {
2029 console . log ( '[Podkey] Extension installed' ) ;
@@ -74,6 +83,13 @@ async function handleMessage (message, sender) {
7483 case 'NIP04_DECRYPT' :
7584 throw new Error ( 'NIP-04 encryption not yet implemented' ) ;
7685
86+ case 'CREATE_NIP98_AUTH_HEADER' :
87+ return await createNip98AuthHeader (
88+ message . url ,
89+ message . method ,
90+ message . body
91+ ) ;
92+
7793 default :
7894 throw new Error ( `Unknown message type: ${ type } ` ) ;
7995 }
@@ -287,3 +303,145 @@ function formatEventForPrompt (event) {
287303
288304 return lines . join ( '\n' ) ;
289305}
306+
307+
308+ /**
309+ * Encode signed event to Authorization header value
310+ * @param {object } signedEvent - Signed Nostr event
311+ * @returns {string } Base64-encoded event for Authorization header
312+ */
313+ function encodeNip98Header ( signedEvent ) {
314+ const eventJson = JSON . stringify ( signedEvent ) ;
315+ // Use btoa for base64 encoding (available in service workers)
316+ return btoa ( eventJson ) ;
317+ }
318+
319+
320+ /**
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
326+ */
327+ /**
328+ * Check if an origin is likely a Solid server
329+ * @param {string } origin - Origin to check
330+ * @returns {boolean }
331+ */
332+ function isLikelySolidServer ( origin ) {
333+ // Common Solid server indicators
334+ const solidIndicators = [
335+ 'solid.social' ,
336+ 'solidcommunity.net' ,
337+ 'inrupt.net' ,
338+ 'solidweb.org' ,
339+ '/.well-known/solid'
340+ ] ;
341+
342+ return solidIndicators . some ( indicator => origin . includes ( indicator ) ) ;
343+ }
344+
345+ async function createNip98AuthHeader ( url , method , body = null ) {
346+ try {
347+ // Check if we should add auth
348+ const origin = new URL ( url ) . origin ;
349+ const trusted = await isTrustedOrigin ( origin ) ;
350+ const autoSign = await getAutoSign ( ) ;
351+ const keyExists = await hasKeypair ( ) ;
352+ const isSolid = isLikelySolidServer ( origin ) ;
353+
354+ console . log ( '[Podkey] NIP-98 auth check:' , {
355+ url,
356+ origin,
357+ keyExists,
358+ trusted,
359+ autoSign,
360+ isSolid
361+ } ) ;
362+
363+ if ( ! keyExists ) {
364+ console . log ( '[Podkey] No keypair found, skipping NIP-98 auth' ) ;
365+ return null ;
366+ }
367+
368+ // For Solid servers, auto-trust on first use if auto-sign is enabled
369+ if ( ! trusted && isSolid && autoSign ) {
370+ console . log ( '[Podkey] Auto-trusting Solid server:' , origin ) ;
371+ await addTrustedOrigin ( origin ) ;
372+ } else if ( ! trusted ) {
373+ console . log ( '[Podkey] Origin not trusted, skipping NIP-98 auth' ) ;
374+ return null ;
375+ }
376+
377+ if ( ! autoSign ) {
378+ console . log ( '[Podkey] Auto-sign disabled, skipping NIP-98 auth' ) ;
379+ return null ;
380+ }
381+
382+ // Hash body if present
383+ let bodyHash = '' ;
384+ if ( body ) {
385+ if ( typeof body === 'string' ) {
386+ bodyHash = bytesToHex ( sha256 ( new TextEncoder ( ) . encode ( body ) ) ) ;
387+ } else if ( body instanceof ArrayBuffer ) {
388+ bodyHash = bytesToHex ( sha256 ( new Uint8Array ( body ) ) ) ;
389+ } else if ( body instanceof Blob ) {
390+ const arrayBuffer = await body . arrayBuffer ( ) ;
391+ bodyHash = bytesToHex ( sha256 ( new Uint8Array ( arrayBuffer ) ) ) ;
392+ }
393+ }
394+
395+ // Check cache
396+ const cacheKey = `${ url } :${ method } :${ bodyHash } ` ;
397+ const cached = nip98Cache . get ( cacheKey ) ;
398+
399+ let signedEvent ;
400+ if ( cached && cached . expires > Date . now ( ) ) {
401+ signedEvent = cached . event ;
402+ console . log ( '[Podkey] Using cached NIP-98 auth event' ) ;
403+ } else {
404+ // Create and sign event
405+ const event = {
406+ kind : 27235 ,
407+ content : '' ,
408+ created_at : Math . floor ( Date . now ( ) / 1000 ) ,
409+ tags : [
410+ [ 'u' , url ] ,
411+ [ 'method' , method ]
412+ ]
413+ } ;
414+
415+ if ( bodyHash ) {
416+ event . tags . push ( [ 'payload' , bodyHash ] ) ;
417+ }
418+
419+ const keypair = await getKeypair ( ) ;
420+ signedEvent = await signEvent ( event , keypair . privateKey ) ;
421+
422+ // Cache
423+ nip98Cache . set ( cacheKey , {
424+ event : signedEvent ,
425+ expires : Date . now ( ) + CACHE_TTL
426+ } ) ;
427+
428+ console . log ( '[Podkey] Created and signed NIP-98 auth event for' , url ) ;
429+ console . log ( '[Podkey] NIP-98 event:' , JSON . stringify ( signedEvent , null , 2 ) ) ;
430+ console . log ( '[Podkey] Public key (did:nostr):' , `did:nostr:${ keypair . publicKey } ` ) ;
431+ }
432+
433+ const authHeader = `Nostr ${ encodeNip98Header ( signedEvent ) } ` ;
434+ console . log ( '[Podkey] Authorization header (first 100 chars):' , authHeader . substring ( 0 , 100 ) + '...' ) ;
435+ return authHeader ;
436+ } catch ( error ) {
437+ console . error ( '[Podkey] Error creating NIP-98 auth header:' , error ) ;
438+ return null ;
439+ }
440+ }
441+
442+ // Note: Blocking webRequest listeners require webRequestBlocking permission,
443+ // which is deprecated in Manifest V3 and only available for enterprise extensions.
444+ // Instead, we use JavaScript-level interception via content scripts.
445+ // See src/injected.js for fetch/XMLHttpRequest interception.
446+
447+ console . log ( '[Podkey] NIP-98 auto-auth: Using JavaScript-level interception (see injected.js)' ) ;
0 commit comments