Skip to content

Commit abe31b2

Browse files
Merge pull request #2 from JavaScriptSolidServer/feature/nip98-auto-auth
Implement Automatic NIP-98 HTTP Authentication
2 parents c9939e6 + 6721ae4 commit abe31b2

4 files changed

Lines changed: 544 additions & 2 deletions

File tree

manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"web_accessible_resources": [
3333
{
3434
"resources": [
35-
"src/nostr-provider.js"
35+
"src/nostr-provider.js",
36+
"src/nip98-interceptor.js"
3637
],
3738
"matches": [
3839
"<all_urls>"

src/background.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1618
console.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
1928
chrome.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

Comments
 (0)