javascript-solid-server 0.0.71 → 0.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1465 @@
1
+ # Nostr-Solid Browser Extension Design
2
+
3
+ ## Executive Summary
4
+
5
+ A browser extension that enables seamless authentication to Solid servers using Nostr keys, eliminating the OAuth dance entirely. Users with existing Nostr identities can access protected Solid resources by signing HTTP requests with their Nostr keys.
6
+
7
+ ## Problem Statement
8
+
9
+ ### Current Solid Authentication UX
10
+
11
+ 1. User navigates to protected resource
12
+ 2. Server returns 401 Unauthorized
13
+ 3. User must initiate OAuth flow (Solid-OIDC)
14
+ 4. Redirect to Identity Provider with complex parameters
15
+ 5. Authenticate at IdP (username/password, WebAuthn, etc.)
16
+ 6. Redirect back with authorization code
17
+ 7. Token exchange happens
18
+ 8. Finally, access granted
19
+
20
+ **Pain points:**
21
+ - Multiple redirects
22
+ - Requires account creation at an IdP
23
+ - Complex client registration (client_id, redirect_uri)
24
+ - Different credentials for different pods
25
+ - Poor mobile experience
26
+
27
+ ### The Nostr SSO Opportunity
28
+
29
+ Nostr users already have:
30
+ - A cryptographic keypair (secp256k1)
31
+ - Key management via browser extensions (nos2x, Alby, nostr-keyx)
32
+ - A universal identity (npub) that works everywhere
33
+ - Experience signing events/messages
34
+
35
+ **With did:nostr authentication:**
36
+ 1. User navigates to protected resource
37
+ 2. Extension detects 401, signs request with Nostr key
38
+ 3. Server verifies signature, grants access
39
+ 4. Done. No redirects, no OAuth dance.
40
+
41
+ ## Current Nostr Extension Landscape
42
+
43
+ ### nos2x (Most Popular)
44
+ - **Capabilities:** `window.nostr.getPublicKey()`, `window.nostr.signEvent()`
45
+ - **Limitations:** Only signs NIP-07 events, not HTTP requests
46
+ - **Users:** ~50,000+
47
+
48
+ ### Alby
49
+ - **Capabilities:** NIP-07 + Lightning payments
50
+ - **Limitations:** Same as nos2x for signing
51
+ - **Users:** ~100,000+
52
+
53
+ ### nostr-keyx
54
+ - **Capabilities:** NIP-07 with better UX
55
+ - **Limitations:** Same signing limitations
56
+
57
+ ### What's Missing
58
+
59
+ None of these extensions can:
60
+ 1. Intercept HTTP 401 responses
61
+ 2. Sign HTTP request headers (not Nostr events)
62
+ 3. Automatically retry requests with authentication
63
+ 4. Understand Solid/WebID concepts
64
+
65
+ ## Architecture Options
66
+
67
+ ### Option A: Standalone Solid-Nostr Extension
68
+
69
+ A new extension specifically for Solid + Nostr authentication.
70
+
71
+ ```
72
+ ┌─────────────────────────────────────────────────────────┐
73
+ │ Browser Extension │
74
+ ├─────────────────────────────────────────────────────────┤
75
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
76
+ │ │ Content │ │ Background │ │ Popup │ │
77
+ │ │ Script │ │ Worker │ │ UI │ │
78
+ │ │ │ │ │ │ │ │
79
+ │ │ - Detect │ │ - Key mgmt │ │ - Settings │ │
80
+ │ │ 401s │ │ - Signing │ │ - Trusted sites │ │
81
+ │ │ - Inject │ │ - Storage │ │ - Key display │ │
82
+ │ │ headers │ │ │ │ │ │
83
+ │ └─────────────┘ └─────────────┘ └─────────────────┘ │
84
+ └─────────────────────────────────────────────────────────┘
85
+ ```
86
+
87
+ **Pros:**
88
+ - Full control over UX
89
+ - Can optimize for Solid use case
90
+ - No dependency on other extensions
91
+
92
+ **Cons:**
93
+ - Users need another extension
94
+ - Key management duplication
95
+ - Fragmented ecosystem
96
+
97
+ ### Option B: Companion to Existing Nostr Extensions
98
+
99
+ Leverage nos2x/Alby for key management, add HTTP signing layer.
100
+
101
+ ```
102
+ ┌─────────────────────────────────────────────────────────┐
103
+ │ Solid-Nostr Auth Extension │
104
+ ├─────────────────────────────────────────────────────────┤
105
+ │ ┌─────────────────────────────────────────────────┐ │
106
+ │ │ HTTP Request Interceptor │ │
107
+ │ │ - webRequest API for 401 detection │ │
108
+ │ │ - Retry logic with auth headers │ │
109
+ │ └─────────────────────────────────────────────────┘ │
110
+ │ │ │
111
+ │ ▼ │
112
+ │ ┌─────────────────────────────────────────────────┐ │
113
+ │ │ NIP-07 Bridge │ │
114
+ │ │ - window.nostr.getPublicKey() │ │
115
+ │ │ - window.nostr.signEvent() → adapt for HTTP │ │
116
+ │ └─────────────────────────────────────────────────┘ │
117
+ │ │ │
118
+ │ ▼ │
119
+ │ ┌─────────────────────────────────────────────────┐ │
120
+ │ │ nos2x / Alby / nostr-keyx │ │
121
+ │ │ (existing extension) │ │
122
+ │ └─────────────────────────────────────────────────┘ │
123
+ └─────────────────────────────────────────────────────────┘
124
+ ```
125
+
126
+ **Pros:**
127
+ - Leverages existing key management
128
+ - Users keep their preferred Nostr extension
129
+ - Smaller, focused extension
130
+
131
+ **Cons:**
132
+ - Dependency on NIP-07 extensions
133
+ - Signing HTTP != signing events (adaptation needed)
134
+
135
+ ### Option C: NIP Proposal for HTTP Signing
136
+
137
+ Propose a new NIP that standardizes HTTP request signing, then work with nos2x/Alby to implement.
138
+
139
+ **Pros:**
140
+ - Ecosystem-wide solution
141
+ - No new extension needed long-term
142
+
143
+ **Cons:**
144
+ - Slow adoption path
145
+ - Political/coordination challenges
146
+
147
+ ### Recommended: Option B (Companion Extension)
148
+
149
+ Best balance of pragmatism and user experience. Users likely already have nos2x or Alby installed.
150
+
151
+ ## Detailed Design
152
+
153
+ ### Extension Components
154
+
155
+ #### 1. Manifest (manifest.json)
156
+
157
+ ```json
158
+ {
159
+ "manifest_version": 3,
160
+ "name": "Solid Nostr Auth",
161
+ "version": "1.0.0",
162
+ "description": "Authenticate to Solid pods using your Nostr identity",
163
+ "permissions": [
164
+ "storage",
165
+ "webRequest",
166
+ "declarativeNetRequestWithHostAccess"
167
+ ],
168
+ "host_permissions": [
169
+ "<all_urls>"
170
+ ],
171
+ "background": {
172
+ "service_worker": "background.js"
173
+ },
174
+ "content_scripts": [
175
+ {
176
+ "matches": ["<all_urls>"],
177
+ "js": ["content.js"],
178
+ "run_at": "document_start"
179
+ }
180
+ ],
181
+ "action": {
182
+ "default_popup": "popup.html",
183
+ "default_icon": {
184
+ "16": "icons/icon16.png",
185
+ "48": "icons/icon48.png",
186
+ "128": "icons/icon128.png"
187
+ }
188
+ },
189
+ "icons": {
190
+ "16": "icons/icon16.png",
191
+ "48": "icons/icon48.png",
192
+ "128": "icons/icon128.png"
193
+ }
194
+ }
195
+ ```
196
+
197
+ #### 2. Background Service Worker (background.js)
198
+
199
+ ```javascript
200
+ /**
201
+ * Solid Nostr Auth - Background Service Worker
202
+ *
203
+ * Responsibilities:
204
+ * - Listen for 401 responses from Solid servers
205
+ * - Coordinate with content script to sign requests
206
+ * - Manage trusted sites list
207
+ * - Handle retry logic
208
+ */
209
+
210
+ // Storage keys
211
+ const STORAGE_KEYS = {
212
+ TRUSTED_SITES: 'trustedSites',
213
+ AUTO_SIGN: 'autoSign',
214
+ PUBKEY_CACHE: 'pubkeyCache'
215
+ };
216
+
217
+ // Track pending auth requests to avoid loops
218
+ const pendingAuth = new Map();
219
+
220
+ /**
221
+ * Detect if a site is a Solid server by checking response headers
222
+ */
223
+ function isSolidServer(headers) {
224
+ const dominated = ['solid', 'ms-author-via', 'wac-allow', 'updates-via'];
225
+ return headers.some(h =>
226
+ dominated.some(d => h.name.toLowerCase().includes(d))
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Listen for 401 responses
232
+ */
233
+ chrome.webRequest.onCompleted.addListener(
234
+ async (details) => {
235
+ // Only handle 401s
236
+ if (details.statusCode !== 401) return;
237
+
238
+ // Avoid infinite loops
239
+ const requestKey = `${details.method}:${details.url}`;
240
+ if (pendingAuth.has(requestKey)) return;
241
+
242
+ // Check if it's a Solid server
243
+ if (!isSolidServer(details.responseHeaders || [])) return;
244
+
245
+ // Check WWW-Authenticate header for Nostr support
246
+ const wwwAuth = details.responseHeaders?.find(
247
+ h => h.name.toLowerCase() === 'www-authenticate'
248
+ );
249
+
250
+ // Get user settings
251
+ const settings = await chrome.storage.local.get([
252
+ STORAGE_KEYS.TRUSTED_SITES,
253
+ STORAGE_KEYS.AUTO_SIGN
254
+ ]);
255
+
256
+ const origin = new URL(details.url).origin;
257
+ const trustedSites = settings[STORAGE_KEYS.TRUSTED_SITES] || [];
258
+ const autoSign = settings[STORAGE_KEYS.AUTO_SIGN] ?? false;
259
+
260
+ // If auto-sign enabled for trusted sites, or site is trusted
261
+ if (trustedSites.includes(origin) || autoSign) {
262
+ // Mark as pending to avoid loops
263
+ pendingAuth.set(requestKey, Date.now());
264
+
265
+ // Send message to content script to initiate signing
266
+ try {
267
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
268
+ if (tab) {
269
+ chrome.tabs.sendMessage(tab.id, {
270
+ type: 'SIGN_REQUEST',
271
+ url: details.url,
272
+ method: details.method
273
+ });
274
+ }
275
+ } catch (err) {
276
+ console.error('Failed to send sign request:', err);
277
+ }
278
+
279
+ // Clean up pending after timeout
280
+ setTimeout(() => pendingAuth.delete(requestKey), 30000);
281
+ } else {
282
+ // Prompt user to trust this site
283
+ chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
284
+ if (tab) {
285
+ chrome.tabs.sendMessage(tab.id, {
286
+ type: 'PROMPT_TRUST',
287
+ origin,
288
+ url: details.url
289
+ });
290
+ }
291
+ });
292
+ }
293
+ },
294
+ { urls: ['<all_urls>'] },
295
+ ['responseHeaders']
296
+ );
297
+
298
+ /**
299
+ * Handle messages from content script and popup
300
+ */
301
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
302
+ switch (message.type) {
303
+ case 'ADD_TRUSTED_SITE':
304
+ addTrustedSite(message.origin).then(sendResponse);
305
+ return true;
306
+
307
+ case 'REMOVE_TRUSTED_SITE':
308
+ removeTrustedSite(message.origin).then(sendResponse);
309
+ return true;
310
+
311
+ case 'GET_TRUSTED_SITES':
312
+ getTrustedSites().then(sendResponse);
313
+ return true;
314
+
315
+ case 'AUTH_COMPLETE':
316
+ // Clean up pending auth
317
+ const key = `${message.method}:${message.url}`;
318
+ pendingAuth.delete(key);
319
+ break;
320
+
321
+ case 'SET_AUTO_SIGN':
322
+ chrome.storage.local.set({ [STORAGE_KEYS.AUTO_SIGN]: message.enabled });
323
+ break;
324
+ }
325
+ });
326
+
327
+ async function addTrustedSite(origin) {
328
+ const { trustedSites = [] } = await chrome.storage.local.get(STORAGE_KEYS.TRUSTED_SITES);
329
+ if (!trustedSites.includes(origin)) {
330
+ trustedSites.push(origin);
331
+ await chrome.storage.local.set({ [STORAGE_KEYS.TRUSTED_SITES]: trustedSites });
332
+ }
333
+ return trustedSites;
334
+ }
335
+
336
+ async function removeTrustedSite(origin) {
337
+ const { trustedSites = [] } = await chrome.storage.local.get(STORAGE_KEYS.TRUSTED_SITES);
338
+ const filtered = trustedSites.filter(s => s !== origin);
339
+ await chrome.storage.local.set({ [STORAGE_KEYS.TRUSTED_SITES]: filtered });
340
+ return filtered;
341
+ }
342
+
343
+ async function getTrustedSites() {
344
+ const { trustedSites = [] } = await chrome.storage.local.get(STORAGE_KEYS.TRUSTED_SITES);
345
+ return trustedSites;
346
+ }
347
+ ```
348
+
349
+ #### 3. Content Script (content.js)
350
+
351
+ ```javascript
352
+ /**
353
+ * Solid Nostr Auth - Content Script
354
+ *
355
+ * Responsibilities:
356
+ * - Bridge to NIP-07 (window.nostr)
357
+ * - Sign HTTP requests using Nostr keys
358
+ * - Inject trust prompts into page
359
+ * - Retry requests with authentication
360
+ */
361
+
362
+ // Check for NIP-07 extension
363
+ let nostrAvailable = false;
364
+ let nostrCheckAttempts = 0;
365
+
366
+ function checkNostr() {
367
+ if (window.nostr) {
368
+ nostrAvailable = true;
369
+ console.log('[Solid Nostr Auth] NIP-07 extension detected');
370
+ return true;
371
+ }
372
+ if (nostrCheckAttempts++ < 10) {
373
+ setTimeout(checkNostr, 100);
374
+ }
375
+ return false;
376
+ }
377
+
378
+ checkNostr();
379
+
380
+ /**
381
+ * Generate HTTP Nostr Authorization header
382
+ *
383
+ * Format: Nostr <base64-encoded-signed-event>
384
+ *
385
+ * The signed event contains:
386
+ * - kind: 27235 (HTTP Auth - proposed)
387
+ * - content: empty
388
+ * - tags: [["u", url], ["method", method]]
389
+ * - created_at: current timestamp
390
+ */
391
+ async function generateNostrAuthHeader(url, method) {
392
+ if (!window.nostr) {
393
+ throw new Error('NIP-07 extension not available');
394
+ }
395
+
396
+ const pubkey = await window.nostr.getPublicKey();
397
+ const timestamp = Math.floor(Date.now() / 1000);
398
+
399
+ // Create event for signing (NIP-98 HTTP Auth style)
400
+ const event = {
401
+ kind: 27235, // HTTP Auth event kind
402
+ created_at: timestamp,
403
+ tags: [
404
+ ['u', url],
405
+ ['method', method.toUpperCase()]
406
+ ],
407
+ content: '',
408
+ pubkey
409
+ };
410
+
411
+ // Sign the event using NIP-07
412
+ const signedEvent = await window.nostr.signEvent(event);
413
+
414
+ // Encode as base64 for Authorization header
415
+ const encoded = btoa(JSON.stringify(signedEvent));
416
+
417
+ return `Nostr ${encoded}`;
418
+ }
419
+
420
+ /**
421
+ * Retry a request with Nostr authentication
422
+ */
423
+ async function retryWithAuth(url, method) {
424
+ try {
425
+ const authHeader = await generateNostrAuthHeader(url, method);
426
+
427
+ const response = await fetch(url, {
428
+ method,
429
+ headers: {
430
+ 'Authorization': authHeader
431
+ },
432
+ credentials: 'omit' // Don't send cookies, we're using Nostr auth
433
+ });
434
+
435
+ if (response.ok) {
436
+ // Reload the page to show authenticated content
437
+ window.location.reload();
438
+ } else {
439
+ console.error('[Solid Nostr Auth] Auth failed:', response.status);
440
+ showNotification('Authentication failed. Check server logs.', 'error');
441
+ }
442
+
443
+ // Notify background script
444
+ chrome.runtime.sendMessage({
445
+ type: 'AUTH_COMPLETE',
446
+ url,
447
+ method,
448
+ success: response.ok
449
+ });
450
+
451
+ } catch (err) {
452
+ console.error('[Solid Nostr Auth] Error:', err);
453
+ showNotification(`Error: ${err.message}`, 'error');
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Show trust prompt UI
459
+ */
460
+ function showTrustPrompt(origin, url) {
461
+ // Remove existing prompt if any
462
+ const existing = document.getElementById('solid-nostr-auth-prompt');
463
+ if (existing) existing.remove();
464
+
465
+ const prompt = document.createElement('div');
466
+ prompt.id = 'solid-nostr-auth-prompt';
467
+ prompt.innerHTML = `
468
+ <style>
469
+ #solid-nostr-auth-prompt {
470
+ position: fixed;
471
+ top: 20px;
472
+ right: 20px;
473
+ z-index: 999999;
474
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
475
+ color: white;
476
+ padding: 20px;
477
+ border-radius: 12px;
478
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
479
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
480
+ max-width: 360px;
481
+ animation: slideIn 0.3s ease;
482
+ }
483
+ @keyframes slideIn {
484
+ from { transform: translateX(100%); opacity: 0; }
485
+ to { transform: translateX(0); opacity: 1; }
486
+ }
487
+ #solid-nostr-auth-prompt h3 {
488
+ margin: 0 0 12px 0;
489
+ font-size: 16px;
490
+ display: flex;
491
+ align-items: center;
492
+ gap: 8px;
493
+ }
494
+ #solid-nostr-auth-prompt p {
495
+ margin: 0 0 16px 0;
496
+ font-size: 14px;
497
+ color: #a0a0a0;
498
+ line-height: 1.5;
499
+ }
500
+ #solid-nostr-auth-prompt .origin {
501
+ background: rgba(255,255,255,0.1);
502
+ padding: 8px 12px;
503
+ border-radius: 6px;
504
+ font-family: monospace;
505
+ font-size: 13px;
506
+ margin-bottom: 16px;
507
+ word-break: break-all;
508
+ }
509
+ #solid-nostr-auth-prompt .buttons {
510
+ display: flex;
511
+ gap: 10px;
512
+ }
513
+ #solid-nostr-auth-prompt button {
514
+ flex: 1;
515
+ padding: 10px 16px;
516
+ border: none;
517
+ border-radius: 8px;
518
+ font-size: 14px;
519
+ font-weight: 500;
520
+ cursor: pointer;
521
+ transition: transform 0.1s, box-shadow 0.1s;
522
+ }
523
+ #solid-nostr-auth-prompt button:hover {
524
+ transform: translateY(-1px);
525
+ }
526
+ #solid-nostr-auth-prompt .btn-primary {
527
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
528
+ color: white;
529
+ }
530
+ #solid-nostr-auth-prompt .btn-primary:hover {
531
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
532
+ }
533
+ #solid-nostr-auth-prompt .btn-secondary {
534
+ background: rgba(255,255,255,0.1);
535
+ color: white;
536
+ }
537
+ #solid-nostr-auth-prompt .close {
538
+ position: absolute;
539
+ top: 10px;
540
+ right: 10px;
541
+ background: none;
542
+ border: none;
543
+ color: #666;
544
+ cursor: pointer;
545
+ font-size: 18px;
546
+ padding: 4px;
547
+ }
548
+ #solid-nostr-auth-prompt .nostr-icon {
549
+ width: 20px;
550
+ height: 20px;
551
+ }
552
+ </style>
553
+ <button class="close" onclick="this.parentElement.remove()">×</button>
554
+ <h3>
555
+ <svg class="nostr-icon" viewBox="0 0 256 256" fill="currentColor">
556
+ <path d="M128 0C57.3 0 0 57.3 0 128s57.3 128 128 128 128-57.3 128-128S198.7 0 128 0zm0 232c-57.3 0-104-46.7-104-104S70.7 24 128 24s104 46.7 104 104-46.7 104-104 104z"/>
557
+ <circle cx="128" cy="128" r="40"/>
558
+ </svg>
559
+ Sign in with Nostr?
560
+ </h3>
561
+ <p>This Solid server supports Nostr authentication. Would you like to sign in using your Nostr identity?</p>
562
+ <div class="origin">${origin}</div>
563
+ <div class="buttons">
564
+ <button class="btn-secondary" onclick="this.closest('#solid-nostr-auth-prompt').remove()">
565
+ Not now
566
+ </button>
567
+ <button class="btn-primary" id="solid-nostr-trust-btn">
568
+ Sign in
569
+ </button>
570
+ </div>
571
+ `;
572
+
573
+ document.body.appendChild(prompt);
574
+
575
+ // Handle trust button click
576
+ document.getElementById('solid-nostr-trust-btn').addEventListener('click', async () => {
577
+ if (!nostrAvailable) {
578
+ showNotification('Please install a Nostr extension (nos2x, Alby)', 'error');
579
+ return;
580
+ }
581
+
582
+ // Add to trusted sites
583
+ await chrome.runtime.sendMessage({
584
+ type: 'ADD_TRUSTED_SITE',
585
+ origin
586
+ });
587
+
588
+ // Remove prompt
589
+ prompt.remove();
590
+
591
+ // Retry the request with auth
592
+ await retryWithAuth(url, 'GET');
593
+ });
594
+ }
595
+
596
+ /**
597
+ * Show notification toast
598
+ */
599
+ function showNotification(message, type = 'info') {
600
+ const existing = document.getElementById('solid-nostr-notification');
601
+ if (existing) existing.remove();
602
+
603
+ const colors = {
604
+ info: '#3b82f6',
605
+ success: '#10b981',
606
+ error: '#ef4444'
607
+ };
608
+
609
+ const notification = document.createElement('div');
610
+ notification.id = 'solid-nostr-notification';
611
+ notification.style.cssText = `
612
+ position: fixed;
613
+ bottom: 20px;
614
+ right: 20px;
615
+ z-index: 999999;
616
+ background: ${colors[type]};
617
+ color: white;
618
+ padding: 12px 20px;
619
+ border-radius: 8px;
620
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
621
+ font-size: 14px;
622
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
623
+ animation: fadeIn 0.3s ease;
624
+ `;
625
+ notification.textContent = message;
626
+ document.body.appendChild(notification);
627
+
628
+ setTimeout(() => notification.remove(), 5000);
629
+ }
630
+
631
+ /**
632
+ * Listen for messages from background script
633
+ */
634
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
635
+ switch (message.type) {
636
+ case 'SIGN_REQUEST':
637
+ retryWithAuth(message.url, message.method);
638
+ break;
639
+
640
+ case 'PROMPT_TRUST':
641
+ showTrustPrompt(message.origin, message.url);
642
+ break;
643
+
644
+ case 'CHECK_NOSTR':
645
+ sendResponse({ available: nostrAvailable });
646
+ return true;
647
+ }
648
+ });
649
+
650
+ /**
651
+ * Intercept fetch for automatic signing (optional enhancement)
652
+ * This allows JavaScript apps to automatically get signed requests
653
+ */
654
+ const originalFetch = window.fetch;
655
+ window.fetch = async function(url, options = {}) {
656
+ const response = await originalFetch(url, options);
657
+
658
+ // If 401 and we have Nostr, offer to retry
659
+ if (response.status === 401 && nostrAvailable) {
660
+ // Check if this is a Solid server
661
+ const wacAllow = response.headers.get('WAC-Allow');
662
+ if (wacAllow) {
663
+ // This is a Solid server, could auto-retry with auth
664
+ // For now, just log - could enhance later
665
+ console.log('[Solid Nostr Auth] 401 from Solid server, auth available');
666
+ }
667
+ }
668
+
669
+ return response;
670
+ };
671
+ ```
672
+
673
+ #### 4. Popup UI (popup.html + popup.js)
674
+
675
+ ```html
676
+ <!DOCTYPE html>
677
+ <html>
678
+ <head>
679
+ <meta charset="UTF-8">
680
+ <style>
681
+ * {
682
+ box-sizing: border-box;
683
+ margin: 0;
684
+ padding: 0;
685
+ }
686
+
687
+ body {
688
+ width: 320px;
689
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
690
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
691
+ color: white;
692
+ padding: 20px;
693
+ }
694
+
695
+ .header {
696
+ display: flex;
697
+ align-items: center;
698
+ gap: 12px;
699
+ margin-bottom: 20px;
700
+ }
701
+
702
+ .header img {
703
+ width: 32px;
704
+ height: 32px;
705
+ }
706
+
707
+ .header h1 {
708
+ font-size: 16px;
709
+ font-weight: 600;
710
+ }
711
+
712
+ .section {
713
+ background: rgba(255,255,255,0.05);
714
+ border-radius: 10px;
715
+ padding: 16px;
716
+ margin-bottom: 16px;
717
+ }
718
+
719
+ .section h2 {
720
+ font-size: 12px;
721
+ text-transform: uppercase;
722
+ letter-spacing: 0.5px;
723
+ color: #888;
724
+ margin-bottom: 12px;
725
+ }
726
+
727
+ .identity {
728
+ display: flex;
729
+ align-items: center;
730
+ gap: 12px;
731
+ }
732
+
733
+ .identity-icon {
734
+ width: 40px;
735
+ height: 40px;
736
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
737
+ border-radius: 50%;
738
+ display: flex;
739
+ align-items: center;
740
+ justify-content: center;
741
+ }
742
+
743
+ .identity-info {
744
+ flex: 1;
745
+ }
746
+
747
+ .identity-npub {
748
+ font-family: monospace;
749
+ font-size: 12px;
750
+ color: #888;
751
+ word-break: break-all;
752
+ }
753
+
754
+ .status {
755
+ display: flex;
756
+ align-items: center;
757
+ gap: 8px;
758
+ font-size: 13px;
759
+ }
760
+
761
+ .status-dot {
762
+ width: 8px;
763
+ height: 8px;
764
+ border-radius: 50%;
765
+ }
766
+
767
+ .status-dot.connected {
768
+ background: #10b981;
769
+ }
770
+
771
+ .status-dot.disconnected {
772
+ background: #ef4444;
773
+ }
774
+
775
+ .toggle {
776
+ display: flex;
777
+ align-items: center;
778
+ justify-content: space-between;
779
+ padding: 12px 0;
780
+ border-bottom: 1px solid rgba(255,255,255,0.1);
781
+ }
782
+
783
+ .toggle:last-child {
784
+ border-bottom: none;
785
+ }
786
+
787
+ .toggle label {
788
+ font-size: 14px;
789
+ }
790
+
791
+ .toggle-switch {
792
+ width: 44px;
793
+ height: 24px;
794
+ background: rgba(255,255,255,0.2);
795
+ border-radius: 12px;
796
+ position: relative;
797
+ cursor: pointer;
798
+ transition: background 0.2s;
799
+ }
800
+
801
+ .toggle-switch.active {
802
+ background: #8b5cf6;
803
+ }
804
+
805
+ .toggle-switch::after {
806
+ content: '';
807
+ position: absolute;
808
+ top: 2px;
809
+ left: 2px;
810
+ width: 20px;
811
+ height: 20px;
812
+ background: white;
813
+ border-radius: 50%;
814
+ transition: transform 0.2s;
815
+ }
816
+
817
+ .toggle-switch.active::after {
818
+ transform: translateX(20px);
819
+ }
820
+
821
+ .trusted-sites {
822
+ max-height: 150px;
823
+ overflow-y: auto;
824
+ }
825
+
826
+ .trusted-site {
827
+ display: flex;
828
+ align-items: center;
829
+ justify-content: space-between;
830
+ padding: 8px 0;
831
+ border-bottom: 1px solid rgba(255,255,255,0.1);
832
+ }
833
+
834
+ .trusted-site:last-child {
835
+ border-bottom: none;
836
+ }
837
+
838
+ .trusted-site-url {
839
+ font-size: 13px;
840
+ font-family: monospace;
841
+ color: #ccc;
842
+ }
843
+
844
+ .trusted-site-remove {
845
+ background: none;
846
+ border: none;
847
+ color: #666;
848
+ cursor: pointer;
849
+ font-size: 16px;
850
+ }
851
+
852
+ .trusted-site-remove:hover {
853
+ color: #ef4444;
854
+ }
855
+
856
+ .empty {
857
+ color: #666;
858
+ font-size: 13px;
859
+ text-align: center;
860
+ padding: 20px;
861
+ }
862
+
863
+ .footer {
864
+ text-align: center;
865
+ font-size: 11px;
866
+ color: #666;
867
+ }
868
+
869
+ .footer a {
870
+ color: #8b5cf6;
871
+ text-decoration: none;
872
+ }
873
+ </style>
874
+ </head>
875
+ <body>
876
+ <div class="header">
877
+ <img src="icons/icon48.png" alt="Solid Nostr Auth">
878
+ <h1>Solid Nostr Auth</h1>
879
+ </div>
880
+
881
+ <div class="section">
882
+ <h2>Your Identity</h2>
883
+ <div id="identity-container">
884
+ <div class="status">
885
+ <span class="status-dot disconnected"></span>
886
+ <span>No Nostr extension detected</span>
887
+ </div>
888
+ </div>
889
+ </div>
890
+
891
+ <div class="section">
892
+ <h2>Settings</h2>
893
+ <div class="toggle">
894
+ <label>Auto-sign for trusted sites</label>
895
+ <div class="toggle-switch" id="auto-sign-toggle"></div>
896
+ </div>
897
+ </div>
898
+
899
+ <div class="section">
900
+ <h2>Trusted Sites</h2>
901
+ <div class="trusted-sites" id="trusted-sites">
902
+ <div class="empty">No trusted sites yet</div>
903
+ </div>
904
+ </div>
905
+
906
+ <div class="footer">
907
+ <a href="https://github.com/example/solid-nostr-auth">GitHub</a> ·
908
+ <a href="https://solidproject.org">Solid</a> ·
909
+ <a href="https://nostr.com">Nostr</a>
910
+ </div>
911
+
912
+ <script src="popup.js"></script>
913
+ </body>
914
+ </html>
915
+ ```
916
+
917
+ ```javascript
918
+ // popup.js
919
+
920
+ /**
921
+ * Solid Nostr Auth - Popup Script
922
+ */
923
+
924
+ // Elements
925
+ const identityContainer = document.getElementById('identity-container');
926
+ const autoSignToggle = document.getElementById('auto-sign-toggle');
927
+ const trustedSitesContainer = document.getElementById('trusted-sites');
928
+
929
+ /**
930
+ * Initialize popup
931
+ */
932
+ async function init() {
933
+ // Check for Nostr extension
934
+ await checkNostrExtension();
935
+
936
+ // Load settings
937
+ await loadSettings();
938
+
939
+ // Load trusted sites
940
+ await loadTrustedSites();
941
+
942
+ // Set up event listeners
943
+ setupEventListeners();
944
+ }
945
+
946
+ /**
947
+ * Check if NIP-07 extension is available
948
+ */
949
+ async function checkNostrExtension() {
950
+ try {
951
+ // Query active tab to check for window.nostr
952
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
953
+
954
+ const response = await chrome.tabs.sendMessage(tab.id, { type: 'CHECK_NOSTR' });
955
+
956
+ if (response?.available) {
957
+ // Get public key
958
+ const result = await chrome.scripting.executeScript({
959
+ target: { tabId: tab.id },
960
+ func: async () => {
961
+ if (window.nostr) {
962
+ return await window.nostr.getPublicKey();
963
+ }
964
+ return null;
965
+ }
966
+ });
967
+
968
+ const pubkey = result[0]?.result;
969
+
970
+ if (pubkey) {
971
+ showIdentity(pubkey);
972
+ return;
973
+ }
974
+ }
975
+ } catch (err) {
976
+ console.error('Error checking Nostr:', err);
977
+ }
978
+
979
+ showNoNostr();
980
+ }
981
+
982
+ /**
983
+ * Display identity when Nostr is available
984
+ */
985
+ function showIdentity(pubkey) {
986
+ // Convert to npub
987
+ const npub = hexToNpub(pubkey);
988
+ const shortNpub = npub.slice(0, 12) + '...' + npub.slice(-8);
989
+
990
+ identityContainer.innerHTML = `
991
+ <div class="identity">
992
+ <div class="identity-icon">
993
+ <svg width="20" height="20" viewBox="0 0 256 256" fill="white">
994
+ <circle cx="128" cy="128" r="40"/>
995
+ </svg>
996
+ </div>
997
+ <div class="identity-info">
998
+ <div class="status">
999
+ <span class="status-dot connected"></span>
1000
+ <span>Connected</span>
1001
+ </div>
1002
+ <div class="identity-npub">${shortNpub}</div>
1003
+ </div>
1004
+ </div>
1005
+ `;
1006
+ }
1007
+
1008
+ /**
1009
+ * Show message when no Nostr extension
1010
+ */
1011
+ function showNoNostr() {
1012
+ identityContainer.innerHTML = `
1013
+ <div class="status">
1014
+ <span class="status-dot disconnected"></span>
1015
+ <span>Install nos2x or Alby extension</span>
1016
+ </div>
1017
+ `;
1018
+ }
1019
+
1020
+ /**
1021
+ * Load settings from storage
1022
+ */
1023
+ async function loadSettings() {
1024
+ const { autoSign = false } = await chrome.storage.local.get('autoSign');
1025
+
1026
+ if (autoSign) {
1027
+ autoSignToggle.classList.add('active');
1028
+ }
1029
+ }
1030
+
1031
+ /**
1032
+ * Load trusted sites
1033
+ */
1034
+ async function loadTrustedSites() {
1035
+ const sites = await chrome.runtime.sendMessage({ type: 'GET_TRUSTED_SITES' });
1036
+
1037
+ if (!sites || sites.length === 0) {
1038
+ trustedSitesContainer.innerHTML = '<div class="empty">No trusted sites yet</div>';
1039
+ return;
1040
+ }
1041
+
1042
+ trustedSitesContainer.innerHTML = sites.map(site => `
1043
+ <div class="trusted-site" data-site="${site}">
1044
+ <span class="trusted-site-url">${new URL(site).hostname}</span>
1045
+ <button class="trusted-site-remove" title="Remove">×</button>
1046
+ </div>
1047
+ `).join('');
1048
+
1049
+ // Add remove handlers
1050
+ trustedSitesContainer.querySelectorAll('.trusted-site-remove').forEach(btn => {
1051
+ btn.addEventListener('click', async (e) => {
1052
+ const site = e.target.closest('.trusted-site').dataset.site;
1053
+ await chrome.runtime.sendMessage({ type: 'REMOVE_TRUSTED_SITE', origin: site });
1054
+ loadTrustedSites();
1055
+ });
1056
+ });
1057
+ }
1058
+
1059
+ /**
1060
+ * Set up event listeners
1061
+ */
1062
+ function setupEventListeners() {
1063
+ // Auto-sign toggle
1064
+ autoSignToggle.addEventListener('click', async () => {
1065
+ autoSignToggle.classList.toggle('active');
1066
+ const enabled = autoSignToggle.classList.contains('active');
1067
+ await chrome.runtime.sendMessage({ type: 'SET_AUTO_SIGN', enabled });
1068
+ });
1069
+ }
1070
+
1071
+ /**
1072
+ * Convert hex pubkey to npub (simplified - real impl needs bech32)
1073
+ */
1074
+ function hexToNpub(hex) {
1075
+ // This is a placeholder - real implementation needs bech32 encoding
1076
+ return 'npub1' + hex.slice(0, 59);
1077
+ }
1078
+
1079
+ // Initialize
1080
+ init();
1081
+ ```
1082
+
1083
+ ## Server-Side Implementation (JSS)
1084
+
1085
+ The server already supports Nostr authentication via `src/auth/nostr.js`. Key points:
1086
+
1087
+ ### Current Implementation
1088
+
1089
+ ```javascript
1090
+ // src/auth/nostr.js (existing)
1091
+
1092
+ /**
1093
+ * Verify Nostr HTTP authentication
1094
+ * Authorization: Nostr <base64-event>
1095
+ */
1096
+ export async function verifyNostrAuth(authHeader, request) {
1097
+ // Extract and decode the signed event
1098
+ const base64Event = authHeader.replace(/^Nostr\s+/i, '');
1099
+ const event = JSON.parse(atob(base64Event));
1100
+
1101
+ // Verify event signature
1102
+ if (!verifyEvent(event)) {
1103
+ return { valid: false, error: 'Invalid signature' };
1104
+ }
1105
+
1106
+ // Check event kind (27235 for HTTP auth)
1107
+ if (event.kind !== 27235) {
1108
+ return { valid: false, error: 'Invalid event kind' };
1109
+ }
1110
+
1111
+ // Verify URL and method match
1112
+ const urlTag = event.tags.find(t => t[0] === 'u');
1113
+ const methodTag = event.tags.find(t => t[0] === 'method');
1114
+
1115
+ // Check timestamp (within 60 seconds)
1116
+ const now = Math.floor(Date.now() / 1000);
1117
+ if (Math.abs(now - event.created_at) > 60) {
1118
+ return { valid: false, error: 'Event expired' };
1119
+ }
1120
+
1121
+ // Convert pubkey to did:nostr
1122
+ const npub = nip19.npubEncode(event.pubkey);
1123
+ const didNostr = `did:nostr:${npub}`;
1124
+
1125
+ return { valid: true, did: didNostr, pubkey: event.pubkey };
1126
+ }
1127
+ ```
1128
+
1129
+ ### did:nostr → WebID Resolution
1130
+
1131
+ ```javascript
1132
+ // src/auth/did-nostr.js (existing)
1133
+
1134
+ /**
1135
+ * Resolve did:nostr to WebID
1136
+ *
1137
+ * Resolution order:
1138
+ * 1. Check local pod registry
1139
+ * 2. Check NIP-05 identifier for WebID
1140
+ * 3. Construct default WebID from server
1141
+ */
1142
+ export async function resolveDidNostrToWebId(didNostr, serverBaseUrl) {
1143
+ const npub = didNostr.replace('did:nostr:', '');
1144
+
1145
+ // Check local registry first
1146
+ const localWebId = await checkLocalRegistry(npub);
1147
+ if (localWebId) return localWebId;
1148
+
1149
+ // Try NIP-05 resolution
1150
+ const nip05WebId = await resolveViaNip05(npub);
1151
+ if (nip05WebId) return nip05WebId;
1152
+
1153
+ // Default: server-local WebID
1154
+ return `${serverBaseUrl}/nostr/${npub}#me`;
1155
+ }
1156
+ ```
1157
+
1158
+ ## Security Considerations
1159
+
1160
+ ### 1. Replay Attack Prevention
1161
+
1162
+ ```javascript
1163
+ // Event must include timestamp and be within 60 seconds
1164
+ if (Math.abs(now - event.created_at) > 60) {
1165
+ return { valid: false, error: 'Event expired' };
1166
+ }
1167
+
1168
+ // Event must include exact URL being accessed
1169
+ const urlTag = event.tags.find(t => t[0] === 'u');
1170
+ if (urlTag[1] !== request.url) {
1171
+ return { valid: false, error: 'URL mismatch' };
1172
+ }
1173
+ ```
1174
+
1175
+ ### 2. Method Binding
1176
+
1177
+ ```javascript
1178
+ // Event must specify HTTP method
1179
+ const methodTag = event.tags.find(t => t[0] === 'method');
1180
+ if (methodTag[1] !== request.method) {
1181
+ return { valid: false, error: 'Method mismatch' };
1182
+ }
1183
+ ```
1184
+
1185
+ ### 3. Trusted Sites List
1186
+
1187
+ - Users explicitly approve each origin
1188
+ - Sites can be removed at any time
1189
+ - Optional auto-sign for convenience
1190
+
1191
+ ### 4. No Private Key Exposure
1192
+
1193
+ - Extension never touches private keys
1194
+ - All signing via NIP-07 (nos2x, Alby)
1195
+ - Keys stay in secure extension storage
1196
+
1197
+ ### 5. Content Security
1198
+
1199
+ - Extension uses minimal permissions
1200
+ - Content script isolated from page
1201
+ - No eval() or dynamic code execution
1202
+
1203
+ ## User Experience Flow
1204
+
1205
+ ### First-Time User
1206
+
1207
+ ```
1208
+ 1. User installs "Solid Nostr Auth" extension
1209
+ └─> Extension detects existing nos2x/Alby
1210
+
1211
+ 2. User navigates to https://alice.example.com/private/
1212
+ └─> Server returns 401
1213
+
1214
+ 3. Extension shows trust prompt:
1215
+ ┌────────────────────────────────────┐
1216
+ │ 🔐 Sign in with Nostr? │
1217
+ │ │
1218
+ │ This Solid server supports Nostr │
1219
+ │ authentication. │
1220
+ │ │
1221
+ │ ┌──────────────────────────────┐ │
1222
+ │ │ alice.example.com │ │
1223
+ │ └──────────────────────────────┘ │
1224
+ │ │
1225
+ │ [Not now] [Sign in] │
1226
+ └────────────────────────────────────┘
1227
+
1228
+ 4. User clicks "Sign in"
1229
+ └─> nos2x prompts to sign event
1230
+ └─> Extension retries request with auth
1231
+ └─> Page reloads with content
1232
+ ```
1233
+
1234
+ ### Returning User (Trusted Site)
1235
+
1236
+ ```
1237
+ 1. User navigates to https://alice.example.com/private/
1238
+ └─> Server returns 401
1239
+
1240
+ 2. Extension auto-signs (if enabled) or shows small notification
1241
+ └─> nos2x may prompt based on its settings
1242
+
1243
+ 3. Request retried with auth
1244
+ └─> Page loads immediately
1245
+ ```
1246
+
1247
+ ## Integration with SolidOS/Mashlib
1248
+
1249
+ ### Option 1: Extension-Only (Recommended for MVP)
1250
+
1251
+ Mashlib doesn't need changes. The extension handles auth before mashlib loads.
1252
+
1253
+ ```
1254
+ Browser navigates to protected resource
1255
+ └─> 401 returned (with mashlib HTML)
1256
+ └─> Extension intercepts, signs, retries
1257
+ └─> 200 returned, mashlib loads with authenticated context
1258
+ ```
1259
+
1260
+ ### Option 2: Mashlib Integration (Future)
1261
+
1262
+ Add Nostr as authentication option in mashlib's login UI.
1263
+
1264
+ ```javascript
1265
+ // In mashlib/src/login/
1266
+ class NostrAuthStrategy {
1267
+ async login() {
1268
+ if (!window.nostr) {
1269
+ throw new Error('Install a Nostr extension');
1270
+ }
1271
+
1272
+ const pubkey = await window.nostr.getPublicKey();
1273
+ const npub = nip19.npubEncode(pubkey);
1274
+
1275
+ // Store WebID for this session
1276
+ const webId = await this.resolveWebId(npub);
1277
+ this.setAuthenticated(webId);
1278
+ }
1279
+ }
1280
+ ```
1281
+
1282
+ ## NIP Proposal: HTTP Authentication (NIP-98)
1283
+
1284
+ For ecosystem alignment, we should formalize this as a NIP:
1285
+
1286
+ ```markdown
1287
+ NIP-98
1288
+ ======
1289
+
1290
+ HTTP Auth
1291
+ ---------
1292
+
1293
+ This NIP defines how Nostr keys can authenticate HTTP requests.
1294
+
1295
+ ## Event Format
1296
+
1297
+ Kind: 27235
1298
+
1299
+ Tags:
1300
+ - `u` - Full URL being accessed (required)
1301
+ - `method` - HTTP method (required)
1302
+ - `payload` - SHA-256 hash of request body (optional, for POST/PUT)
1303
+
1304
+ Content: Empty string
1305
+
1306
+ ## Authorization Header
1307
+
1308
+ Format: `Authorization: Nostr <base64-encoded-event>`
1309
+
1310
+ ## Example
1311
+
1312
+ ```json
1313
+ {
1314
+ "kind": 27235,
1315
+ "created_at": 1704067200,
1316
+ "tags": [
1317
+ ["u", "https://example.com/private/data.json"],
1318
+ ["method", "GET"]
1319
+ ],
1320
+ "content": "",
1321
+ "pubkey": "...",
1322
+ "id": "...",
1323
+ "sig": "..."
1324
+ }
1325
+ ```
1326
+
1327
+ ## Verification
1328
+
1329
+ 1. Decode base64 event from Authorization header
1330
+ 2. Verify event signature
1331
+ 3. Check kind is 27235
1332
+ 4. Verify `u` tag matches request URL
1333
+ 5. Verify `method` tag matches HTTP method
1334
+ 6. Check `created_at` is within 60 seconds
1335
+ 7. For POST/PUT, verify `payload` hash if present
1336
+ ```
1337
+
1338
+ ## Implementation Phases
1339
+
1340
+ ### Phase 1: MVP (1-2 weeks)
1341
+ - [ ] Basic extension structure
1342
+ - [ ] NIP-07 integration (use existing keys)
1343
+ - [ ] 401 detection and signing
1344
+ - [ ] Trust prompt UI
1345
+ - [ ] Popup with settings
1346
+
1347
+ ### Phase 2: Polish (1 week)
1348
+ - [ ] Auto-sign for trusted sites
1349
+ - [ ] Better error handling
1350
+ - [ ] Notification system
1351
+ - [ ] Icon states (authenticated vs not)
1352
+
1353
+ ### Phase 3: Ecosystem (2-3 weeks)
1354
+ - [ ] Submit NIP-98 proposal
1355
+ - [ ] Firefox extension port
1356
+ - [ ] Work with nos2x/Alby on native support
1357
+ - [ ] Mashlib integration PR
1358
+
1359
+ ### Phase 4: Advanced (Future)
1360
+ - [ ] Multiple key support
1361
+ - [ ] Per-site key selection
1362
+ - [ ] Key backup/recovery hints
1363
+ - [ ] Integration with DID resolvers
1364
+
1365
+ ## Testing Strategy
1366
+
1367
+ ### Manual Testing
1368
+ 1. Install extension + nos2x
1369
+ 2. Navigate to JSS server with protected resource
1370
+ 3. Verify trust prompt appears
1371
+ 4. Verify signing works
1372
+ 5. Verify page loads with content
1373
+
1374
+ ### Automated Testing
1375
+ ```javascript
1376
+ // test/extension.test.js
1377
+ describe('Solid Nostr Auth Extension', () => {
1378
+ it('should detect 401 from Solid server', async () => {
1379
+ // Use puppeteer/playwright with extension loaded
1380
+ });
1381
+
1382
+ it('should generate valid auth header', async () => {
1383
+ const header = await generateNostrAuthHeader(
1384
+ 'https://example.com/test',
1385
+ 'GET'
1386
+ );
1387
+ expect(header).toMatch(/^Nostr /);
1388
+ });
1389
+
1390
+ it('should verify auth on server', async () => {
1391
+ // End-to-end test with running JSS instance
1392
+ });
1393
+ });
1394
+ ```
1395
+
1396
+ ## Comparison with Alternatives
1397
+
1398
+ | Feature | Solid-OIDC | did:nostr + Extension | HTTP Signatures |
1399
+ |---------|------------|----------------------|-----------------|
1400
+ | Redirects | 2+ | 0 | 0 |
1401
+ | Account needed | Yes (IdP) | No | Yes (key reg) |
1402
+ | Mobile UX | Poor | Good* | Good |
1403
+ | Browser support | All | Chrome/Firefox | All |
1404
+ | Key portability | Limited | Excellent | Limited |
1405
+ | Setup complexity | High | Low | Medium |
1406
+
1407
+ *Requires mobile Nostr app with NIP-07 support
1408
+
1409
+ ## Open Questions
1410
+
1411
+ 1. **Should we support unsigned requests for public resources?**
1412
+ - Current: Yes, auth only added for 401s
1413
+ - Alternative: Always sign (proves identity even for public)
1414
+
1415
+ 2. **How to handle key rotation?**
1416
+ - Current: Not addressed
1417
+ - Option: Support multiple pubkeys per WebID
1418
+
1419
+ 3. **Should the extension manage its own keys?**
1420
+ - Current: No, rely on nos2x/Alby
1421
+ - Trade-off: Convenience vs security
1422
+
1423
+ 4. **How to surface Nostr identity in Solid apps?**
1424
+ - WebID profile could include Nostr pubkey
1425
+ - Apps could show npub alongside WebID
1426
+
1427
+ ## Appendix: Related Standards
1428
+
1429
+ - [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md): Browser Extension Signing
1430
+ - [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md): DNS-based Nostr Identity
1431
+ - [DID:nostr](https://github.com/pnp/did-nostr): DID Method for Nostr
1432
+ - [Solid-OIDC](https://solidproject.org/TR/oidc): Current Solid Auth Spec
1433
+ - [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures): IETF Draft
1434
+
1435
+ ## Repository Structure
1436
+
1437
+ ```
1438
+ solid-nostr-auth/
1439
+ ├── manifest.json
1440
+ ├── background.js
1441
+ ├── content.js
1442
+ ├── popup.html
1443
+ ├── popup.js
1444
+ ├── icons/
1445
+ │ ├── icon16.png
1446
+ │ ├── icon48.png
1447
+ │ └── icon128.png
1448
+ ├── lib/
1449
+ │ └── nostr-tools.min.js (for bech32/nip19)
1450
+ ├── test/
1451
+ │ └── extension.test.js
1452
+ ├── README.md
1453
+ └── LICENSE
1454
+ ```
1455
+
1456
+ ## Conclusion
1457
+
1458
+ A browser extension bridging Nostr keys to Solid authentication provides:
1459
+
1460
+ 1. **Immediate UX improvement**: No OAuth dance, no redirects
1461
+ 2. **Leverages existing infrastructure**: nos2x, Alby already installed
1462
+ 3. **True SSO**: Same identity across all Solid pods
1463
+ 4. **Progressive enhancement**: Falls back to Solid-OIDC gracefully
1464
+
1465
+ The extension is lightweight (~50KB), focused, and can be built incrementally while working toward ecosystem-wide adoption through NIP standardization.